petranaya
petranaya

Reputation: 769

Sorting a hash by value when it has many keys

I believe this is how you would normally sort a hash by value:

foreach my $key (sort { $hash{$a} <=> $hash{$b} } (keys %hash) ) {
    print "$key=>$hash{$key}";
}

This would print out the values smallest to largest.

Now, what if I have a hash like this:

$hash{$somekey}{$somekey2}{$thirdkey}

How could I sort by values and get all the keys as well?

Upvotes: 5

Views: 331

Answers (4)

FMc
FMc

Reputation: 42411

Here's a way to do it using Deep::Hash::Utils.

use Deep::Hash::Utils qw(slurp);

my %h = (
    A => {
        Aa => { Aaa => 4, Aab => 5 },
        Ab => { Aba => 1 },
        Ac => { Aca => 2, Acb => 9, Acc => 0 },
    },
    B => {
        Ba => { Baa => 44, Bab => -55 },
        Bc => { Bca => 22, Bcb => 99, Bcc => 100 },
    },
);

my @all_keys_and_vals = slurp \%h;
print "@$_\n" for sort { $a->[-1] <=> $b->[-1] } @all_keys_and_vals;

Output:

B Ba Bab -55
A Ac Acc 0
A Ab Aba 1
A Ac Aca 2
A Aa Aaa 4
A Aa Aab 5
A Ac Acb 9
B Bc Bca 22
B Ba Baa 44
B Bc Bcb 99
B Bc Bcc 100

Upvotes: 1

ErikR
ErikR

Reputation: 52029

I would just create a new hash:

my %new;
for my $k1 (keys %hash) {
  for my $k2 (keys %{$hash{$k1}}) {
    for my $k3 (keys %{$hash{$k1}{$k2}}) {
      $new{$k1,$k2,$k3} = $hash{$k1}{$k2}{$k3};
    }
  }
}

my @ordered = sort { $new{$a} <=> $new{$b} } keys %new;
for my $k (@ordered) {
  my @keys = split($;, $k);
  print "key: @k      - value: $new{$k}\n";
}

Upvotes: 3

hobbs
hobbs

Reputation: 239821

For academic purposes, here's a fairly tidy recursive function:

sub flatten_hash {
  my ($hash, $path) = @_;
  $path = [] unless defined $path;

  my @ret;

  while (my ($key, $value) = each %$hash) {
    if (ref $value eq 'HASH') {
      push @ret, flatten_hash($value, [ @$path, $key ]);
    } else {
      push @ret, [ [ @$path, $key ], $value ];
    }
  }

  return @ret;
}

which takes a hash like

{
    roman => {
        i => 1,
        ii => 2,
        iii => 3,
    },
    english => {
        one => 1,
        two => 2,
        three => 3,
    },
}

and turns it into a list like

(
    [ ['roman','i'], 1 ],
    [ ['roman', 'ii'], 2 ],
    [ ['roman', 'iii'], 3 ],
    [ ['english', 'one'], 1 ],
    [ ['english', 'two'], 2 ],
    [ ['english', 'three'], 3 ]
)

although of course the order is bound to vary. Given that list, you can sort it on { $a->[1] <=> $b->[1] } or similar, and then extract the key path from @{ $entry->[0] } for each entry. It works regardless of the depth of the data structure, and even if the leaf nodes don't occur all at the same depth. It needs a little bit of extension to deal with structures that aren't purely of hashrefs and plain scalars, though.

Upvotes: 1

Brad Krusemark
Brad Krusemark

Reputation: 533

I have done something similar by moving a reference down to the appropriate hash key. You can then perform the sort on the pointer.

The advantage to doing it this way is that it is easy to adjust if the level changes.

What I have used this methodology for is systematically moving the pointer to a specific level by referencing an array of keys. (Ex: my @Keys = ('Value', 'Value2');)

I believe a derivative of the following example might give you what you are looking for.

my $list_ref;
my $pointer;

my %list = (
   Value => {
      Value2 => {
         A => '1',
         C => '3',
         B => '2',
      },
   },
);

$list_ref = \%list;
$pointer = $list_ref->{Value}->{Value2};

foreach my $key (sort { $pointer->{$a} <=> $pointer->{$b} } (keys %{$pointer})) {
   print "Key: $key\n";
}

Upvotes: 1

Related Questions