http://qs321.pair.com?node_id=46759

electronicMacks has asked for the wisdom of the Perl Monks concerning the following question:

Fellow Monks,

I’m writing a program that parses a log containing dates in the format mm/dd/yy, I store these dates in a hash with their respective data, and at the end of the program I’d like to report the dates in order.

I’m thinking something along the lines of:
foreach $day (sort (keys (%totals))) {
But with an appropriate argument given to sort.

I could certainly write my own subroutine to sort it, but I feel like I’d be reinventing the wheel, does anyone know of an existing, elegant way to sort mm/dd/yy dates???

Replies are listed 'Best First'.
(tye)Re: sorting mm/dd/yy
by tye (Sage) on Dec 15, 2000 at 04:17 UTC

    Convert to the one true date format of yyyy-mm-dd and just use the default sort:

    foreach( keys %totals ) { $totals{$_}= "20" . join "-",( split m#/#, delete $totals{$_} )[2,0,1]; }
    and thank vroom that the y2k thing finally blew over so you don't have to do that 2-digit-to-4-digit year conversion thing the "proper" way anymore.

    update: got the one true date format wrong the first time... i'm so ashamed...

            - tye (and not even my friends are sure when I'm joking)
      ++'s to tye for stating the answer which should always be considered first:P Why work so hard when the work has already been done for you?

      unix epoch is good too as long as you don't have to do too much date interaction with users. I love using straight time() in my programs because I get everything I could ever want to know in one shot
      If there is ever a chance to switch date formats, YYYYMMDD and unix epoch rank right up there.

      If that's any help convincing your boss, customer or yourself that this is an acceptable format for date it is an ISO standard, ISO-8601 (pdf) and I found this page to be a very good description of the standard (TIMTOWTFAD - There Is More Than One Way To Format A Date) and why use it.

(Date::Manip) Re: sorting mm/dd/yy
by mwp (Hermit) on Dec 15, 2000 at 04:10 UTC
    A quick search of the Date::Manip perldoc yields this:
    use Date::Manip qw(ParseDate Date_Cmp); sub sortDate { my($date1, $date2); $date1 = &ParseDate($a); $date2 = &ParseDate($b); return (&Date_Cmp($date1,$date2)); } my @dates = ("Fri 16 Aug 96", "Mon 19 Aug 96", "Thu 15 Aug 96"); my @i = sort &sortDate @dates;
    (edited somewhat for clarity)

    You could also write this as:

    my @i = sort { &Date_Cmp(&ParseDate($a),&ParseDate($b)) } @dates;
Re: sorting mm/dd/yy
by Hrunting (Pilgrim) on Dec 15, 2000 at 08:02 UTC
    alakaboo mentions Date::Manip which is an excellent module but is very much a heavyweight module (in fact, the POD for Date::Manip even recommends not to use it if you're looking for simple date transformations of the form you're looking for). Try using Time::ParseDate which is much more lightweight and does exactly what you're looking for too much else. Combine it with a Schwartzian transform (see merlyn's post above) like so:
    use Time::Parsedate; ... foreach my $day ( map { $_->[0] } sort { $a->[1] <=> $b->[1] } map { [ $_, parsedate( $_ ) ] } @dates ) { ... }
    And that will be all she wrote. parsedate() from Time::Parsedate is pretty speedy and comparing the dates using a simple numeric compare is probably going to be faster than using Date_Cmp() (parsedate() returns numeric timestamps).
Re: sorting mm/dd/yy
by Hot Pastrami (Monk) on Dec 15, 2000 at 04:26 UTC
    If you're stuck with that lousy date format, something like this could work for your sorting sub:
    sub sortDates { my ($month1, $day1, $year1) = split /\//, $a; my ($month2, $day2, $year2) = split /\//, $b; $year1 += 1900 if $year1 < 50; $year2 += 1900 if $year2 < 50; $year1 <=> $year2 or $month1 <=> $month2 or $day1 <=> $day2; }
    ...however this assumes that none of the dates are older than 1950... you may need to adjust the '50' to fit your values.

    Alan "Hot Pastrami" Bellows
Re: sorting mm/dd/yy
by I0 (Priest) on Dec 15, 2000 at 11:11 UTC
    foreach $day ( map{join'/',(split/-/)[1,2,0]} sort map{join'-',(split'/')[2,0,1]} keys%totals ){ print "$day\n"; }
Re: sorting mm/dd/yy
by royalanjr (Chaplain) on Dec 15, 2000 at 20:23 UTC
    Which format do you folks would be best for storing dates then? The ISO YYYYMMDD, or unix epoch?

    Roy Alan

        That is what I have been doing at the moment. I use Time:ParseDate to turn the date into epoch and store that. Then when I need the formated into something a human can understand, I use localtime() to change it back.

        Someone changed the time zone on the server and it knocked all the dates out of whack. I had to write a script to make up for the time zone hour shift to make the dates right.

        Now, did I just do something bone-headed when I chose that scheme for the dates, or what?

        Roy Alan

Re: sorting mm/dd/yy
by Anonymous Monk on Dec 15, 2000 at 19:42 UTC
    Store them internally as YYMMDD and use a straight numeric sort.
      Just one more thing: Please use a four digit year (YYYY) to avoid any ambiguities.

      If you're dealing with data more than 12 months old, then it might be better to use YYYYMMDD.

      --
      <http://www.dave.org.uk>

      "Perl makes the fun jobs fun
      and the boring jobs bearable" - me

Re: sorting mm/dd/yy
by InfiniteSilence (Curate) on Dec 15, 2000 at 21:40 UTC
    I mostly wrote the following as a tutorial to myself as to how Perl's sorting function works:
    #!/usr/bin/perl -w use strict; #simple sort tests my @a; @a = (3,5,9,2,15,90,200); print "Standard sort: " , join(" ",sort(@a)); print "\nCorreted numeric sort: ", join(" ", sort {$a <=> $b} @a); #try dates my @b = ('11/5/99','2/5/87','11/6/99'); print "\nUnsorted Dates: " , join(" ", @b) , "\n"; print "\nCustom Date Sort: ", join(" ", sort {date_sort($a, $b)} @b); sub date_sort { my ($first, $second) = @_; if($first =~ m/(\d+)\/(\d+)\/(\d+)/g) { my (@mm, @dd, @yy); ($mm[1], $dd[1], $yy[1]) = ($1, $2, $3); if ($yy[1] > 50) {$yy[1] += 1900} else {$yy[1] += 2000}; if($second =~ m/(\d+)\/(\d+)\/(\d+)/g) { ($mm[2], $dd[2], $yy[2]) = ($1, $2, $3); } if ($yy[2] > 50) {$yy[2] += 1900} else {$yy[2] += 2000}; if ($yy[1] > $yy[2]) {return($first cmp $second)} elsif ($yy[1] < $yy[2]) {return($second cmp $first)} else { #fall into testing month dates if($mm[1] > $mm[2]) {return ($first cmp $second)} elsif($mm[2] > $mm[1]) {return ($second cmp $first)} else { #if that fails, test the days if($dd[1] > $dd[2]) {return ($first cmp $second)} else {return ($second cmp $first)} } } ; } } 1;
    Quick explanation

    The first and second sorts use Perl's standard sorting syntax. Nothing special here.

    Perl also allows you to call a special sorting routine sort {date_sort($a, $b)} @b where the stuff between the curly braces is your call to the special function. The function basically checks the date and returns either one of two options...either $a is greater than $b or the reverse.

    This is a very involved way to sort some bloody dates but, if you think about it, you could reuse the code if you needed a very, very specialized kind of date comparison function...e.g. perhaps something that seeks a particular range of dates and places them at the top of the sort order or something.

    Oops...I tried a different set for the test data for this function and I am getting incorrect results. Don't use this for dates < 1950. Celebrate Intellectual Diversity

      Uh, no. Don't do this. You have duplicated the code for parsing $a and $b. Once you've done that, you run into maintenance headaches or worse, you could be treating them asymetrically, and on older versions of Perl this could actually dump core.

      PLEASE DON'T copy this style at all. There have been other much better postings in this thread.

      -- Randal L. Schwartz, Perl hacker

      Your way of calling a special sorting routine really isn't the way that feature was designed to be used.

      Instead of declaring a sort block that calls your sort routine explicitly, with $a and $b as args, you should write a sub that processes $a and $b directly, and just give the name of the sub to sort. Here's an example:

      sub backwards { $b cmp $a } sort backwards @a;