Reputation: 37146
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
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
Reputation: 409
If the format is a rigid 10 chars:
sort { substr($a,3,2) cmp substr($b,3,2) } @dates;
Upvotes: 7
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
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