user4560113
user4560113

Reputation:

How do I sort alphanumerical keys in Perl?

I have a hash with keys that look like AA00, AA01, AB00, AB23, ZA03, ZB45, AA02, DA05, AA45, DE67, DE84, ZZ99 and I need to sort them first by letter set then by number from minor to major.

Edit: the case is more complex indeed. Letters must be read from left to right and the number of digits and letter might change. That is, A00 must be before AA00 and AB00. B00 must be after AZ99 but before BA00.

Also, If I find AA, AAA, AAB and AAAA, then AAA, AAB shall be considered a subset that goes before AA, and AAAA shall be before the triple letter subset. But ABAA, for example, must be after AA.

Numbers are in linear order, that is 0 is before 1, and 1 is before 99, but there is no limit for the number of digits. 1 might be represented by 1 or by 01 (See ZB). Ignore the spaces, they are there to maintain the columns.

That is,

A   00,
AAAA00,
AAA 00,
AAB 00,
AA  00,
AA  01,
AA  02,
AA  45,
ABAA00,
AB  00, (first letter change)
AB  23,
AZ  99,
B   00,
BA  00,
DA  05, (second letter change, first letter restarts as A)
DE  67,
DE  84,
ZA  03,
ZB  45,
ZB 145,
ZB1145,
ZZ  99,

I tried the classical

for $key ( sort {$a<=>$b} keys %hash) {
       print "($key)->($hash{$key})\n";
}

But no sorting is produced at all. Indeed, keys are passed totally disorganzied.

There is the logic that was used to produced the data set. They used:

 While (Certain thing is True) {
   Select a $letter; 
   $identifier .= $letter 
 } 

It is supposed that the first letter is a represents a Set A, the second letter represents a subset in A. That is, if I hava AA, AB and AC, then A, B and C are subsets in A. If I had ABC, C is a subset in the subset B of the set A. If I had, then, ABCA, the last A is a subset in C.

Upvotes: 1

Views: 195

Answers (2)

ikegami
ikegami

Reputation: 385915

my @sorted =
   map $_->[0],
   sort {
      $a->[1] cmp $b->[1]
         ||
      $a->[2] <=> $b->[2]
   }
   map {
      my ($l, $n) = /^([A-Z]+)([0-9]{1,9})\z/
         or die("Unexpected data");

      $l .= "\xFF" if length($l) > 1;

      [ $_, $l, $n ]
   }
   @unsorted;

Optimized:

my @sorted =
   map { unpack('J/a*', scalar(reverse($_))) }
   sort
   map {
      my ($l, $n) = /^([A-Z]+)([0-9]{1,9})\z/
         or die("Unexpected data");

      pack('a* a* J>', $l, length($l) == 1 ? "\x00" : "\xFF", $n) . reverse(pack('J/a*', $_))
   }
   @unsorted;

Note: It may be possible to handle larger numbers without changing anything but the check, depending on your build of Perl.

Upvotes: 5

David Levner
David Levner

Reputation: 341

As some of the posters wrote, the question is not completely clear. Nevertheless, my attempt at a solution is below. I have not tested this code, so you may need to tweak it a bit to get it to work.

my @sorted_keys = sort by_letters_then_numbers keys %hash;

sub by_letters_then_numbers
{
    if ($a !~ /^([a-zA-Z]+)\s*(\d+)$/) {return 0};
    my $a_letters = $1;
    my $a_number = $2;
    if ($b !~ /^([a-zA-Z]+)\s*(\d+)$/) {return 0};
    my $b_letters = $1;
    my $b_number = $2;
    return(($a_letters cmp $b_letters) || ($a_number <=> $b_number));
}

Upvotes: -2

Related Questions