views:

61

answers:

3
my %data (
    KEY1 => {
        SUBKEY1 => "Canada",
        SUBKEY3 => "75.00",
        SUBKEY2 => "50.00",    
    },
    KEY3 => {
        SUBKEY2 => "150.00",
    },  
    KEY2 => { 
        SUBKEY3 => "200.00",
        SUBKEY1 => "Mexico",
 },
);

How do I print a list that is sorted by Keyname and for each keyname sorted by subkeyname?

Here is what I want to print: (notice that if the subkey wasn't defined, a placeholder for the subkey with a null string is present)

KEY1: SUBKEY1 is "Canada"
KEY1: SUBKEY2 is "50.00"
KEY1: SUBKEY3 is "75.00"
KEY2: SUBKEY1 is ''
KEY2: SUBKEY2 is "150.00"
KEY2: SUBKEY3 is ''
KEY3: SUBKEY1 is "Mexico"
KEY3: SUBKEY2 is ''
KEY3: SUBKEY3 is "200.00"
+3  A: 
use strict;
use warnings;

my %data = (
    KEY1 => {
        SUBKEY1 => "Canada",
        SUBKEY3 => "75.00",
        SUBKEY2 => "50.00",    
    },
    KEY3 => {
        SUBKEY2 => "150.00",
    },  
    KEY2 => { 
        SUBKEY3 => "200.00",
        SUBKEY1 => "Mexico",
 },
);

my %all_sub_keys;
for my $sub_hash (values %data){
    $all_sub_keys{$_} ++ for keys %$sub_hash;
}

my @all_sub_keys = sort keys %all_sub_keys;

for my $k ( sort keys %data ){
    for my $sk (@all_sub_keys){
        my $val = exists $data{$k}{$sk} ? $data{$k}{$sk} : '--';
        print join(' ', $k, $sk, $val), "\n";
    }
}
FM
+2  A: 

I am assuming the set of subkeys is known ahead of time.

#!/usr/bin/perl

use strict; use warnings;

my %data = (
    KEY1 => {
        SUBKEY1 => "Canada",
        SUBKEY3 => "75.00",
        SUBKEY2 => "50.00",
    },
    KEY3 => {
        SUBKEY2 => "150.00",
    },
    KEY2 => {
        SUBKEY3 => "200.00",
        SUBKEY1 => "Mexico",
 },
);

my @subkeys = qw( SUBKEY1 SUBKEY2 SUBKEY3 );

for my $key ( sort keys %data ) {
    my %sub = map {
        my $v = $data{$key}{$_};
        $_ => defined($v) ? $v : '';
    } @subkeys;

    for my $subkey ( @subkeys ) {
        print "$key $subkey $sub{$subkey}\n";
    }
}

Output:

KEY1 SUBKEY1 Canada
KEY1 SUBKEY2 50.00
KEY1 SUBKEY3 75.00
KEY2 SUBKEY1 Mexico
KEY2 SUBKEY2
KEY2 SUBKEY3 200.00
KEY3 SUBKEY1
KEY3 SUBKEY2 150.00
KEY3 SUBKEY3
Sinan Ünür
+2  A: 

How about a Schwartzian transform?

#! /usr/bin/perl

use 5.10.0;  # for // (aka defined-or)
use warnings;
use strict;

my %data = ...;

# get all subkeys used in %data
my @subkeys = keys %{
  { map { map +($_ => 1),
          keys %{ $data{$_} } }
    keys %data
  }
};

print map qq|$_->[0]: $_->[1] is "$_->[2]"\n|,
      sort { $a->[0] cmp $b->[0]
                     ||
             $a->[1] cmp $b->[1] }
      map { my $key = $_;
            map [ $key, $_, $data{$key}{$_} // "" ] =>
            @subkeys }
      keys %data;

Remember to read Schwartzian transforms from back-to-front. The first—closest to the end—map flattens or "denormalizes" %data into a list of records in some unspecified order. The nested map is necessary to reach the subkeys. To handle arbitrarily deep nesting, define flatten recursively.

We made an earlier pass to collect all the subkeys used, so if a particular subkey is not present, the value of $data{$key}{$_} is the undefined value. Using //, the defined-or operator new in version 5.10.0, specifies a default value of "".

With flattened records of the form

[ "KEY1", "SUBKEY3", "75.00" ],
[ "KEY1", "SUBKEY1", "Canada" ],
...

sorting is straightforward: compare the respective first elements (the keys) and if those are equal, fall back to the seconds (the subkeys).

Finally, the outermost map formats the now-sorted denormalized records for output, and the resulting list goes to the standard output by way of the print operator.

Output:

KEY1: SUBKEY1 is "Canada"
KEY1: SUBKEY2 is "50.00"
KEY1: SUBKEY3 is "75.00"
KEY2: SUBKEY1 is "Mexico"
KEY2: SUBKEY2 is ""
KEY2: SUBKEY3 is "200.00"
KEY3: SUBKEY1 is ""
KEY3: SUBKEY2 is "150.00"
KEY3: SUBKEY3 is ""

To group subkeys on the same line with the respective key of each, go with code such as

my @subkeys = sort keys %{ ... ;

foreach my $key (sort keys %data) {
  my @values;
  foreach my $subkey (@subkeys) {
    my $value = $data{$key}{$subkey} // "";
    push @values => qq|$subkey is "$value"|;
  }

  local $" = ", ";
  print "$key: @values\n";
}

You could write it in a functional style, but the result is a muddy mess:

print map { my $key = $_;
            "$key: " .
              join(", " =>
                map { my $value = $data{$key}{$_} // "";
                      qq|$_ is "$value"|
                    }
                @subkeys) .
            "\n"
          }
      sort keys %data;

Output:

KEY1: SUBKEY1 is "Canada", SUBKEY2 is "50.00", SUBKEY3 is "75.00"
KEY2: SUBKEY1 is "Mexico", SUBKEY2 is "", SUBKEY3 is "200.00"
KEY3: SUBKEY1 is "", SUBKEY2 is "150.00", SUBKEY3 is ""
Greg Bacon
Thanks, I had been researching that method but I just couldn't grasp the syntax until you posted this example...very nice!
what does this line do? map [ $key, $_, $data{$key}{$_} // "" ] =>
@cgmojoco Thanks, I'm glad it helps! See updated answer.
Greg Bacon
Thank you gbacon for the solution AND the wonderful explanationI'm still a little perplexed, perhaps if you had the time, you could show how you would re-write to produce the following output: KEY1: SUBKEY1 is "Canada", SUBKEY2 is "50.00", SUBKEY3 is "75.00" NEWLINE KEY2: SUBKEY1 is "Mexico", SUBKEY2 is "", SUBKEY3 is "200.00" NEWLINE KEY3: SUBKEY1 is "", SUBKEY2 is "150.00", SUBKEY3 is "" NEWLINE
@cgmojoco See updated answer.
Greg Bacon
This is VERY helpful, can't thank you enough!