Read hashes in Perl based on keys - perl

I have an hash in Perl as below. There are:
%typeMethodsMap = (
CHECK_REP_EXISTS => "1_abc",
CHECK_JDK_VERSION => "2_abc",
CHECK_BLOCKS_FAILED => "1_xyz",
CHECK_OR_EXISTS => "2_xyz",
CHECK_UPG_EXISTS => "3_xyz",
CHECK_SSO_EXISTS => "4_xyz"
);
When the hash is read, the keys are not read as defined but are read randomly. I needs to read and run through the loop on the hash based on the ascending format of the keys i.e. CHECK_BLOCKS_FAILED, followed by CHECK_OR_EXISTS followed by CHECK_UPG_EXISTS and CHECK_SSO_EXISTSfor value "1_xyz", "2_xyz", "3_xyz" and "4_xyz" respectively.
Please let me know if any body can help me here?

Yes. By design, hash keys are random order.
There's a bunch of reasons for that - covered in perlsec and keys - but the long and short of it is if you need to preserve key ordering, you need to use sort.
Or a slice:
my #order = qw ( first second third );
my %hash = ( second => 'a', third => 'b', first => 'c' );
print "#hash{#order}";
Or:
foreach my $key ( #order ) {
print "$key = $hash{$key}\n";
}
Arrays are explicitly ordered numerically. Hashes are explicitly unordered (or random order).
If you're custom sorting, then you can use any function you like that returns -1, 0 or 1 based on the value of the comparison.
cmp does this for strings, and <=> does this for numbers.
Notes for custom sorting, it might look like this:
use strict;
use warnings;
use Data::Dumper;
my %typeMethodsMap = (
CHECK_REP_EXISTS => "1_abc",
CHECK_JDK_VERSION => "2_abc",
CHECK_BLOCKS_FAILED => "1_xyz",
CHECK_OR_EXISTS => "2_xyz",
CHECK_UPG_EXISTS => "3_xyz",
CHECK_SSO_EXISTS => "4_xyz",
);
my #order = qw(
CHECK_REP_EXISTS
CHECK_JDK_VERSION
CHECK_BLOCKS_FAILED
CHECK_OR_EXISTS
CHECK_UPG_EXISTS
CHECK_SSO_EXISTS
);
my $count = 0;
my %magic_order = map { $_ => $count++ } #order;
print Dumper \%magic_order;
sub custom_sort {
return $magic_order{$a} <=> $magic_order{$b};
}
foreach my $key ( sort { custom_sort } keys %typeMethodsMap ) {
print $key,"\n";
}
Although note - this isn't much more efficient, it's merely intended to illustrate 'custom sorting'. Alternatively - if you're wanting to sort based on your 'keys' being sorted:
sub custom_sort {
my ( $a_number, $a_text ) = split ('_',$a);
my ( $b_number, $b_text ) = split ( '_', $b );
if ( $a_number == $b_number ) {
return $a_text cmp $b_text;
}
else {
return $a_number <=> $b_number
}
}
This will sort numerically first, and then alphabetically second. (Swap the <=> and cmp if you want the opposite).

If you know what the keys are then you can write just
for my $key (qw/ 1_CHECK_BLOCKS_FAILED 2_CHECK_OR_EXISTS 3_CHECK_UPG_EXISTS /) {
...
}
Otherwise you must either keep track of the order of the keys in a separate array when you are building the hash, or use something like the Tie::Hash::Indexed module (there are several similar ones) which maintains the order of hash data

Related

How can I sort an array of hashes by the hashes name?

I want to sort an array of hashes by the hashes key, how could I do that in Perl?
The structure is created like this :
push (#{$structure[$endpoint][1]}, \%temp_hash);
%temp_hash is a simple hash with key->value.
And now I want to sort that array by the hashes key, there is only one key->value in each hash... been fighting with it for 2 hours already and I gave up..
Try
#sorted = sort { (keys %$a)[0] cmp (keys %$b)[0] } #{$structure[$endpoint][1]};
This sorts the elements of the array (which are hash references) according to the first (only) key of each hash. If the keys are numeric use <=> instead.
Test code:
%a = ( 'a' => 1 );
%b = ( 'zz' => 2 );
%c = ( 'g' => 3);
#arr = (\%a, \%b, \%c);
print "Unsorted\n";
for (#arr)
{
printf "%s\n",((keys %$_)[0]);
}
#sorted = sort { (keys %$a)[0] cmp (keys %$b)[0] } #arr;
print "\nSorted\n";
for (#sorted)
{
printf "%s\n",((keys %$_)[0]);
}

Maintain order within a hash of hashes, and output as .csv

Here's my code:
my %hash = (
'2564' => {
'st_responsible' => 'mname1',
'critical' => '',
'last_modified_by' => 'teamname1',
'transstatus' => '',
'rt_res' => 'pname1'
},
'2487' => {
'st_responsible' => 'mname2',
'critical' => '',
'last_modified_by' => 'teamname2',
'transstatus' => '',
'rt_res' => ''
}
);
print "xnum,st_responsible,critical,last_modified_by,transstatus,rt_res\n";
foreach my $x_number (sort keys %hash)
{
print "$x_number";
foreach my $element (keys %{$hash{$x_number}})
{
print ",$hash{$x_number}{$element}";
}
print "\n";
}
Expected output
xnum,st_responsible,critical,last_modified_by,transstatus,rt_res
2487,mname2,,teamname2,,
2564,mname1,,teamname1,,pname1
Actual output
xnum,st_responsible,critical,last_modified_by,transstatus,rt_res
2487,mname2,,,teamname2,
2564,mname1,,,teamname1,pname1
Please help in letting me know as to how exactly do I preserve the order of this data structure, and then write this to a CSV file.
I would suggest that for this, you'd be better off doing this with a slice, which is a way of extracting a list of values from a hash in a particular order?
#configure field order
my #order = qw ( st_responsible critical last_modified_by transstatus rt_res );
#print header row
print join (",", "xnum", #order ),"\n";
#iterate the rows
foreach my $key ( sort keys %hash ) {
#extract hash slice and join it with commas
print join ( ",", $key, #{$hash{$key}}{#order} ),"\n";
}
This gives:
xnum,st_responsible,critical,last_modified_by,transstatus,rt_res
2487,mname2,,teamname2,,
2564,mname1,,teamname1,,pname1
You can consider Text::CSV - but I'd suggest in this scenario it's overkill, best used when you've got quotes and quoted field separators to worry about. (And you don't).
If you have to deal with not just empty keys, but missing ones, you can make use of map:
my #order = qw ( st_responsible critical last_modified_by
transstatus missing rt_res extra_field_here );
print join (",", "xnum", #order ),"\n";
foreach my $key ( sort keys %hash ) {
print join ( ",", $key, map { $_ // '' } #{$hash{$key}}{#order} ),"\n";
}
(Otherwise you'll get a warning about an undef value).
Perl doesn't guarantee the order of items in the hash, this is the root cause of the issue. Even two different hashes with the same keys can have different order of keys. It may also differ from platform to platform and architecture and perl version.
You need to define another array with the list of keys which you want to print in correct order.
my #keys = qw(st_responsible critical last_modified_by transstatus rt_res);
foreach my $element (#keys) {
... print the value
}
EDIT: As you're trying to write CSV file, consider using Text::CSV which takes care about special characters, correct formatting and things like that.
There's probably a slicker way of achieving this, but give this a go:
use warnings;
use strict;
open my $csv_out, '>', 'out.csv' or die $!;
my #keys = qw(2487 2564);
my #vals = qw(st_responsible critical last_modified_by transstatus rt_res);
print $csv_out "xnum,st_responsible,critical,last_modified_by,transstatus,rt_res\n";
for my $key (#keys){
print $csv_out "$key,";
for my $vals (#vals){
$vals eq $vals[-1] ? print $csv_out "$hash{$key}{$vals}\n" : print $csv_out "$hash{$key}{$vals},";
}
}
This will print out comma-separated values to a csv file out.csv maintaining your original order (by iterating over arrays). If it's the last value it will print a newline.
--- OUTPUT ---
xnum,st_responsible,critical,last_modified_by,transstatus,rt_res
2487,mname2,,teamname2,,
2564,mname1,,teamname1,,pname1

In Perl, how do I print hash keys in order of their (numerical) values?

I have a hash in which the keys are strings and the values are single-digit numbers; here's a slice of said hash:
'f92a0d43-a230-4bfd-b580-9eac5e0ce6cf' => 7,
'26c4b622-969f-4861-bbab-dd506ea4b00a' => 1,
'afb1f925-4109-4b1d-967f-3958106e0bc3' => 3,
'a099a6dc-0c66-4683-94c3-29d6ef6947fd' => 1,
'e71c4860-224d-4b8d-ae9e-4700e9e65a97' => 2,
I want print the keys in order of descending values. So for the slice listed there, the output would be:
'f92a0d43-a230-4bfd-b580-9eac5e0ce6cf' => 7
'afb1f925-4109-4b1d-967f-3958106e0bc3' => 3
'e71c4860-224d-4b8d-ae9e-4700e9e65a97' => 2
'26c4b622-969f-4861-bbab-dd506ea4b00a' => 1
'a099a6dc-0c66-4683-94c3-29d6ef6947fd' => 1
The order of keys with identical values does not matter. Answers to this question:
In Perl, how can I print the key corresponding to the maximum value in a hash?
suggest using the sort function; which I have:
my #values = sort { $b <=> $a } values %ID_hash;
What I am having trouble with is actually printing the keys in the order.
I tried:
foreach(#values) {
my $cur = $_;
print "$ID_hash{$cur}\t$cur\n";
}
Which fails because I'm supplying values rather than keys.
I know I can always just print the key/value pairs as a tab-separated file and use the Unix version of sort but I'm sure there's a way to do this with Perl. Any help will be much appreciated.
Sort the keys by the values in the hash, then use the sorted keys to print.
for my $key ( sort { $ID_hash{$b} <=> $ID_hash{$a} } keys %ID_hash ) {
print join( "\t", $key, $ID_hash{$key} ), "\n";
}
This equivalent may be a little clearer:
my #sorted_keys = sort { $ID_hash{$b} <=> $ID_hash{$a} } keys %ID_hash ;
print "$_\t$ID_hash{$_}\n" for #sorted_keys;

Perl extract range of elements from a hash

If I have a hash:
%hash = ("Dog",1,"Cat",2,"Mouse",3,"Fly",4);
How can I extract the first X elements of this hash. For example if I want the first 3 elements, %newhash would contain ("Dog",1,"Cat",2,"Mouse",3).
I'm working with large hashes (~ 8000 keys).
"first X elements of this hash" doesn't mean anything. First three elements in order by numeric value?
my %hash = ( 'Dog' => 1, 'Cat' => 2, 'Mouse' => 3, 'Fly' => 4 );
my #hashkeys = sort { $hash{$a} <=> $hash{$b} } keys %hash;
splice(#hashkeys, 3);
my %newhash;
#newhash{#hashkeys} = #hash{#hashkeys};
You might want to use something like this:
my %hash = ("Dog",1,"Cat",2,"Mouse",3,"Fly",4);
for ( (sort keys %hash)[0..2] ) {
say $hash{$_};
}
You should have an array 1st:
my %hash = ("Dog" => 1,"Cat"=>2,"Mouse"=>3,"Fly"=>4);
my #array;
foreach $value (sort {$hash{$a} <=> $hash{$b} }
keys %hash)
{
push(#array,{$value=>$hash{$value}});
}
#get range:
my #part=#array[0..2];
print part of result;
print $part[0]{'Cat'}."\n";

Inverting a Hash's Key and Values in Perl

I would like to make the value the key, and the key the value. What is the best way to go about doing this?
Adapted from http://www.dreamincode.net/forums/topic/46400-swap-hash-values/:
Assuming your hash is stored in $hash:
while (($key, $value) = each %hash) {
$hash2{$value}=$key;
}
%hash=%hash2;
Seems like much more elegant solution can be achieved with reverse (http://www.misc-perl-info.com/perl-hashes.html#reverseph):
%nhash = reverse %hash;
Note that with reverse, duplicate values will be overwritten.
Use reverse:
use Data::Dumper;
my %hash = ('month', 'may', 'year', '2011');
print Dumper \%hash;
%hash = reverse %hash;
print Dumper \%hash;
As mentioned, the simplest is
my %inverse = reverse %original;
It "fails" if multiple elements have the same value. You could create an HoA to handle that situation.
my %inverse;
push #{ $inverse{ $original{$_} } }, $_ for keys %original;
So you want reverse keys & vals in a hash? So use reverse... ;)
%hash2 = reverse %hash;
reverting (k1 => v1, k2 => v2) - yield (v2=>k2, v1=>k1) - and that is what you want. ;)
my %orig_hash = (...);
my %new_hash;
%new_hash = map { $orig_hash{$_} => $_ } keys(%orig_hash);
The map-over-keys solution is more flexible. What if your value is not a simple value?
my %forward;
my %reverse;
#forward is built such that each key maps to a value that is a hash ref:
#{ a => 'something', b=> 'something else'}
%reverse = map { join(',', #{$_}{qw(a b)}) => $_ } keys %forward;
Here is a way to do it using Hash::MultiValue.
use experimental qw(postderef);
sub invert {
use Hash::MultiValue;
my $mvh = Hash::MultiValue->from_mixed(shift);
my $inverted;
$mvh->each( sub { push $inverted->{ $_[1] }->#* , $_[0] } ) ;
return $inverted;
}
To test this we can try the following:
my %test_hash = (
q => [qw/1 2 3 4/],
w => [qw/4 6 5 7/],
e => ["8"],
r => ["9"],
t => ["10"],
y => ["11"],
);
my $wow = invert(\%test_hash);
my $wow2 = invert($wow);
use DDP;
print "\n \%test_hash:\n\n" ;
p %test_hash;
print "\n \%test_hash inverted as:\n\n" ;
p $wow ;
# We need to sort the contents of the multi-value array reference
# for the is_deeply() comparison:
map {
$test_hash{$_} = [ sort { $a cmp $b || $a <=> $b } #{ $test_hash{$_} } ]
} keys %test_hash ;
map {
$wow2->{$_} = [ sort { $a cmp $b || $a <=> $b } #{ $wow2->{$_} } ]
} keys %$wow2 ;
use Test::More ;
is_deeply(\%test_hash, $wow2, "double inverted hash == original");
done_testing;
Addendum
Note that in order to pass the gimmicky test here, the invert() function relies on %test_hash having array references as values. To work around this if your hash values are not array references, you can "coerce" the regular/mixed hash into a multi-value hash thatHash::MultiValue can then bless into an object. However, this approach means even single values will appear as array references:
for ( keys %test_hash ) {
if ( ref $test_hash{$_} ne 'ARRAY' ) {
$test_hash{$_} = [ $test_hash{$_} ]
}
}
which is longhand for:
ref($_) or $_ = [ $_ ] for values %test_hash ;
This would only be needed to get the "round trip" test to pass.
Assuming all your values are simple and unique strings, here is one more easy way to do it.
%hash = ( ... );
#newhash{values %hash} = (keys %hash);
This is called a hash slice. Since you're using %newhash to produce a list of keys, you change the % to a #.
Unlike the reverse() method, this will insert the new keys and values in the same order as they were in the original hash. keys and values always return their values in the same order (as does each).
If you need more control over it, like sorting it so that duplicate values get the desired key, use two hash slices.
%hash = ( ... );
#newhash{ #hash{sort keys %hash} } = (sort keys %hash);