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

Hello, fellow monks. It has been a while since I have actually asked a question since I have found answers to just about everything I have needed in the Super Search.

However, this past week I ran into a situation at work where I ended up parsing some logfiles (in a company-only format) to find the "Top 25" hits for either alerts, errors, or warnings for Web applications. Parsing the data was not a real issue, thanks to Perl, but I ended up with a datastructure that looked like this (in pseudo-code):

$hash{APP}{SEVERITY}{ERRORCODE} = $count_of_errors;

Now, I do pretty well with hashes, and have found a good bit of information in the Super Search looking for things like "sort values multidimensional hash"; in fact that gives me more than a couple of pages worth of hits.

I was looking through another listing of hits tonight, hoping to find something pertinent, and stumbled on merlyn's "Inverting a hash to get all keys for this value". This started to help me see one way to do what I was trying to do: invert my hash in order to do the compare.

Below, I have posted some basic test code illustrating a method that does seem to work. My questions to you, fellow monks, are:

  1. Is this inversion of the hash the best way to sort the data by value while retaining the keys to the hash (and therefore the original data)?
  2. Is there any way to clean up this code to either make it look better or run faster without too much obfuscation?
my %hash = ( APP1 => { error => { 6001 => 10, 6002 => 15, 6003 => 25, }, alert => { 1001 => 2, 1005 => 200, }, warning => { 0022 => 10, }, }, APP2 => { error => { 6001 => 4, 6004 => 21, }, alert => { 1002 => 3, 1005 => 165, }, }, ); my %revhash; for my $key1 (keys %hash) { for my $key2 (keys %{$hash{$key1}}) { push @{$revhash{$hash{$key1}{$key2}{$_}}}, $key1.":".$key2.":".$ +_ for keys %{$hash{$key1}{$key2}}; } } my $cnt; for my $key (sort { $b <=> $a } keys %revhash) { for (@{$revhash{$key}}) { printf "%3d %3d %s\n", $cnt, $key, $_ if ++$cnt <= 5; } }

Output:

1 200 APP1:alert:1005 2 165 APP2:alert:1005 3 25 APP1:error:6003 4 21 APP2:error:6004 5 15 APP1:error:6002

This is just sample data and output, and only shows the Top 5, but if I can make this work (better), I can rearrange the output as necessary. And yes, I do use strict and -w.

Thanks for the help!

D a d d i o

Replies are listed 'Best First'.
Re: Sorting values of multi-dimensional hashes while retaining key values
by bikeNomad (Priest) on Jun 25, 2001 at 07:33 UTC
    Here's a more generic way to flatten a hash with an arbitrary number of levels. It can be easily extended to handle embedded array refs, as well:

    #!/usr/bin/perl -w use strict; my %hash = ( APP1 => { error => { 6001 => 10, 6002 => 15, 6003 => 25, }, alert => { 1001 => 2, 1005 => 200, }, warning => { '0022' => 10, }, }, APP2 => { error => { 6001 => 4, 6004 => 21, }, alert => { 1002 => 3, 1005 => 165, }, }, ); my $separator = '.'; sub flattenHash { my $hashRef = shift; my $prefixArrayRef = shift || []; my $outputArrayRef = shift || []; while ( my ( $key, $value ) = each(%$hashRef) ) { push @$prefixArrayRef, $key; if ( UNIVERSAL::isa( $value, 'HASH' ) ) { flattenHash( $value, $prefixArrayRef, $outputArrayRef ); } else { push ( @$outputArrayRef, join ( $separator, @$prefixArrayR +ef ), $value ); } pop @$prefixArrayRef; } return wantarray ? @$outputArrayRef : $outputArrayRef; } my %flatHash = flattenHash( \%hash ); my @sorted = sort { $b->[0] <=> $a->[0] } map { [ $flatHash{$_}, $_ ] } keys(%fla +tHash); foreach my $arrayRef (@sorted) { print "$arrayRef->[0] $arrayRef->[1]\n"; }

    update: made sure of no value collisions in reverse, quoted octal key.