1

Using one already chosen date: Test example

$dates[] = array("date" => "2016-02-18 02:00:00", "duration" => "600"); // 10 mins
$dates[] = array("date" => "2016-02-18 02:05:00", "duration" => "300"); // 5 mins
$dates[] = array("date" => "2016-02-18 02:10:00", "duration" => "600");
$dates[] = array("date" => "2016-02-18 02:25:00", "duration" => "300");
$dates[] = array("date" => "2016-02-18 02:30:00", "duration" => "600");

$alreadyChosenDates[] = array("date" => "2016-02-18 02:10:30", "duration" => "600"); // 10 mins
//$alreadyChosenDates[] = array("date" => "2016-02-18 02:05:00", "duration" => "300"); // 5 mins

function returnClosestTime($alreadyChosenDates, $dates){
    // Set an array called $closestTime that has the time difference and the key
    $closestTime = [null, null];

    // Check each element in array
    foreach($dates as $key => $date){
        foreach($alreadyChosenDates as $chosenDates){
            // Calculate difference between already chosen dates array and the dates array
            $diff = (strtotime($chosenDates["date"]) + $chosenDates["duration"]) - strtotime($date["date"]);
            if($diff < 0) $diff = $diff * -1; 

            // If $closestTime is empty, populate it
            if($closestTime[0] === null) $closestTime = [$diff, $key];

            // If $closestTime isn't empty and the current date's time difference
            // is smaller, populate $closestTime with the time difference and key
            else if($diff < $closestTime[0]) $closestTime = [$diff, $key];
        }
    }
    return $dates[$closestTime[1]];
}

echo "<pre>";
    print_r(returnClosestTime($alreadyChosenDates, $dates));
echo "</pre>";

Output:

Array
(
    [date] => 2016-02-18 02:25:00
    [duration] => 300
)

In the above example i'm using only one already chosen date but i'm looking for some help on how i'd use two already chosen dates?

so instead of just this:

$alreadyChosenDates[] = array("date" => "2016-02-18 02:10:30", "duration" => "600"); // 10 mins

It would be this

$alreadyChosenDates[] = array("date" => "2016-02-18 02:10:30", "duration" => "600"); // 10 mins
$alreadyChosenDates[] = array("date" => "2016-02-18 02:05:00", "duration" => "300"); // 5 mins

and the closest date/time would be the closest to either 2016-02-18 02:10:30 or 2016-02-18 02:05:00 but it would also be good to see if the closest time would be in the middle of 2016-02-18 02:10:30 and 2016-02-18 02:05:00.

How would I go about doing this?

YaBCK
  • 2,949
  • 4
  • 32
  • 61
  • Maybe you are trying to invent a kind of [clustering algorithm](https://en.wikipedia.org/wiki/Cluster_analysis)? Look at these examples: [1](https://en.wikipedia.org/wiki/Hierarchical_clustering), [2](https://en.wikipedia.org/wiki/K-means_clustering) – Ivan Velichko Nov 07 '16 at 10:11
  • @IvanVelichko No i'm just trying to get the closest date/time – YaBCK Nov 07 '16 at 10:15
  • 1
    thoughts: For each 'not chosen' record, work out the absolute difference between each chosen record and sum them. Repeat for each record and keep the lowest sum. i.e .try and find the one record with a total local minimum time difference from all the chosen records? – Ryan Vincent Nov 09 '16 at 13:04
  • @RyanVincent could you show an example? – YaBCK Nov 09 '16 at 13:52
  • 1
    Hmm, starting to work through it. the duration makes it 'tricky'. It just got interesting :) I notice that you can use any 'chosen time' that you want - it doesn't have to be in the array. Also makes it 'interesting' :) – Ryan Vincent Nov 09 '16 at 15:18
  • In your code, you never use `"duration"` from `$dates` array elements. Is this intended? – sevavietl Nov 09 '16 at 19:03
  • What is the significance of `duration`? I have interpreted it as meaning that each record is really a 'time window' of `start date/time` to `end-date/time`. Ok so far. However, you have overlapping and nested ones (see first two). Your 'chosenTime' also is a 'time window'. So, how can I work out the closest record when multiple `time windows` overlap the `chosenTime` window? – Ryan Vincent Nov 09 '16 at 20:23
  • @RyanVincent - I don't want overlapping, if they overlap then don't push them into the chosenTime. So I basically want all the times which can fit around each other. Do you understand that, you can edit my code as much as possible if it means getting the result. You don't have to keep my code as it is. – YaBCK Nov 10 '16 at 09:06
  • Ok, I will give the 'none-overlapping' a go and see what happens :) – Ryan Vincent Nov 10 '16 at 09:33
  • @RyanVincent Thank you for taking your time to look at this :) – YaBCK Nov 10 '16 at 09:36
  • 1
    @RyanVincent Just quick addition... if there is a time at lets say `02:00:00` with duration of `600` making the endtime `02:10:00`. It should allow a time to be added to `chosenTime` which is the same as `02:10:00` – YaBCK Nov 10 '16 at 09:38
  • @RyanVincent - Your answer to my other bounty is the answer to this question...If you want to answer this one aswell. Then you can get the bounty from this question aswell ;) – YaBCK Nov 14 '16 at 09:32

1 Answers1

2

I don't know if you would like the OOP approach, but you can use this class:

class ClosestIndicationFinder
{
    protected $indications;
    protected $chosenIndications;

    public function __construct(
        array $indications,
        array $chosenIndications = null
    ) {
        $this->indications = $indications;
        $this->chosenIndications = $chosenIndications;
    }

    public function setChosenIndications(array $chosenIndications)
    {
        $this->chosenIndications = $chosenIndications;

        return $this;
    }

    public function addChosenIndication($indication)
    {
        $this->chosenIndications[] = $indication;

        return $this;
    }

    public function findClosestIndication()
    {
        $this->findAverageChosenIndicationTimestamp();
        $this->findAbsoluteIndicationDifferences();

        return $this->findClosestsIndicationBasedOnDiffs();
    }

    protected $averageChosenTimestamp;
    protected function findAverageChosenIndicationTimestamp()
    {
        $sum = array_reduce(
            $this->chosenIndications,
            function ($sum, $indication) {
                return $sum + $this->calculateIndicationEndTimestamp($indication);
            },
            0
        );

        $this->averageChosenTimestamp = (int) $sum / count($this->chosenIndications);
    }

    protected $diffs;
    protected function findAbsoluteIndicationDifferences()
    {
        $this->diffs = array_map(function ($indication) {
            return abs($this->calculateIndicationsDifference(
                $indication,
                $this->averageChosenTimestamp
            ));
        }, $this->indications);
    }

    protected function calculateIndicationsDifference($indication1, $indication2)
    {
        $timestamp1 = is_array($indication1) 
            ? $this->calculateIndicationBeginningTimestamp($indication1)
            : $indication1;

        $timestamp2 = is_array($indication2) 
            ? $this->calculateIndicationEndTimestamp($indication2) 
            : $indication2;

        return $timestamp1 - $timestamp2;
    }

    protected function calculateIndicationBeginningTimestamp(array $indication)
    {
        return strtotime($indication['date']);
    }

    protected function calculateIndicationEndTimestamp(array $indication)
    {
        return strtotime($indication['date']) + $indication['duration'];
    }

    protected function findClosestsIndicationBasedOnDiffs()
    {
        $closestIndicationIndex = array_search(min($this->diffs), $this->diffs);

        if (!isset($this->indications[$closestIndicationIndex])) {
            return null;
        }

        return $this->indications[$closestIndicationIndex];
    }
}

I have tried to make methods self-describing, so there are no comments in the code. The OOP approach makes it possible to tweak the logic by extending the class and rewriting only those methods which behavior should be changed. For example, I saw your other question, where you want to add a newly chosen date to the chosen array. There is also a method for this in the class, however, I would remove the chosen date from initial dates array. But as I know only the end task it is hard for me to make the solution closely fit the initial task.

As for multiple chosen times. You have written "but it would also be good to see if the closest time would be in the middle of". Correct me if I am wrong but, if you want the date, that resides in the middle of the two dates closest to chosen dates, you can calculate the average of chosen dates and reduce the task to find only one closest date.

Once again if you can tell what are you trying to do, the solution can be made more accurately.

By the way, here is working demo.

sevavietl
  • 3,762
  • 1
  • 14
  • 21
  • So if I already had a time in `$alreadyChosenDates` for example: `2016-02-18 02:10:00` with duration of `600` this would result in endTime being `2016-02-18 02:20:00` - So I want to find the closest time either before or after the time. However `startTime/endTimes` should never overlap. - I write a better question here for what I need please check it out: http://stackoverflow.com/questions/40505794/how-to-push-the-next-date-time-to-array-which-fits-around-current-date-times-in – YaBCK Nov 10 '16 at 12:06