views:

215

answers:

6

So I build an array of various dates. Birthdays, anniversaries, and holidays. I'd like to order the array by which one is happening next, essentially sort October to September (wrapping to next year)

so if my array is

$a = ([0]=>"1980-04-14", [1]=>"2007-06-08", 
  [2]=>"2008-12-25", [3]=>"1978-11-03")

I'd like to sort it so it is arranged

$a = ([0]=>"1978-11-03", [1]=>"2008-12-25", 
  [2]=>"1980-04-14", [3]=>"2007-06-08")

because the november 'event' is the one that will happen next (based on it being october right now).

I'm trying usort where my cmp function is

function cmp($a, $b)
{
  $a_tmp = split("-", $a);
  $b_tmp = split("-", $b);
  return strcmp($a_tmp[1], $b_tmp[1]);
}

I am not sure how to modify this to get my desired effect.

A: 

Don't compare strings, instead use seconds since 1970 (ints):

$date1 = split("-", $a);
$date2 = split("-", $b);
$seconds1 = mktime(0,0,0,$date1[1],$date1[2],$date1[0]);
$seconds2 = mktime(0,0,0,$date2[1],$date2[2],$date2[0]);
// eliminate years
$seconds1 %= 31536000;
$seconds2 %= 31536000;
return $seconds1 - $seconds2;

Also I don't know PHP but I think the gist is correct.

Edit: The comparison function is encapsulated to perform comparison, nothing more. To order a list in regards to the original question sort an array with today's date included, locate today's date in the array, and then move the elements before that position to the end in ascending order by position.

cfeduke
Nowhere near correct, I'm afraid.
Jonathan Leffler
Mmm yes you'd have to modulo ticks1 and ticks2 by 31536000 to eliminate years but it'd work correctly, I'm afraid.
cfeduke
+1  A: 

I would be tempted to establish the original year of the event, and then add enough whole years to it to ensure that the value is greater than your reference date (normally today's date). Or, possibly, greater than or equal to the reference date. You can then sort in simple date order.

Edited to add:

I'm not fluent enough in PHP to give an answer in that, but here's a Perl solution.

#!/bin/perl -w

# Sort sequence of dates by next occurrence of anniversary.
# Today's "birthdays" count as low (will appear first in sequence)

use strict;

my $refdate = "2008-10-05";

my @list = (
    "1980-04-14", "2007-06-08",
    "2008-12-25", "1978-11-03",
    "2008-10-04", "2008-10-05",
    "2008-10-06", "2008-02-29"
);

sub date_on_or_after
{
    my($actdate, $refdate) = @_;
    my($answer) = $actdate;
    if ($actdate lt $refdate)   # String compare OK with ISO8601 format
    {
        my($act_yy, $act_mm, $act_dd) = split /-/, $actdate;
        my($ref_yy, $ref_mm, $ref_dd) = split /-/, $refdate;
        $ref_yy++ if ($act_mm < $ref_mm || ($act_mm == $ref_mm && $act_dd < $ref_dd));
        $answer = "$ref_yy-$act_mm-$act_dd";
    }
    return $answer;
}

sub anniversary_compare
{
    my $r1 = date_on_or_after($a, $refdate);
    my $r2 = date_on_or_after($b, $refdate);
    return $r1 cmp $r2;
}

my @result = sort anniversary_compare @list;

print "Before:\n";
print "* $_\n" foreach (@list);
print "Reference date: $refdate\n";
print "After:\n";
print "* $_\n" foreach (@result);

Clearly, this is not dreadfully efficient - to make it efficient, you'd calculate the date_on_or_after() value once, and then sort on those values. Perl's comparison is slightly peculiar - the variables $a and $b are magic, and appear as if out of nowhere.

When run, the script produces:

Before:
* 1980-04-14
* 2007-06-08
* 2008-12-25
* 1978-11-03
* 2008-10-04
* 2008-10-05
* 2008-10-06
* 2008-02-29
Reference date: 2008-10-05
After:
* 2008-10-05
* 2008-10-06
* 1978-11-03
* 2008-12-25
* 2008-02-29
* 1980-04-14
* 2007-06-08
* 2008-10-04

Note that it largely ducks the issue of what happens with the 29th of February, because it 'works' to do so. Basically, it will generate the 'date' 2009-02-29, which compares correctly in sequence. The anniversary for 2000-02-28 would be listed before the anniversary for 2008-02-29 (if 2000-02-28 were included in the data).

Jonathan Leffler
I suspect mine is not very efficent either. But I also don't feel like mine has an 02-29 bug.
Jack B Nimble
A: 

So it occured to me just to add 12 to any month that is less than my target month. Which is now working.

so the final function

function cmp($a, $b)
{
$a_tmp = explode("-", $a["date"]);
$b_tmp = explode("-", $b["date"]);
if ($a_tmp[1] < date("m"))
{
  $a_tmp[1] += 12;
}
if ($b_tmp[1] < date("m"))
{
  $b_tmp[1] += 12;
}
return strcmp($a_tmp[1] . $a_tmp[2], $b_tmp[1] . $b_tmp[2]);
}
Jack B Nimble
That's not going to work if you try to include a day in the month that has already passed (e.g., if you include 10/1/2000 in your list).
Randy
Good point. I changed the compare to month-day.
Jack B Nimble
What happens when one of the dates is 2008-02-29?
Jonathan Leffler
Given today is 2008-10-05, how does your code show handle entries 2008-10-04, 2008-10-05, 2008-10-06? In particular, note that the next celebration of 2008-10-04 is later than all the other dates under consideration. You have not defined whether today's date counts as this year or next year.
Jonathan Leffler
And, bother - the 'show' in my previous comment is superfluous.
Jonathan Leffler
There is a potential for 10-04 to be displayed. At this point I am not concerned about it. I might have to add 30 to any day above the number of the current one. It is just string sorting at this point, so days like 02-29 and 10-45 don't matter, it just sorts them by order.
Jack B Nimble
Just use the day of the year ('date("z", mktime(0,0,0,$d,$m,$y))') for comparisons, you can ignore month and year and not worry about leap years.
cfeduke
+2  A: 
function relative_year_day($date) {
    $value = date('z', strtotime($date)) - date('z');

    if ($value < 0)
        $value += 365;

    return $value;
}

function cmp($a, $b)
{
    $aValue = relative_year_day($a);
    $bValue = relative_year_day($b);

    if ($aValue == $bValue)
        return 0;

    return ($aValue < $bValue) ? -1 : 1;
}

$a = array("1980-04-14", "2007-06-08",
    "2008-12-25", "1978-11-03");

usort($a, "cmp");
Neil Williams
A: 

use strtotime() to convert the all dates to a timestamp before you add them to the array, then you can sort the array into ascending (also chronological) order. Now all you have to do is deal with the dates in the past which is easily done by comparing them against a current timestamp

i.e.

for ($i=0; $i<count($a); $i++){
  if ($currentTimestamp > $a[$i]){
    unset($a[$i]);
  }
}
JimmyJ
This is missing the point of the question.
Jonathan Leffler
A: 

No reason to reinvent the wheel. If you don't care about the keys you can use this.

$a = array_combine(array_map('strtotime', $a), $a);
ksort($a);

Or if you want to define your own callback.

function dateCmp($date1, $date2) {
  return (strtotime($date1) > strtotime($date2))?1:-1;
}

usort($a, 'dateCmp');

If you want to keep the keys associated correctly just call uasort instead.

uasort($a, 'dateCmp');

I did a quick speed check and the callback functions were over a magnitude slower.

gradbot