Perl Hash : get keys if the value is in-between a range - loops

I have a two dimensional hash with 100K keys(primary) like this and I need to get the primary key - the name of the fruit only if a particular condition is satisfied;
like - if the price is between 35 and 55; Desired output is Orange and Grape.
And there is a list (hundreds in count) of unique price ranges for which I need a list of fruits within each range.
Iterating through hash again and again for each price range takes a lot of time.
Is there a way we can do it quickly instead of looping through the entire hash for each price range?
Hash format :
$fruits{"Mango"}{color}=Yellow
$fruits{"Mango"}{price}=80
$fruits{"Orange"}{color}=Orange
$fruits{"Orange"}{price}=40
$fruits{"Grape"}{color}=Green
$fruits{"Grape"}{price}=50

Here is an example of how you can do a single scan through the fruits by sorting the the prices in numerical order. This should be faster than scanning the whole hash once for each price range:
package Main;
use v5.20.0;
use feature qw(say);
use strict;
use warnings;
use experimental qw(signatures);
{
my %fruits;
$fruits{Mango}{color} = "Yellow";
$fruits{Mango}{price} = 80;
$fruits{Orange}{color} = "Orange";
$fruits{Orange}{price} = 40;
$fruits{Grape}{color} = "Green";
$fruits{Grape}{price} = 50;
my #ranges = ( [35, 55], [45, 55], [2, 85] );
my $self = Main->new(
fruits => \%fruits,
ranges => \#ranges
);
$self->init_mapping_arrays();
my $names = $self->get_fruit_names_for_all_ranges();
}
sub init_mapping_arrays( $self ) {
my #prices;
my #names;
for my $fruit (keys %{ $self->{fruits} }) {
push #names, $fruit;
push #prices, $self->{fruits}{$fruit}{price};
}
my #idx = map { $_->[0] }
sort { $a->[1] <=> $b->[1] } map { [$_, $prices[$_]] } 0..$#prices;
$self->{prices} = [#prices[#idx]];
$self->{names} = [#names[#idx]];
}
sub get_fruit_names_for_all_ranges ($self) {
my #names;
my $prices = $self->{prices};
my $ranges = $self->{ranges};
for my $i (0..$#$prices) {
for my $range (0..$#$ranges) {
if ( ($ranges->[$range][0] <= $prices->[$i])
&& ($ranges->[$range][1] >= $prices->[$i]))
{
push #{$names[$range]}, $self->{names}[$i];
}
}
}
return \#names;
}
sub new( $class, %args ) { bless \%args, $class }
If this is not fast enough, the get_fruit_names_for_all_ranges() sub can be optimized further by also sorting the ranges.

If the fruits are sorted, two binary searches would find the fruits quickly.
sub search_cmp
my #fruits = (
{ name => "Orange", price => 40, ... },
...
);
my #ranges = (
[ 35, 55 ],
...
);
my #sorted_fruits = sort { $a->{price} <=> $b->{price} } #fruits;
for my $range (#ranges) {
my $i = binsearch { $a <=> $b->{price} } $range[0], #sorted_fruits, 0;
$i = ~$i if $i < 0;
my $j = binsearch { $a <=> $b->{price} } $range[1], #sorted_fruits, $i;
$j = ~$j - 1 if $j < 0;
say "[$range->{min}, $range->{max}]: #fruits[$i..$j]";
}
sub _unsigned_to_signed { unpack('j', pack('J', $_[0])) }
sub binsearch(&$\#;$$) {
my $compare = $_[0];
#my $value = $_[1];
my $array = $_[2];
my $min = $_[3] // 0;
my $max = $_[4] // $#$array;
return -1 if $max == -1;
my $ap = do { no strict 'refs'; \*{caller().'::a'} }; local *$ap;
my $bp = do { no strict 'refs'; \*{caller().'::b'} }; local *$bp;
*$ap = \($_[1]);
while ($min <= $max) {
my $mid = int(($min+$max)/2);
*$bp = \($array->[$mid]);
my $cmp = $compare->()
or return $mid;
if ($cmp < 0) {
$max = $mid - 1;
} else {
$min = $mid + 1;
}
}
return _unsigned_to_signed(~$min);
}
Performance analysis
The best possible worst case is O(R * F) because every range could match every fruit.
The naive approach described by the OP asked to replace is O(R * F). So is it as fast as it can be? No, because the naive approach always adopts its worse case.
In practice, if we can assume that each range matches just a few fruits, we can get far far better results on average from the above: O( ( F + R ) log F )

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;

Accessing array of hashes in Perl

I have problem with accessing a hash in each element of array after creating it but it gave the last element. What should I do to access all the elements of my array?
When I want push a hash to an array I use {} instead of () because if I don't it gave me error. How does it see when I use {}?
#stem = ();
for ($i = 0; $i < 2; ++$i) {
push #stem, { u1 => 1, u2 => 2 , u3 => 3 };
}
#ants = ();
$count = 0;
for ($i = 0; $i < scalar(#stem); ++$i) {
#allowed = ();
%hash = ();
for ($j = 0; $j < scalar(#stem); ++$j) {
push #allowed, { stem => ++$count, hinfo => ++$count };
}
%hash = (allowed => \#allowed, solution => ++$count);
push (#ants, \%hash);
}
for ($i = 0; $i < scalar(#ants); ++$i) {
%test = %{$ants[$i]};
print "=>", $test{solution}, "\n";
#temp = #{$test{allowed}};
for ($j = 0; $j < scalar(#temp); ++$j) {
print $j, ":", $temp[$j]->{stem}, " ", $temp[$j]->{hinfo}, "\n";
}
}
Output:
=>21
0:16 16
1:18 18
2:20 20
=>21
0:16 16
1:18 18
2:20 20
2) When I want push a hash to an array I use {} instead of () because if I don't it gave me error. How does it see when I use {}?
I can answer this Question 2.
When you use () perl sees as list of elements. When you use {} perl sees it as reference to a hash.
So When you do this: push #Arr, (x=>2, y=>5); four elements will be pushed to #Arr: x, 2, y, 5. Which is not your intention.
But When you do this: push #Arr , {x => 2, y => 5}; a single reference to an anonymous hash containing x and y as the keys, and 2 and 5 as the respective values will be pushed to #Arr.

Loop through multidimensional Hash of Arrays in Perl

I have a multidimensional hash of arrays that represent a student's grade in each subject for the first four assignments.
my %students_grades = (
Colton => {
English => [ 90, 95, 80, 75 ],
Mathematics => [ 77, 89,94, 100 ],
},
);
The syntax is a bit off but here's the code that creates the hash of arrays above.
#!/usr/bin/perl
my %students_grades;
$students_grades{'Colton'}{'English'}[0] = 90;
$students_grades{'Colton'}{'English'}[1] = 95;
$students_grades{'Colton'}{'English'}[2] = 80;
$students_grades{'Colton'}{'English'}[3] = 75;
$students_grades{'Colton'}{'History'}[0] = 77;
$students_grades{'Colton'}{'History'}[1] = 89;
$students_grades{'Colton'}{'History'}[2] = 94;
$students_grades{'Colton'}{'History'}[3] = 100;
How do I loop through the student's grades he received in History using a foreach loop? Right now I'm looping through it using a for loop.
my $num_of_grades = scalar #{$students_grades{'Colton'}{'History'}};
for (my $i=0; $i <= $num_of_grades; $i++) {
print $students_grades{'Colton'}{'History'}[$i] . "\n";
}
This is a representation of my code but in my actual program my hash of arrays is more complicated so I want to loop through the hash of arrays using a foreach loop because it'll be easier to handle. How do I do that?
for (my $i=0; $i<#array; ++$i) {
my $ele = $array[$i];
...
}
can be written simply as
for my $ele (#array) {
...
}
so you could have used the following:
for my $grade (#{ $students_grades{'Colton'}{'English'} }) {
print("$grade\n");
}
my $grade = $students_grades{'Colton'}{'English'}[0];
is short for
my $grade = $students_grades{'Colton'}->{'English'}->[0];
which means you can do
my $grades = $students_grades{'Colton'}{'English'};
my $grade = $grades->[0];
which means you could also have used the following:
my $grades = $students_grades{'Colton'}{'English'};
for my $grade (#$grades) {
print("$grade\n");
}
Knowing this allows one to easily escalate to the following:
for my $student_name (keys(%students_grades)) {
my $student_grades_by_class = $students_grades{$student_name};
for my $class_name (keys(%$student_grades_by_class)) {
my $grades = $student_grades_by_class->{$class_name};
for my $grade (#$grades) {
print("$student_name: $class_name: $grade\n");
}
}
}
In perl, for and foreach mean the same.
But the thing here is - you don't have a hash of arrays - you have a hash of array references.
So:
$students_grades{'Colton'}{'History'}
is actually an array reference.
So you can dereference it:
foreach my $grade ( #{ $students_grades{'Colton'}{'History'} } ) {
print "$grade\n";
}

PERL dynamically match arrays based on unique key

I'm trying to compare 2 huge arrays and want to use map. I am using unique key concept here to match the indexes.
My Arrays:
my #array1 = ( ['a','b','c','d'], ['e','f','g','h'], ['i','j','k','l'], ['m','n','o','p'], ['q','r','s','t']);
my #array2 = ( ['r','q','s','t'], ['b','a','c','d'], ['n','m','o','p'], ['f','e','g','h'], ['j','i','k','l']);
My unique Keys:
my #uk1 = (0,2,3);
my #uk2 = (1,2,3);
These arrays will be huge in size, over 30,000 indexes in each with over 20 elements in each index.
So effectively i create a map where
for ( my $j = 0; $j <= $#array1 ; $j++ )
{
my searchString;
for ( my $k = 0; $k <= $#uk1; $k++ )
{
if ( $k != 0 )
{
$searchString .= ","
}
$my searchString .= $array1[$j][$uk[$k];
}
my #result = map { $_ }
grep { join (",",$array2[$_][1],$array2[$_][2],$array2[$_][3]) ) =~ join(",",$array1[$j][0],$array1[$j][1],$array1[$j][2]) }
0 .. $#array;
}
returns matched indexes.
My problem is, how do i make this dependant on the unique keys? as the length of the unique key will keep changing and as far as i know i cannot dynamically create the $array2[$_] join part.
Hope my question is clear enough.
I want to have the logic that compares
$array1[$uk1[0]],$array1[$uk1[1]],$array1[$uk1[2]] and so on (depending on the number of keys in UK) with
$array2[$uk2[0]],$array2[$uk2[1]],$array2[$uk2[2]].......
Perhaps,
my #array1 = ( ['a','b','c','d'], ['e','f','g','h'], ['i','j','k','l'], ['m','n','o','p'], ['q','r','s','t']);
my #array2 = ( ['r','q','s','t'], ['b','a','c','d'], ['n','m','o','p'], ['f','e','g','h'], ['j','i','k','l']);
my #result;
for my $i (0 .. $#array1) {
push #result,
map { [$i, $_] }
grep {
"#{ $array1[$i] }[1,2,3]" eq "#{ $array2[$_] }[0,2,3]"
}
0 .. $#array2;
}
use Data::Dumper; print Dumper \#result;
output
$VAR1 = [
[
0,
1
],
[
1,
3
],
[
2,
4
],
[
3,
2
],
[
4,
0
]
];
What you want to use is an array slice:
But lets also make life easier:
for my $sample ( #array1 )
{
my $test= join(",", #$sample[#uk1]) ;
my #result = grep { $_ eq $test } map { join(",", #$_[#uk2] ) } #array2 ;
say "huzzah" if #result ;
}
Perl lets you specify multiple elements from an array via the "array slice":
my #list= ('a', 'b', 'c', 'd') ;
my #pieces= #list[1,3] ;

Iterate through a hash and an array in Perl

I have an array and a hash:
#arraycodons = "AATG", "AAAA", "TTGC"... etc.
%hashdictionary = ("AATG" => "A", "AAAA" => "B"... etc.)
I need to translate each element of the array for the corresponding value in hashdictionary. However, I obtain a wrong translation.....
To see the problem, I have printed $codon (each element of the array), but each codon is repeated several times..... and It shouldn't.
sub translation() {
foreach $codon (#arraycodons) {
foreach $k (keys %hashdictionary) {
if ($codon == $k) {
$v = $hashdictionary{$k};
print $codon;
}
}
}
}
I don't know if I've explained my problem well enough, but I can't go on with my code if this doesn't work...
Many thanks in advance.
You appear to be looping through the keys of your hash (also known as a "dictionary") to find your desired key. This defeats the purpose of a hash (also known as a "dictionary") - the primary advantage of which is ultra fast lookups of a key.
Try, instead of
foreach $codon (#arraycodons) {
foreach $k (keys %hashdictionary) {
if ($codon == $k) {
$v = $hashdictionary{$k};
print $codon;
}
}
}
this:
foreach $codon (#arraycodons) {
my $value = $hashdictionary{$codon};
print( "$codon => $value\n" );
}
or:
foreach my $key ( keys %hashdictionary ) {
my $value = $hashdictionary{$key};
print( "$key => $value\n" );
}
my #mappedcodons = map {$hashdictionary{$_}}
grep (defined $hashdictionary{$_},#arraycodons);
or
my #mappedcodons = grep ($_ ne "", map{$hashdictionary{$_} || ""} #arraycodons);
my #words = ("car", "house", "world");
my %dictionary = ("car" => "el coche", "house" => "la casa", "world" => "el mundo");
my #keys = keys %dictionary;
foreach(#words) {
my $word = $_;
foreach(#keys) {
if($_ eq $word) { # eq, not ==
my $translation = $dictionary{$_};
print "The Spanish translation of $word is $translation\n";
}
}
}

Resources