I have a few arrays of the same length. I want to sort the first array, and make all the others array "sort" accordingly. For example, if the first array is (7,2,9) the second is ("seven","two","nine") and the third is ("VII","II","IX") after the sort (ascendingly according to the first array values) we will have (2,7,9) ("two","seven","nine") and ("II","VII","IX").
How can I do that?
While I agree with eugene y and MvanGeest that usually the best answer is to switch to another data structure, sometimes you might want parallel arrays (or at least, might not be able to avoid them), and there actually is a way to sort parallel arrays in parallel. It goes like this:
my #nums = (7, 2, 9);
my #names = qw(seven two nine);
my #roman = qw(VII II IX);
my #sorted_indices = sort { $nums[$a] <=> $nums[$b] } 0..$#nums;
#$_ = #{$_}[#sorted_indices] for \(#nums, #names, #roman);
That is, generate a list of the indices that correspond to all of the arrays, and then sort them according to the order that will put the "primary" array in order. Once we have the sorted list of indices, re-order all of the arrays to match.
The final line could be written out longhand as
#nums = #nums[#sorted_indices];
#names = #names[#sorted_indices];
#roman = #roman[#sorted_indices];
but I tried to reduce the amount of copy-paste necessary, even at the cost of some slightly hairy syntax. The More You Know...
I know you've already accepted an answer, and there are other really good
answers here, but I would propose something different: don't duplicate your
data. You only need to keep track of the arabic -> roman mapping once -- why
store what are essentially duplicate arrays of numbers, and sort every one?
Just sort the master list and look up the other values in a reference array as needed:
my #roman = qw(0 I II III IV V VI VII VIII IX X);
my #text = qw(zero one two three four five six seven eight nine ten);
my #values = (7, 2, 9);
my #sorted_values = sort #values;
my #sorted_roman = map { $roman[$_] } #sorted_values;
my #sorted_text = map { $text[$_] } #sorted_values;
use Data::Dumper;
print Dumper(\#sorted_values, \#sorted_roman, \#sorted_text);
prints:
$VAR1 = [
2,
7,
9
];
$VAR2 = [
'II',
'VII',
'IX'
];
$VAR3 = [
'two',
'seven',
'nine'
];
In a real environment, I would suggest using libraries to perform the
Roman and textual conversions for you:
use Roman;
my #sorted_roman = map { roman($_) } #sorted_values;
use Lingua::EN::Numbers 'num2en';
my #sorted_text = map { num2en($_) } #sorted_values;
Re-organize the data to a single array for sorting:
my #a = ([7, "seven", "VII"], [2, "two", "II"], ..);
#a = sort { $a->[0] <=> $b->[0] } #a;
Then recreate the original arrays:
my(#a1, #a2, #a3);
for (#a) {
push #a1, shift #$_;
push #a2, shift #$_;
push #a3, shift #$_;
}
As you are discovering, maintaining parallel arrays can be a hassle and error prone. An alternative approach is to keep related information together.
use strict;
use warnings;
# One array-of-hashes instead of three parallel arrays.
my #numbers = (
{ arabic => 7, text => 'seven', roman => 'VII' },
{ arabic => 2, text => 'two', roman => 'II' },
{ arabic => 9, text => 'nine', roman => 'IX' },
);
#numbers = sort { $a->{arabic} <=> $b->{arabic} } #numbers;
Related
This question already has answers here:
Simple hash search by value
(5 answers)
Closed 5 years ago.
I recently started learning Perl, so I'm not too familiar with the functions and syntax.
If I have a Perl array and some variables,
#!/usr/bin/perl
use strict;
use warnings;
my #numbers = (a =>1, b=> 2, c => 3, d =>4, e => 5);
my $x;
my $range = 5;
$x = int(rand($range));
print "$x";
to generate a random number between 1-5, how can I get the program to print the actual key (a, b, c, etc.) instead of just the number (1, 2, 3, 4, 5)?
It seems that you want to do a reverse lookup, key-by-value, opposite to what we get from a hash. Since a hash is a list you can reverse it and use the resulting hash to look up by number.
A couple of corrections: you need a hash variable (not an array), and you need to add 1 to your rand integer generator so to have the desired 1..5 range
use warnings;
use strict;
use feature 'say';
my %numbers = (a => 1, b => 2, c => 3, d => 4, e => 5);
my %lookup_by_number = reverse %numbers; # values need be unique
my $range = 5;
my $x = int(rand $range) + 1;
say $lookup_by_number{$x};
Without reversing the hash you'd need to iterate the hash %numbers over values, testing each against $x so to find its key.
If there are same values for various keys in your original hash then you have to do it by hand since reverse-ing would attempt to create a hash with duplicate keys, in which case only the last one assigned remains. So you'd lose some values. One way
my #at_num = grep { $x == $numbers{$_} } keys %numbers;
as in the post that this was marked as duplicate of.
But then you should build a data structure for reverse lookup so to not search through the list every time information is needed. This can be a hash where keys are the list of unique numbers while their values are then array references (arrayrefs) with corresponding keys from the original hash
use warnings;
use strict;
my %num = (a => 1, b => 2, c => 1, d => 3, e => 2); # with duplicate values
my %lookup_by_num;
foreach my $key (keys %num) {
push #{ $lookup_by_num{$num{$key}} }, $key;
}
say "$_ => [ #{$lookup_by_num{$_}} ]" for keys %lookup_by_num;
This prints
1 => [ c a ]
3 => [ d ]
2 => [ e b ]
A nice way to display complex data structures is via Data::Dumper, or Data::Dump (or others).
The expression #{ $lookup_by_num{ $num{$key} } } extracts the value of %lookup_by_num for the key $num{$key}and dereferences it #{ ... }, so that it can then push the $key to it. The critical part of this is that the first time it encounters $num{$key} it autovivifies the arrayref and its corresponding key. See this post with its references for details.
There's many ways to do it. For example, declare "numbers" as a hash rather than an array. Note that the keys come first in each key-value pair, and here you want to use your random int as the key:
my %numbers = ( 0 => 'a', 1 => 'b', 2 => 'c', 3 => 'd', 4 => 'e' );
Then you can look up the "key" as you call it using:
my $key = $numbers{$x};
Note that rand( $x ); returns a number greater than or equal to zero and less than $x. So if you want integers in the range 1-5, you must add 1 in your code: at the moment you'll get 0-4, not 1-5.
Firstly, arrays don't have keys (well, they kind of do, but they're integers and not the values you want). So I think you want a hash, not an array.
my %numbers = (a =>1, b=> 2, c => 3, d =>4, e => 5);
And if you want to get the letter, given the integer then you need the reverse of this hash:
my %rev_numbers = %numbers;
Note that reversing a hash like this only works if the values in your original hash are unique (because reversing a hash makes the values into keys and hash keys are always unique).
Then, you can just look up an integer in your %rev_hash to get its associated letter.
my $integer = 3;
say $rev_numbers{$integer}; # prints 'c'
How can i reset the hash values in perl
use warnings;
use strict;
my %hash = qw(one 1 two 2 three 3 four 4);
my #key = keys(%hash);
my #avz = (9..12);
my %vzm;
print "Original hash and keys : ",%hash,"\n";
for(my $i = 0; $i<=scalar #avz; $i++){
my #new = "$key[$i] $avz[$i] ";
push(%vzm , #new);
}
print "modified hash and keys",%vzm,"\n";
I tried to alter the keys of original hash with another keys. How can i do it
This program give the error is:
Original hash and keys : three3one1two2four4
Not an ARRAY reference at key.pl line 10.
I expect the output is
Original hash and keys : three3one1two2four4
modified hash and keys : three11one9two10four12
How can i do it
Ok, first off - you're doing something nasty in your code:
You're trying to take an ordered data structure - an array - and push it into a keyed data structure, which has no particular ordering defined.
This isn't going to work very well - it technically works, because internally perl treats arrays and hashes similarly.
But for example your first assignment - what you're actually getting is:
my %hash = (
one => 1,
two => 2,
three => 3,
four => 4
);
You can access the keys (in no particular order) via keys(). And the values via values(). But to try and treat it like an array is undefined behaviour.
To add elements to your array:
$hash{'nine'} = 9;
To delete elements from your array:
delete ( $hash{'one'} );
You can iterate on keys or values - and combined with sort even do them in some sort of order. (Just bear in mind for sorting alphanumeric numbers you'll have a custom sort job).
foreach my $key ( sort keys %hash ) {
print "$key => $hash{$key}\n";
}
(Note - this is sorting by alphanumeric string, so gives:
four => 4
one => 1
three => 3
two => 2
If you want to sort by value:
foreach my $key ( sort { $hash{$a} <=> $hash{$b} } keys %hash ) {
print "$key => $hash{$key}\n";
}
And so you'll get:
one => 1
two => 2
three => 3
four => 4
So the real question remains - what are you actually trying to accomplish? The point of a hash is to give you an unordered mini-database of key-value pairs. Treating one like an array doesn't make an awful lot of sense. Either you're iterating hash elements in arbitrary order, or you're applying a specific sort to it - but one where you're relying on getting elements in a particular order is a bad plan - it may work, but it's not guaranteed to work, and that makes for bad code.
You have to keep the order of the keys in some array, or take it from original list
my #tmp = qw(one 1 two 2 three 3 four 4);
my %hash = #tmp;
# 'one', 'two', ..
my #key = #tmp[ grep !($_%2), 0 .. $#tmp ];
# ..
for my $i (0 .. $#avz) {
$vzm{ $key[$i] } = $avz[$i];
}
or using hash slice as more perlish approach,
#vzm{ #key } = #avz;
You can't do what you want (replace the values for keys in the hash in the order they originally were added) without keeping track of that order separately, since the hash doesn't have any particular order. In other words, this:
my #key = keys(%hash);
needs to be this:
my #key = ( 'one', 'two', 'three', 'four' );
Once you have that, you can just assign the values all at once with a hash slice:
my %vzm;
#vzm{#key} = #avz;
To create a hash element, you use assignment to $var{$key}.
for (my $i = 0; $i < scalar #avz; $i++) {
$vzm{$key[$i]} = $avz[$i];
}
Note also that the loop condition should be <, not <=. List/array indexes end at scalar #avz - 1.
I have a custom perl hash data structure . The sturcture is like bellow:
%myhash = (
1 => {
'scf1' => [
1,3,0,4,6,7,8,
],
'sef2' => [
10,15,20,30,
]
},
2 => {
'scf1' => [
10,3,0,41,6,47,81,
],
'scf3' => [
1,66,0,123,4,1,2435,33445,1
]
},
);
How I can access this kind of perl structure.
I'm afraid your code ... is showing signs that you are misunderstanding what hashes do, and how they work. Specifically, when you're referencing #{$myhash} - this is NOT the same as the %myhash that you undef.
Likewise - what's going on with #features? It looks like you're trying to build an array of arrays, but doing so by iterating through fetchrow_array and then pushing. Multidimensional arrays are sometimes the right tool for the job, but it is unclear why it would be suitable for what you're doing. (After all, you don't use it for anything else in this piece of code).
You've also got $line[2] - which is also not doing what you might think - it does NOT refer to $line, it's the second element of a list called #line - which doesn't exist.
You are also trying to process is list of database entries, and set it '-1' if it's undef.
We need some more detail about what data you're getting out of your database - $sth -> fetchrow_array() could be anything. However, I'd strongly suggest that what you want to do is name each of the fields as you go. I'd suggest you DON'T want to be using $line there, because it's ... well, wrong. You're iterating columns in the row you've just fetched.
Which field in your fetched array are the keys to your hash? It looks like you're trying to key on 'field 5' 'field 7' and trying to insert values of 'field 1' and 'field 2'. Is that correct?
Oh, and turn on use strict; use warnings whilst you're at it.
get the inner array:
my #array = #{$hash{1}->{'scf1'}};
# is same as
# my $array_ref = $hash{1}->{'scf1'};
# my #array = #{$array_ref};
# then you can
my $some_thing = $array[0];
or get one element:
$hash{1}->{'scf1'}->[0];
From your Data::Dumper dump, I see that you have a hash called %myhash. Each element in that hash contains a reference to another hash. And, each element in that inner hash contains a reference to an array.
Let's take your Data::Dumper, and restate it like this:
$myhash{1}->{sff1} = [1, 3, 0, 4, 6, 7, 8];
$myhash{1}->{sef2} = [10, 15, 20, 30];
$myhash{2}->{scf1} = [10, 3, 0, 41, 6, 47, 81];
$myhash{2}->{scf3} = [1, 66, 0, 123, 4, 2435, 33445, 1];
Same thing. It's just a bit more compact.
To print this out, we'll need to loop through each of these layers of references:
#
# First loop: The outer hash which is a plain normal hash
#
for my $outer_key ( sort keys %myhash ) {
#
# Each element in that hash points to another hash reference. Dereference
#
my %inner_hash = %{ $myhash{$outer_key} };
for my $inner_key ( sort keys %inner_hash ) {
#
# Finally, this is our array reference in the inner hash. Let's dereference and print
#
print "\$myhash{$outer_key}->{$inner_key}: ";
my #array = #{ $myhash{$outer_key}->{$inner_key} };
for my $value ( #array ) {
print "$value";
}
print "\n";
}
}
I have the following hash, and I need to find the duplicates between the top most hash values 6 and 4. I've tried a few solutions to no avail, and am not too familiar with Perl syntax to make it work.
The Hash I Have
$VAR1 = {
'6' => [ '1000', '2000', '4000' ],
'4' => [ '1000', '2000', '3000' ]
};
The Hash I Need
$VAR1 = {
'6' => ['4000'],
'4' => ['3000'],
'Both' => ['1000','2000']
}
Find all common elements, e.g. by deduplicating with a hash.
Find all elements that are not common.
Given two arrays #x, #y, this would mean:
use List::MoreUtils 'uniq';
# find all common elements
my %common;
$common{$_}++ for uniq(#x), uniq(#y); # count all elements
$common{$_} == 2 or delete $common{$_} for keys %common;
# remove entries from #x, #y that are common:
#x = grep { not $common{$_} } #x;
#y = grep { not $common{$_} } #y;
# Put the common strings in an array:
my #common = keys %common;
Now all that is left is to do a bit of dereferencing and such, but that should be fairly trivial.
No need for other modules. perl hashes are really good for finding uniq or common values
my %both;
# count the number of times any element was seen in 4 and 6
$both{$_}++ for (#{$VAR1->{4}}, #{$VAR1->{6}});
for (keys %both) {
# if the count is one the element isn't in both 4 and 6
delete $both{$_} if( $both{$_} == 1 );
}
$VAR1->{Both} = [keys %both];
I have a problem where pairs of numbers map to other pairs of numbers. For instance, (1,2)->(12,97). Some pairs may map to multiple other pairs, so what I really need is the ability to map a pair into a list of lists, like (1,2)->((12,97),(4,1)). At the end of the day I want to process each of the values (i.e., each list of lists) separately.
In Python, I could do this by simply saying:
key = ( x, y )
val = [ a, b ]
if (x,y) not in my_dict:
my_dict[ (x,y) ] = []
my_dict[ (x,y) ].append( [a,b] )
However, in Perl, I have to use refs for the keys and values. So I can certainly say:
$keyref = [ x1, y1 ]
$valref = [ a, b ]
%my_hash = { $keyref => $valref }
But what happens when another pair (x2,y2) comes along? Even if x2==x1 and y2==y1, $keyref=[x2,y2] will differ from the previous keyref generated, so I do not see a way to do the lookup. Of course, I could compare (x2,y2) with each dereferenced hash key, but after all, God gave us hash tables precisely to avoid the need to do so.
Is there a Perl solution?
Thanks,
-W.
In Perl, all hash keys are strings, or are "stringified" before lookup. Using an array reference as a key is usually the wrong approach.
What about using a "two-dimensional" hash?
$hash{$x1}{$y1} = [ $a, $b ];
# or
%hash = ( $x1 => { $y1 => [ $a, $b ] } );
($x2,$y2)=($x1,$y1);
print #{$hash{$x2}{$y2}}; # will print $a and $b
Like most things in Perl, TMTOWTDI.
Option 1: Use multidimensional array emulation
$hash{$x,$y} = [$a, $b];
See also the documentation for the built-in variable $;.
Option 2: Use the Hash::MultiKey module
tie %hash, 'Hash::MultiKey';
$hash{[$x, $y]} = [$a, $b];
Option 3: Use a HoH (hash of hashes) instead
$hash{$x}{$y} = [$a, $b];
I ended up using Socket Puppet's solution (in the form of Michael Carmen's Option 3). FYI, here is a little Perl script that carries out all the operations I need in my app.
Printed lines 2:,3: and 4:,5: just use different syntax to do the same thing, and lines 0: and 1: were just intended as sanity checks along the way.
What this this adds to the suggested solution is the use of an array of arrays as the value that goes along with a key.
#k1 = ( 12, 13 );
$aref = [ 11, 22 ];
$bref = [ 33, 44 ];
%h = {};
if( not exists $h{$k1[0]}{$k1[1]} ) {
print "initializing\n";
$h{$k1[0]}{$k1[1]} = [];
}
push #{$h{$k1[0]}{$k1[1]}}, $aref;
push #{$h{$k1[0]}{$k1[1]}}, $bref;
print "0: ", join ':', #{$h{$k1[0]}{$k1[1]}}, "\n";
print "1: ", join ':', ${$h{$k1[0]}{$k1[1]}}[0], "\n";
print "2: ", join ':', #{${$h{$k1[0]}{$k1[1]}}[0]}, "\n";
print "3: ", join ':', #{${$h{$k1[0]}{$k1[1]}}[1]}, "\n";
print "4: ", join ':', #{$h{$k1[0]}{$k1[1]}->[0]}, "\n";
print "5: ", join ':', #{$h{$k1[0]}{$k1[1]}->[1]}, "\n";
P.S. I would have added this as a comment but it was too long, and I thought it made sense to include a worked example.