Zaid
Zaid

Reputation: 37146

How can I sort a dd/mm/yyyy formatted date by month using Perl?

I have a list of dd/mm/yyyy dates stored in a hash that I need to sequentially print out in order of date by month. Here is the relevant excerpt.

use feature 'say';

for my $date (sort {$a cmp $b} keys %data) {
    say $date;
}

This outputs:

16/07/2008
16/08/2008
16/09/2008
17/07/2008
17/08/2008
17/09/2008, etc.

when what I need is:

16/07/2008
17/07/2008
16/08/2008
17/08/2008
16/09/2008
16/09/2008, etc.

How can I achieve this?

Upvotes: 2

Views: 2157

Answers (4)

brian d foy
brian d foy

Reputation: 132858

From perlfaq4's answer to How do I sort an array by (anything)?


Supply a comparison function to sort() (described in sort in perlfunc):

@list = sort { $a <=> $b } @list;

The default sort function is cmp, string comparison, which would sort (1, 2, 10) into (1, 10, 2). <=>, used above, is the numerical comparison operator.

If you have a complicated function needed to pull out the part you want to sort on, then don't do it inside the sort function. Pull it out first, because the sort BLOCK can be called many times for the same element. Here's an example of how to pull out the first word after the first number on each item, and then sort those words case-insensitively.

@idx = ();
for (@data) {
    ($item) = /\d+\s*(\S+)/;
    push @idx, uc($item);
    }
@sorted = @data[ sort { $idx[$a] cmp $idx[$b] } 0 .. $#idx ];

which could also be written this way, using a trick that's come to be known as the Schwartzian Transform:

@sorted = map  { $_->[0] }
    sort { $a->[1] cmp $b->[1] }
    map  { [ $_, uc( (/\d+\s*(\S+)/)[0]) ] } @data;

If you need to sort on several fields, the following paradigm is useful.

@sorted = sort {
    field1($a) <=> field1($b) ||
    field2($a) cmp field2($b) ||
    field3($a) cmp field3($b)
    } @data;

This can be conveniently combined with precalculation of keys as given above.

See the sort article in the "Far More Than You Ever Wanted To Know" collection in http://www.cpan.org/misc/olddoc/FMTEYEWTK.tgz for more about this approach.

See also the question later in perlfaq4 on sorting hashes.

Upvotes: 2

larelogio
larelogio

Reputation: 409

If the format is a rigid 10 chars:

sort { substr($a,3,2) cmp substr($b,3,2) } @dates;

Upvotes: 7

jrockway
jrockway

Reputation: 42684

Parse and sort:

sub get_month {
    my $date = shift;
    my ($d, $m, $y) = split m{/}, $date;
    return $m;
}

sort { get_month($a) <=> get_month($b) } @dates;

I would probably use DateTime, though, since I want to work with objects, not meaningless strings:

my $parser = DateTime::Format::Natural->new(format => 'dd/mm/yyyy');
sort map { $parser->parse_datetime($_) } @dates;

Upvotes: 6

friedo
friedo

Reputation: 67028

You can do it using a Schwartzian Transform, like so:

for my $date( map  { $_->[0] }
              sort { $a->[1] <=> $b->[1] } 
              map  { [ $_, (split /\//, $_)[1] ] }
              keys %data ) { 
    ...

This takes each key of %data and puts it in an anonymous array where the first element is the key and the second is the middle field from splitting on /. Then it sorts by the second element of the array, and uses another map to get the original list of keys.

Upvotes: 12

Related Questions