How can I delete empty arrays/refs from a Perl hash? - arrays

Lets say I have the following Perl hash:
%hash = (
'A' => {
'B' => ['C', 'D', 'E'],
'F' => { 'G' => [], 'H' => [] },
'I' => []
} );
and I'd like to get rid of the []'s to get the hash result below:
%hash = (
'A' => [
'B' => ['C', 'D', 'E'],
'F' => [ 'G', 'H', 'I' ]
]
)
(I hope I got my {} and [] balanced, my apologies if not, but) essentially I'd like to make it so that no empty arrays/ref's exist. I'm sure this is possible/simple, but I'm not sure whether delete() will work, or if there's a better method or a Perl module out there. Can someone steer me in the right direction?

It appears like your data might be nested arbitrarily, and you want to walk through it recursively, rewriting certain patterns to others. For that, I'd be using Data::Visitor.
use Data::Visitor::Callback;
use List::MoreUtils 'all';
my $visitor = Data::Visitor::Callback->new(
hash => sub {
my ($self, $href) = #_;
# fold hashrefs with only empty arrayrefs as values into arrayrefs
if (all { ref $_ eq 'ARRAY' && !#{ $_ } } values %{ $href }) {
return [ keys %{ $href } ];
}
# strip k/v pairs with an empty arrayref as a value
return {
map {
$_ => $href->{$_}
} grep {
ref $href->{$_} ne 'ARRAY' || #{ $href->{$_} }
} keys %{ $href }
};
},
);
my %new_hash = %{ $visitor->visit(\%hash) };
This just illustrates the basic approach I'd use, and happens to work for the example input you gave. It might need various tweaks depending on what you want to do in the corner-cases pointed out in the other comments.

[This should be a comment, but I need the formatting.]
Your question is puzzling. (1) By what principle does the I key (from the original hash) end up inside the list for the F key (in the expected hash)? (2) And what should happen if F were to contain stuff besides the empty array refs (see my addition to the original hash)?
my %hash_orig = (
'A' => {
'B' => ['C', 'D', 'E'],
'F' => {
'G' => [],
'H' => [],
'Z' => ['FOO', 'BAR'], # Not in the OP's original.
},
'I' => [],
},
);
my %hash_expected = (
'A' => [
'B' => ['C', 'D', 'E'],
'F' => [ 'G', 'H', 'I'], # Where should the Z info go?
],
);

Walking a hash (tree, whatever) is a technique that any programmer should know. rafl uses a visitor module, but in some cases I think the cure is almost worse than the disease.
Is your expected output what you intended? It seems different that what you said in the text, as FM says. I use his hash in my example.
It's pretty easy if you use a queue. You start with the top-level hash. Every time you run into a hash ref, you add it to the queue. When you run into an array ref, you check that it has values and delete that key if it doesn't. Everything else you leave alone:
#!perl
use strict;
use warnings;
use 5.010;
my %hash = ( # From FM
'A' => {
'B' => ['C', 'D', 'E'],
'F' => {
'G' => [],
'H' => [],
'Z' => ['FOO', 'BAR'], # Not in the OP's original.
},
'I' => [],
},
);
my #queue = ( \%hash );
while( my $ref = shift #queue ) {
next unless ref $ref eq ref {};
KEY: foreach my $key ( keys %$ref ) {
if( ref $ref->{$key} eq ref {} ) {
push #queue, $ref->{$key};
next KEY;
}
elsif( ref $ref->{$key} eq ref [] ) {
delete $ref->{$key} if #{$ref->{$key}} == 0;
}
}
}
use Data::Dumper;
print Dumper( \%hash );
My output is:
$VAR1 = {
'A' => {
'F' => {
'Z' => [
'FOO',
'BAR'
]
},
'B' => [
'C',
'D',
'E'
]
}
};
That output sounds more like what you are asking for, rather than the reorganization that you specify. Can you clarify the output?

Related

Convert array to multidimensional hash

My task is convert array, containing hash with x keys to x-1 dimensional hash.
Example:
use Data::Dumper;
my $arr = [
{
'source' => 'source1',
'group' => 'group1',
'param' => 'prm1',
'value' => 1,
},
{
'source' => 'source1',
'group' => 'group1',
'param' => 'prm2',
'value' => 2,
},
];
my $res;
for my $i (#$arr) {
$res->{ $i->{source} } = {};
$res->{ $i->{source} }{ $i->{group} } = {};
$res->{ $i->{source} }{ $i->{group} }{ $i->{param} } = $i->{value};
}
warn Dumper $res;
my $res_expected = {
'source1' => {
'group1' => {
'prm1' => 1, # wasn't added, why ?
'prm2' => 2
}
}
};
However it doesn't work as expected, 'prm1' => 1 wasn't added. What is wrong and how to solve this task ?
The problem is that you are assigning to the source even if something was there, and you lose it. Just do a ||= instead of = and you'll be fine.
Or even easier, just use the fact that Perl autovivifies and leave that out.
my $res;
for my $i (#$arr) {
$res->{ $i->{source} }{ $i->{group} }{ $i->{param} } = $i->{value};
}
warn Dumper $res;
The first 2 lines in the for loop are what is causing your problem. They assign a new hash reference each iteration of the loop (and erase what was entered in the previous iteration). In perl, there is no need to set a reference as you did. Just eliminate the first 2 lines and your data structure will be as you wish.
The method you chose only shows 'prmt' => 2 because that was the last item entered.

perl: Plotting data from multiple arrays

In my perl code, I have several arrays with the name 'a', 'b', 'c'.... 'e', 'f'. I am sending them as the argument while calling 'MyFunc'. There I want to plot any two arrays, for example, 'e' vs 'f'.
I tried in the following way (please look at the code), but I get a message that no data points are available in the line where my $gd = $graph->plot(\#e,\#f) or die $graph->error; command is being executed.
How to make it work?
MyFunc(
'a' => [0, 1,2,3,4],
'c' => [0, -1,2,-3,6],
'c' => [0, 2,4,2,5],
'd' => [0, 1,2,3,4],
'e' => [0, 9,2,1,7],
'f' => [-2, 5,-1,1,7],
'g' => [5, 1,8,-2,5],
);
sub MyFunc {
use GD::Graph::lines;
my $graph = GD::Graph::lines->new;
$graph->set(
x_label => 'X Label',
y_label => 'Y label',
title => 'Some simple graph',
y_max_value => 8,
y_tick_number => 8,
y_label_skip => 2
) or die $graph->error;
my $gd = $graph->plot(\#e,\#f) or die $graph->error;
open(IMG, '>file.gif') or die $!;
binmode IMG;
print IMG $gd->gif;
close IMG;
};
Passing an argument 'e' => [0,9,2,1,7] to a subroutine does not automatically create a variable called #e inside the subroutine. Your subroutine does do not do any processing of arguments whatsoever. Consider something like this to do what you seem to want:
sub MyFunc {
my %params = #_;
...
my $gd = $graph->plot( [$params{"e"}, $params{"f"}] ) ...
...
}

perl: deep merge with per-element arrays merge

I'm trying to merge two hashes and Hash::Merge does almost exactly what I need, except for arrays. Instead of concatenating arrays I need it to do per-element merge.
For example:
use Hash::Merge qw (merge);
my %a = ( 'arr' => [ { 'a' => 'b' } ] );
my %b = ( 'arr' => [ { 'c' => 'd' } ] );
my %c = %{ merge( \%a, \%b) };
Desired result is ('arr'=>[{'a'=>'b','c'=>'d'}]), actual result is ('arr'=>[{'a'=>'b'},{'c'=>'d'}])
Can this be done by using specify_behavior or is there some other way?
I think that specify_behaviour is used to specify how to handle conflicts, or uneven structures to merge. The documentation doesn't actually say much. But try it, go through defined shortcuts, or try to set them yourself. For your data structure you could try
SCALAR => ARRAY => sub { [ %{$_0}, %{$_[0]} ] }
SCALAR => ARRAY => HASH => sub { [ $_[0], $_[0] ] }
If you tried and it didn't work you may have found a bug in the module? By what you show it just didn't go "deep" enough. Here it is without the module. I've enlarged your sample structures.
use warnings;
use strict;
my %a = (
'arr1' => [ { a => 'A', a1 => 'A1' } ],
'arr2' => [ { aa => 'AA', aa1 => 'AA1' } ]
);
my %b = (
'arr1' => [ { b => 'B', b1 => 'B1' } ],
'arr2' => [ { bb => 'BB', bb1 => 'BB1' } ]
);
# Copy top level, %a to our target %c
my %c;
#c{keys %a} = values %a;
# Iterate over hash keys, then through array
foreach my $key (sort keys %c) {
my $arr_len = #{$c{$key}};
foreach my $i (0..$arr_len-1) {
my %hb = %{ ${$b{$key}}[$i] };
# merge: add %b to %c
#{ ${$c{$key}}[$i] }{keys %hb} = values %hb;
}
}
# Print it out
foreach my $key (sort keys %c) {
print "$key: ";
my $arr_len = #{$c{$key}};
foreach my $i (0..$arr_len-1) {
my %hc = %{ ${$c{$key}}[$i] };
print "$_ => $hc{$_}, " for sort keys %hc;
}
print "\n";
}
This prints the contents of %c (aligned manually here)
arr1: a => A, a1 => A1, b => B, b1 => B1,
arr2: aa => AA, aa1 => AA1, bb => BB, bb1 => BB1,
Code does not handle arrays/hashes of unequal sizes but checks can be added readily.
Another solution (that handles uneven hash elements in %a and %b).
my %c;
foreach my $key (keys %a, keys %b) {
my $a_ref = $a{$key};
my $b_ref = $b{$key};
$c{$key} = { map %$_, #$a_ref, #$b_ref };
}
use Data::Dumper;
print Dumper \%c;

How do I store an array as a value in a Perl hash?

I'm trying to create a hash in Perl, whose values are arrays. Something like:
my #array = split(/;/, '1;2');
my $hash = {'a' => #array};
Surprisingly, this reports (using Data::Dumper):
$VAR1 = {
'a' => '1',
'2' => undef
};
This page gives an example of storing an array in a hash by defining the array use square brackets, like:
my $hash = {'a' => ['1', '2']};
That works, but I'm getting my array from a call to the split method. What's magic about square brackets versus parentheses for defining an array, and how can I turn a "parentheses-array" into a 'square-brackets' array?
The values of hash (and array) elements are scalars, so you can't store an array into a hash.
The following are all equivalent:
my $hash = { a => #array };
my $hash = { 'a', #array };
my $hash = { 'a', $array[0], $array[1] };
my $hash = { a => $array[0], $array[1] => () };
A common solution is to store a reference to the array.
my #array = split(/;/, '1;2');
my $hash = { a => \#array }; # my $hash = { a => [ '1', '2' ] };
[ LIST ] similarly create an array, assigns LIST to it, then returns a reference to the array.

Hashes array elements copy

My array of hashes:
#cur = [
{
'A' => '9872',
'B' => '1111'
},
{
'A' => '9871',
'B' => '1111'
}
];
Expected result:
#curnew = ('9872', '9871');
Any simple way to get only the values of the first hash element from
this and assign it to an array?
Mind that hashes are unordered, so I take the word first to mean lexicographically first.
map { # iterate over the list of hashrefs
$_->{ # access the value of the hashref
(sort keys $_)[0] # … whose key is the first one when sorted
}
}
#{ # deref the arrayref into a list of hashrefs
$cur[0] # first/only arrayref (???)
}
The expression returns qw(9872 9871).
Assigning an arrayref to an array as in #cur = […] is probably a mistake, but I took it at face value.
Bonus perl5i solution:
use perl5i::2;
$cur[0]->map(sub {
$_->{ $_->keys->sort->at(0) }
})->flatten;
The expression returns the same values as above. This code is a bit longer, but IMO more readable because the flow of execution goes strictly from top to bottom, from left to right.
First your array have to be defined as
my #cur = (
{
'A' => '9872',
'B' => '1111'
},
{
'A' => '9871',
'B' => '1111'
}
);
Note the parenthesis
#!/usr/bin/perl
use strict;
use warnings;
use Data::Dump qw(dump);
my #cur = (
{
'A' => '9872',
'B' => '1111'
},
{
'A' => '9871',
'B' => '1111'
}
);
my #new;
foreach(#cur){
push #new, $_->{A};
}
dump #new;
use Data::Dumper;
my #hashes = map (#{$_}, map ($_, $cur[0]));
my #result = map ($_->{'A'} , #hashes);
print Dumper \#result;

Resources