Programmatically change sorting order in Perl - arrays

I'd like to give the user the possibility to change the sorting order (asc / desc) in a data structure. As far as I know, this is done changing the order of $a and $bin the code, but I'd like to programmatically change this to avoid redundant code.
I made a working example:
use 5.018;
use warnings;
# Supply any argument to change sorting order
my $sorting_direction = $ARGV[0];
my $data = {
'item1' => {
'min' => 4,
'size' => 825,
'max' => 256,
},
'item2' => {
'min' => 4,
'size' => 130,
'max' => 65,
},
};
if (defined $sorting_direction) {
foreach my $item (sort { $$data{$a}{'size'} <=> $$data{$b}{'size'} } keys %{$data} ) {
say "$item\t", $$data{$item}{'size'};
}
} else {
foreach my $item (sort { $$data{$b}{'size'} <=> $$data{$a}{'size'} } keys %{$data} ) {
say "$item\t", $$data{$item}{'size'};
}
}
Giving any parameter will change the sorting_direction. Can I do this without the if conditional?

As <=> has a value of -1, 0 or 1, you can multiply with -1 to get the opposite sorting order.
So if your $sorting_direction is 1 or -1 use
$sorting_direction * ( $$data{$a}{'size'} <=> $$data{$b}{'size'} )

A generic solution is to use different compare functions.
my %sorters = (
by_size_asc => sub { $data->{$a}{size} <=> $data->{$b}{size} },
by_size_desc => sub { $data->{$b}{size} <=> $data->{$a}{size} },
# ...
);
#ARGV
or die("usage\n");
my $sorter = $sorters{$ARGV[0]}
or die("Invalid sort function \"$ARGV[0]\".\n");
my #sorted_keys = sort $sorter keys(%$data);
You could also use different sort functions, such as when using the great Sort::Key module.
use Sort::Key qw( ikeysort rikeysort );
my %sorters = (
by_size_asc => sub { ikeysort { $data->{$_}{size} } #_ },
by_size_desc => sub { rikeysort { $data->{$_}{size} } #_ },
# ...
);
#ARGV
or die("usage\n");
my $sorter = $sorters{$ARGV[0]}
or die("Invalid sort function \"$ARGV[0]\".\n");
my #sorted_keys = $sorter->( keys(%$data) );

While it's always going to be slower because it's a full extra operation, if performance is not as much a concern as code cleanliness you could just reverse the list when the opposite sorting direction is chosen. Note that this would be slightly different in the case of sorting equal elements, as sort in Perl is normally stable (equal elements stay in the same order they originally were).
my #sorted = sort { $$data{$a}{'size'} <=> $$data{$b}{'size'} } keys %{$data};
#sorted = reverse #sorted if $reverse;

Related

How to iterate through an Array of hashes in Perl?

I have the following array:
ifNameList -> $VAR1 = [
{
'VALUE' => ' gpon_olt-1/1/1',
'ASN1' => '285278465'
},
{
'VALUE' => ' gpon_olt-1/1/2',
'ASN1' => '285278466'
},
{
'VALUE' => ' gpon_olt-1/1/3',
'ASN1' => '285278467'
},
{
'VALUE' => ' gpon_olt-1/1/4',
'ASN1' => '285278468'
},
{
'VALUE' => ' gpon_olt-1/1/5',
'ASN1' => '285278469'
},
]
I need to iterate through this array of hashes comparing the "VALUE" field of each hash, until it matches and do some action.
I've already made the following code, but its not working. What I'm doing wrong?
sub GetIfIndexFromName{
my $ifName = shift;
my #ifList = shift;
my $index;
for (#ifList){
my %interfaceHash = %$_;
# Just trims any blank space on the string:
$interfaceHash->{"VALUE"} =~ s/^\s+|\s+$//g;
if($interfaceHash->{"VALUE"} eq $ifName){
print "trimmed interface name-> ".$interfaceHash->{"VALUE"}."\n\n";
$index = $interfaceHash->{"ASN1"};
}
}
print "Returning index value: ".$index;
return $index;
}
Two errors.
Problem 1: Wrong variable
ALWAYS use use strict; use warnings;. It would have found this error:
# Access the `VALUE` element of the hash referenced by `$interfaceHash`.
$interfaceHash->{"VALUE"}
You have no variable named $interfaceHash.
There are three ways to fix this:
for ( #ifList ) {
my %interfaceHash = %$_;
... $interfaceHash{ VALUE } ...
}
for my $interfaceHash ( #ifList ) {
... $interfaceHash->{ VALUE } ...
}
The latter is recommended. It avoids creating a copy of the hash, which involves create a number of temporary scalars. This is all useless work.
Problem 2: Incorrect parameter retrieval
The following is wrong:
my #ifList = shift;
shift returns a scalar. There's absolutely no point in using an array to hold exactly one scalar at all times.
sub GetIfIndexFromName {
my $ifName = shift;
my $ifList = shift;
for ( #$ifList ) {
...
}
}
# Pass a reference to the array.
GetIfIndexFromName( $ifName, $VAR1 )
sub GetIfIndexFromName {
my $ifName = shift;
my #ifList = #_;
for ( #ifList ) {
...
}
}
# Pass each element of the array.
GetIfIndexFromName( $ifName, #$VAR1 )
The former convention is more efficient, but the latter can create cleaner code in the caller. Probably not in your program, though.
How I'd write this:
use strict;
use warnings;
use feature qw( say );
use List::Util qw( first );
sub trim_inplace { $_[0] =~ s/^\s+|\s+\z//g; }
my #ifList = ...;
my $ifName = ...;
trim_inplace( $_->{ VALUE } ) for #ifList;
my $match = first { $_->{ VALUE } eq $ifName } #ifList
or die( "Interface not found.\n" );
my $asn1 = $match->{ ASN1 };
say $asn1;

convert hash to array of hashes in Perl

How simply convert a hash to an array of key/value ?
my %h;
%h{1} = 11;
%h{2} = 22;
and I want an array #result which I could represent as : [ { 1 => 11 }, { 2 => 22} ] (yes like in json to be clear)
That's an easy one.
my #h = map { { $_ => $h{$_} } } keys %h;
You could also use the built in List::Util library's excellent pairmap function.
use List::Util qw< pairmap >;
my #array_of_pairs = pairmap { { $a => $b } } %some_hash;
Even if you don't use this function today, take some time to check out List::Util, there is a lot of good stuff in there.

Perl: find the max and min values in the array of hashes

I have the structure below in Perl:
#!/usr/bin/perl
use strict;
use warnings;
my %hash = (
'firstitem' => {
'1' => ["A","99"],
'2' => ["B","88"],
'3' => ["C","77"],
},
'seconditem' => {
'3' => ["C","100"],
'4' => ["D","200"],
'5' => ["E","300"],
},
);
I am looking for a way to find the max number and min number in the array of each hash.
So the output will be
firstitem: max:99, min:77
seconditem: max:300, min:100
My idea is sorting the secondary key first and then do the bubble sort or other sorts in the for loop. It looks like not very elegant and smart.
foreach my $k1 (keys %hash) {
my $second_hash_ref = $hash{$k1};
my #sorted_k2 = sort { $a <=> $b } keys %{$second_hash_ref};
foreach my $i (0..$#sorted_k3){
#bubble sort or other sort
}
}
List::Util is a core module that provides the min and max functions:
use strict;
use warnings;
use List::Util qw(min max);
my %hash = (
'firstitem' => {
'1' => ["A","99"],
'2' => ["B","88"],
'3' => ["C","77"],
},
'seconditem' => {
'3' => ["C","100"],
'4' => ["D","200"],
'5' => ["E","300"],
},
);
for my $key (keys(%hash)) {
my #numbers = map { $_->[1] } values(%{$hash{$key}});
printf("%s: max: %d, min: %d\n", $key, max(#numbers), min(#numbers));
}
Output:
firstitem: max: 99, min: 77
seconditem: max: 300, min: 100
You are almost there.
Once you get to the second level what you need is the whole list of numbers, so to be able to find max and min of the lot. Since you need the extreme values for all arrays within the subkey you don't need to iterate over the second-level keys.
Also, you can fetch all array content unless the number is guaranteed to be at the specific place. Then use grep with Scalar::Util::looks_like_number to filter out elements that aren't numbers.
Finally, there is no benefit in sorting the keys.
use warnings;
use strict;
use feature 'say';
use Scalar::Util 'looks_like_number';
use List::MoreUtils 'minmax';
my %hash = (
'firstitem' => {
'1' => ["A","99"], '2' => ["B","88"], '3' => ["C","77"],
},
'seconditem' => {
'3' => ["C","100"], '4' => ["D","200"], '5' => ["E","300"],
},
);
foreach my $k1 (keys %hash)
{
my #nums =
grep { looks_like_number($_) }
map { #{ $hash{$k1}{$_} } }
keys %{$hash{$k1}};
my ($min, $max) = minmax #nums;
say "$k1: max = $max, min = $min";
}
This prints the expected values. If your real hash is exactly as shown you can extract arrayrefs directly by values %{$hash{$k1}}, with map { #$_ } to dereference them.
Since you need both extremes a good fit is minmax from List::MoreUtils, which is "the most efficient possible algorithm" for the job. The algorithm's performance is fully realized in the XS version of the module while the Perl version has some overhead.
Also note the core List::Util module, where individual min and max are, among other utilities.

Generating a unordered list hash from a array

I am trying to use hashes to generate an unordered list that i can further use in a jstree. But this array has to be generated only from an array that has been passed thru .
my #array = ( "New Order","Recurring Order","Previously Cancelled Order");
I want the output to look something like
$data = {
"New Order" => {
"Recurring Order" =>{
Previously cancelled Order = 1
}
}
};
I can simply do
my $data{$array[0]}{$array[1]}{$array[2]} = 1
but the array can be of n variables, so it becomes a bit more complicated than that. I am thinking of recursion, but i have been sitting here for the last hour trying to figure that out
This will generate the data structure as you have defined it. Not sure why you'd want it though.
my #input = ( "New Order","Recurring Order","Previously Cancelled Order");
my $data = 1;
$data = {$_ => $data} for reverse #input;
use Data::Dump;
dd $data;
If you're just wanting to randomize your array, then use List::Util;
use List::Util qw(shuffle);
my #newOrder = shuffle #input;
sub recursive {
my $v = shift #_;
return #_>1 ? { $v => recursive(#_) } : { $v => #_ };
}
my #array = ( "New Order","Recurring Order","Previously Cancelled Order");
use Data::Dumper; print Dumper recursive(#array, 1);
output
$VAR1 = {
'New Order' => {
'Recurring Order' => {
'Previously Cancelled Order' => 1
}
}
};

Perl: Get parallel value in hash array

I have this:
my(%arr) = (
monsters => ["Test","Test2"],
kills => [-1, -2 ]);
Then later I search for Test2:
if ( grep { $_ eq "Test2"} #{ $arr{monsters} } )
{
#Get parallel value of Test2 (-2)
next;
}
How can I get the parallel value without knowing the index (an actual variable is used when searching and not a string literal)?
Rather than using a grep, just loop over the array and keep a count variable:
for my $idx( 0 .. $#{ $arr{monsters} } ) {
if ( $arr{monsters}[$idx] eq 'Test2' ) {
print "Kills = $arr{kills}[$idx]\n";
last;
}
}
A better way to handle this, however, might be to rethink your data structure. Instead of parallel arrays, consider an array of hashes:
my #monsters = ( { name => 'Test', kills => -1 }, { name => 'Test2', kills => -2 } );
Now, to find a specific monster:
my ( $monst ) = grep { $_->{name} eq 'Test2' } #monsters;
print $monst->{kills};
This would allow you to search by name and kills equally easily. If you are going to always search by name, then making a hash keyed on name and pointing to the number of kills (as #dmah suggests) might be better.
An even better way to handle this would be to wrap up your monsters in a class, and have each object keep track of its own kills, but I'll leave that as an exercise for the OP.
Try a hash of hashes:
my %arr = (
'Test' => {
'kills' => -1,
},
'Test2' => {
'kills' => -2,
},
);
print $arr{'Test2'}{'kills'}, "\n";

Resources