How do I iterate through an array of hashes of arrays once per hash, and not once per nested array element?
-
27-06-2021 - |
سؤال
Apologies if this has been resolved elsewhere. I searched and searched.
I'm working with a config file that separates chunks of data by left-aligning the name of each chunk and indenting arguments for each name (see __DATA__
below). I've written a script that stores each chunk into a hash having a $hash{name}
key pointed at a scalar value, and a %hash{args}
key pointed at an array of values. As each chunk is stored into its own hash, a reference to the hash is stored anonymously in an array. Eventually I'd like to grab these hashes one by one and process the values in them but I'm having trouble iterating through the array.
When I try to print the values stored in each hash it seems to be taking the hash refs in that array in list context, so if %hash{args} was a reference to an array having three elements, that's three more times the foreach loop runs.
How do I get the code in that loop to just run once for each hash reference I've socked away into that array?
If you check my output it's clear that the nested array reference needs to be dereferenced but I'm so stuck on getting the loop right that I haven't been able to tackle that yet. Maybe the solution will fix both?
Behold:
use strict;
use warnings;
my @array;
my %hash;
my ($name, $args);
while (my $line = <DATA>) {
chomp($line);
if ($line !~ /^\s/)
{
my ($key) = $line =~ /^\S+/g;
$hash{name} = $key;
print "Defined a name $key\n";
}
else
{
$line =~ s/^\s+//;
push (@{ $hash{args} }, $line);
print "Defined an arg $line\n";
}
push (@array, \%hash);
}
foreach my $i (@array)
{
foreach my $h (keys %{$i})
{
print $h . "\t";
print $i->{$h} . "\n";
}
}
__DATA__
Sports
Basketball
Tennis
Boxing
Guys
Tom
Dick
Harry
And here's the output:
Defined a name Sports
Defined an arg Basketball
Defined an arg Tennis
Defined an arg Boxing
Defined a name Guys
Defined an arg Tom
Defined an arg Dick
Defined an arg Harry
args ARRAY(0x4a8e24)
name Guys
args ARRAY(0x4a8e24)
name Guys
args ARRAY(0x4a8e24)
name Guys
args ARRAY(0x4a8e24)
name Guys
args ARRAY(0x4a8e24)
name Guys
args ARRAY(0x4a8e24)
name Guys
args ARRAY(0x4a8e24)
name Guys
args ARRAY(0x4a8e24)
name Guys
المحلول
I think you need a different data structure. Try this:
use strict;
use warnings;
use Data::Dumper;
my @array;
my %hash;
my ( $name, $args, $key );
while ( my $line = <DATA> ) {
chomp($line);
if ( $line !~ /^\s/ ) {
$key = $line; #=~ /^\S+/g;
print "Defined a name $key\n";
}
else {
$line =~ s/^\s+//;
push (@{$hash{$key}}, $line);
print "Defined an arg $line\n";
}
}
print Dumper(\%hash);
__DATA__
Sports
Basketball
Tennis
Boxing
Guys
Tom
Dick
Harry
Output:
$VAR1 = {
'Sports' => [
'Basketball',
'Tennis',
'Boxing'
],
'Guys' => [
'Tom',
'Dick',
'Harry'
]
};
I tried to keep the solution close to your own code.
Edit: If you deal with multi-level data structures always used Data::Dumper
to dump what exactly is in your array/hash. This will help you resolve assignment problems such as this instance.
Your data structure looked like this:
$VAR1 = [
{
'args' => [
'Basketball',
'Tennis',
'Boxing',
'Tom',
'Dick',
'Harry'
],
'name' => 'Guys'
},
$VAR1->[0],
$VAR1->[0],
$VAR1->[0],
$VAR1->[0],
$VAR1->[0],
$VAR1->[0],
$VAR1->[0]
];
نصائح أخرى
Your problems:
While buidling your data structures you have made a few mistakes:
- You always assign a new
$key
to the same$hash{name}
field. Previous contents are overwritten. The last valueGuys
stays. - You push a reference of the
%hash
into the@array
. Because the reference reflects all the changes of the original object, you get the same output every time.
The solution:
Use a different data structure ;-)
Your data works like this: We have a Set of names that each carry a List of values. Sets are modelled with hashes in Perl, so we use one:
my %hash = ();
Each entry will contain a reference to an array containing the values. The final data structure will look like this:
%hash = (
Sports => ["Basketball", "Tennis", "Boxing"],
Guys => ["Tom", "Dick", "Harry"]
);
You can inspect your current data structure with the Data::Dumper
module:
use Data::Dumper;
print Dumper (\%name); # has to be given references
To use this data structure, only minimal improvements have to be made to your code:
my %hash;
my $key;
while (my $line = <DATA>) {
chomp $line;
if ($line =~ /^(\S+)/) {
$key = $1;
$hash{$key} = []; # this is the main difference
print "Defined a name $key\n";
} else {
$line =~ s/^\s+//;
push @{ $hash{$key} }, $line;
print "Defined an arg $line\n";
}
}
I changed the coding style a little bit as well, but essentially the only difference is that I use the name as a hash key. I remember the last $key
used outside the loop so that all args know where to go.
When I want to print out the data structure, I would use the each
function if the order of names in not important:
while (my ($name, $arrayRef) = each %hash) {
my @args = @$arrayRef; # dereference the array
print qq{name $name with args "@args"\n};
}
or
while (my ($name, $arrayRef) = each %hash) {
print "name\t$name\n";
print "arg\t$_\n" foreach @$arrayRef;
}