How can I organize data in rows and columns with Perl? - arrays

The base problem is that I have lots of datapoints with normalized names that are just dumped from the server into a file, but I need to organize these datapoints into a file with rows and columns automatically, according to the data they contain (indicated in their normalized names).
The original file with all the datapoints comes as follows (these are not the original datapoint tags but rather simplified ones):
temp_r301
airflow_r301
temp_r345
airflow_r345
solar_w
solar_e
...
As you can see, they all come as one column, so there is one tag per row.
And I want to organize them so that for each state ("temp" as in temperature), I have the corresponding information in the same row, such as:
temp_r301 301 airflow_r301 solar_w solar_e #airflow in 301 and general solar radiation affect temperature (state) in room 301
temp_r345 345 airflow_r345 solar_w solar_e #airflow in 345 and general solar radiation affect temperature (state) in room 345
Of course the lenght of the array can vary so the idea is to make an algorithm that detects the length and organizes the data accordingly. Also, I am aware I will have to use regular expressions to find the matches and define which datapoints are states and which ones inputs, as well as knowing the room to which they belong.
So far I have tried the following:
use strict;
use warnings;
use diagnostics;
my #transpose = ();
my #sorted = ();
push(#sorted, [qw(temp_r301 temp_r345)]);
push(#sorted, [qw(301 345)]);
push(#sorted, [qw(airflow_r301 airflow_r345 solar_w solar_e)]);
for my $sorted (#sorted) {
for my $column (0 .. $#sorted) {
push(#{$transpose[$column]}, $sorted->[$column]);
}
}
for my $new_row (#transpose) {
for my $new_col (#{$new_row}) {
print "$new_col ";
}
print "\n";
}
But this only works fine if all the arrays have the same lenght (not this case).
I also discovered a loop that can be used to store data into matrix form (array of arrays), but still, I can't seem to find a solution to write in the matrix the data from different arrays:
use strict;
use warnings;
use diagnostics;
use feature 'say';
my #states = qw(temp_r301 temp_r345);
my #zones = qw(301 345);
my #inputs = qw(airflow_r301 airflow_r345 solar_w solar_e);
my #matrix = ();
for my $x (0 .. $#states) {
for my $y (0 .. $#inputs) {
$matrix[$x][$y] = $states[$x]; #of course this only copies the states array and
} #repeats it for each created array
}
for my $aref (#matrix) { #print array of arrays
say "[ #$aref ],";
}
So, knowing that I have all the data dumped into an input file, what would be the best way to sort that data into a matrix? Is there any loop I should give more attention to? Should I be working with arrays?

Details of this problem are still unclear, while explanations did help. So here is what I'll assume.
I take data to have a piece of information per line. Some contain a tag (description) followed by the room number, and I assume format tag_rN, identifying a room number that the tag applies to.
As for others, that don't have the room number, additional processing is needed to decide where that information belongs. The question puts forth only an example of tags that apply to all rooms, related to solar radiation that affects them (see comments), so that's all that's processed.
The fact that some of the data does not neatly classify with a room is what makes organization of the parsed data non-trivial. Since no details are given I merely split it into two hashes, one by room number and another one which structure will depend on specifics.
use warnings;
use strict;
use feature 'say';
use Data::Dump qw(dd);
my $file = shift // die "Usage: $0 file\n";
open my $fh, '<', $file or die "Can't open $file: $!";
my (%room, %other);
while (<$fh>) {
chomp;
if ( my ($tag, $room_num) = /([^_]+)_r([0-9]+)/ ) {
$room{$room_num}{$tag} = $_; # have room number
}
else { # more processing needed
my ($tag, $value) = parse_line($_);
push #{ $other{$tag} }, $value;
}
}
dd \%room; dd \%other; say '';
# Print in CSV format. Header first
my #tags = ( keys %{ $room{ (keys %room)[0] } }, keys %other );
say join ',', 'room', #tags;
foreach my $rnum (keys %room) {
say join ',',
$rnum, map { $room{$rnum}{$_} // join ' ', #{$other{$_}} } #tags;
}
sub parse_line {
my ($line) = #_;
my ($tag, $value);
if ($line =~ /solar_w|solar_e/) { # example from sample data
$tag = 'solar';
$value = $line;
}
else { } # other possibilities
return $tag, $value;
}
The data with the room number is sorted out by the identifying description ("tag") as a key, with the line being its value. Each such key-value pair is in a hashref assigned to each room number.
The data without the room number is parsed in a separate sub, with just some token code since no details are given. Then that is stored in another hash, for easier manipulation (since it's not tied to any one room).
How tags are extracted from data is a bit arbitrary, since it's not specified in the question.
All this is combined into a CSV format. The above, with the input file from the question and the explanation in comments that the solar radiation from both west and east affects all rooms, prints:
{
301 => { airflow => "airflow_r301", temp => "temp_r301" },
345 => { airflow => "airflow_r345", temp => "temp_r345" },
}
{ solar => ["solar_w", "solar_e"] }
room,airflow,temp,solar
345,airflow_r345,temp_r345,solar_w solar_e
301,airflow_r301,temp_r301,solar_w solar_e
Comment out the line with dd ... (from Data::Dump) to remove the initial diagnostic prints. Then the last few lines are the CSV that would go into some file etc.
Some data may be missing for some rooms, and there is yet more data which may not classify so uniformly. Then the fields for those headers will be merrily empty in some rows, as desired.

Related

Perl ... create horizontal children of a %hash using #array items

I've been banging my head on this awhile and searched many ways. I'm sure this is going to boil down to being really basic.
I have data in an #array that I want to move to a tree in a %hash.
This might be something more appropriate to JSON? But I haven't delved into it before and I don't need to save out/restore this information.
Desire:
Create a dependent tree of USB devices that can nest under each other that can track the end point (deviceC) through a hub (deviceB) and finally the root (deviceA).
Example:
Simplified (I hope ... this isn't from the actual longer script):
I want to convert an array in this format:
my #array = ['deviceA','deviceB','deviceC'];
to multidimensional hashes equal to:
my %hash = ('deviceA' => { 'deviceB' => { 'deviceC' => '' } } )
that would dump like:
$VAR1 = {
'deviceA' => {
'deviceB' => {
'deviceC' => ''
}
}
};
For just looking at a single device this isn't necessary, but I'm building out an IOMMU -> PCI Device -> USB map that contains many devices.
NOTES:
I'm trying to avoid installing CPAN modules so the script is to similar systems (Proxmox VE)
The last device (deviceC above) has no children
value '' is fine
undef would probably work
mixing the types would work but I need to know how to set that
I will never need to modify or manipulate the hash once created
I don't know the right way to recurse the #array to populate the %hash children. * I want the data horizontal for each USB device
I'd switch to an Object/package but each device can have a different set of children (or none) making it infeasible to know Object names
Some USB devices have no children (root hubs) ... similar to %hash = ('deviceA' => '')
Some have 1 child that is the final device ... similar to %hash = ('deviceA' => { 'deviceB' =>'' } )
Some have multiple steps between the root via additional hub(s) ... similar to %hash = ('deviceA' => { 'deviceB' => { 'deviceC' => '' } } ) or more
Starting point :
This is basic and incomplete but will run:
#!/usr/bin/perl
use strict;
use warnings;
use Data::Dumper qw(Dumper);
# data in from parsing usb device path:
my #array = ['deviceA','deviceB','deviceC'];
# needs to be converted to:
my %hash = ('deviceA' => { 'deviceB' => { 'deviceC' => '' } } );
print "\n\%hash:\n" . Dumper \%hash;
Pseudo-code
This section is NOT working code in any form. I'm just trying to make a note of what I'm thinking. I know the format is wrong, I've tried multiple ways to create this and I'd look even dumber showing all of my attempts :)
I'm very new to refs and I'm not going to try and get that right here. The idea below is:
For each item in #array:
Create a way (either a ref or a copy of the current hash) that can be used next iteration to place the next child
Attach item as a child of the previous iteration with an empty value (that can be appended if there is further iteration)
my #array = ['deviceA','deviceB','deviceC'];
my %hash = {};
my %trackref;
for (#array) {
%trackref = %hash; # a copy of the existing that won't change when %hash updates
$hash{last_child} ::append_child:: $_;
}
You're actually pretty close, but it seems that you need to understand references a bit better. perldoc perlref is probably a good starting point to understand references.
A few mistakes in your code, before looking at the solution:
my #array = [ ... ];: [] creates an arrayref, not an array, which means that #array actually stores a single scalar item: a reference to another array. Use () to initialize an array: my #array = ( ... );.
my %hash = {};: similarly, {} creates a hashref, not a hash. Which means that this lines stores a single hashref in %hash, which will cause this warning: Reference found where even-sized list expected at hash.pl line (because a hash contains keys-values and you only provided a key). Use () for a simple (ie, not a hashref) hash. In this case however, you don't need to initialize %hash: my %hash; and my %hash = () do the same thing (that is, create an empty hash).
%trackref = %hash; copies the content of %hash in %trackref. Which means that, contrary to what the name "trackref" implies, %trackref doesn't contain a reference to anything, but a copy of %hash. Use \%hash to create a reference to %hash.
Note that if you already have a hashref, then assigning it to another variables copies the reference. For instance, if you do my $hash1 = {}; my $hash2 = $hash1, then both $hash1 and $hash2 reference the same hash.
So, fixing those issues in your attempt, we get:
my #array = ('deviceA','deviceB','deviceC');
my %hash;
my $trackref = \%hash;
for my $usb (#array) {
$trackref->{$usb} = {};
$trackref = $trackref->{$usb};
}
print Dumper \%hash;
Which outputs:
$VAR1 = {
'deviceA' => {
'deviceB' => {
'deviceC' => {}
}
}
};
The main change that I did was to replace your $hash{last_child} ::append_child:: $_; by $trackref->{$_} = {};. But the idea remains the same: Attach item as a child of the previous iteration with an empty value to reuse your words.
To help you understand the code a bit better, let's see what happens in the loop step by step:
Before the first iteration, %hash is empty and $trackref references %hash.
In the first iteration, we put deviceA => {} in $trackref (or, more pedantically, we associate {} with the key deviceA in $trackref). Since $trackref references %hash, this puts deviceA => {} in %hash. Then, we store in $trackref this new {} that we just created, which means that $trackref now references $hash{deviceA}.
In the second iteration, we put deviceB => {} in $trackref. $trackeref references $hash{deviceA} (which we created in the previous iteration), which means that %hash is now (deviceA => { deviceB => {} }). We then store in $trackref the new {}.
And so on...
You'll note that in the innermost hash, {} is associated to the key deviceC. When iterating of the hash, you can thus know if you are at the end by doing something like if (%$hash) (instead of just if ($hash) if this last {} would have been undef or ''). Let me know if that's an issue: we can add a bit of code to convert this {} into undef (alternatively, you can do it yourself, it will be a good exercise to get used to references)
Minor remark: #array and %hash are poor array and hash names, because the # already indicates an array, and % already indicates a hash. It's possible that you used those names just for this small example for your question, in which case, no problem. However, if you use those names in your actual code, consider changing them for something more explicit... #usb_devices and %usb_devices_tree maybe?

Identifying elements in one array of hashes that are not in another array of hashes (perl)

I'm a novice perl programmer trying to identify which elements are in one array of hashes but not in another. I'm trying to search through the "new" array, identifying the id, title, and created elements that don't exist from the "old" array.
I believe I have it working with a set of basic for() loops, but I'd like to do it more efficiently. This only came after having tried to use grep() and failed.
These arrays are built from a database as such:
use DBI;
use strict;
use Data::Dumper;
use Array::Utils qw(:all);
sub db_connect_new();
sub db_disconnect_new($);
sub db_connect_old();
sub db_disconnect_old($);
my $dbh_old = db_connect_old();
my $dbh_new = db_connect_new();
# get complete list of articles on each host first (Joomla! system)
my $sql_old = "select id,title,created from mos_content;";
my $sql_new = "select id,title,created from xugc_content;";
my $sth_old = $dbh_old->prepare($sql_old);
my $sth_new = $dbh_new->prepare($sql_new);
$sth_old->execute();
$sth_new->execute();
my $ref_old;
my $ref_new;
while ($ref_old = $sth_old->fetchrow_hashref()) {
push #rv_old, $ref_old;
}
while ($ref_new = $sth_new->fetchrow_hashref()) {
push #rv_new, $ref_new;
}
my #seen = ();
my #notseen = ();
foreach my $i (#rv_old) {
my $id = $i->{id};
my $title = $i->{title};
my $created = $i->{created};
my $seen = 0;
foreach my $j (#rv_new) {
if ($i->{id} == $j->{id}) {
push #seen, $i;
$seen = 1;
}
}
if ($seen == 0) {
print "$i->{id},$i->{title},$i->{state},$i->{catid},$i->{created}\n";
push #notseen, $i;
}
}
The arrays look like this when using Dumper(#rv_old) to print them:
$VAR1 = {
'title' => 'Legal Notice',
'created' => '2004-10-07 00:17:45',
'id' => 14
};
$VAR2 = {
'created' => '2004-11-15 16:04:06',
'id' => 86096,
'title' => 'IRC'
};
$VAR3 = {
'id' => 16,
'created' => '2004-10-07 16:15:29',
'title' => 'About'
};
I tried to use grep() using array references, but I don't think I understand arrays, hashes, and references well enough to do it properly. My failed grep() attempts are below. I'd appreciate any ideas of how to do this properly.
I believe the problem with this is that I don't know how to reference the id field in the second array of hashes. Most of the examples using grep() that I've seen are to just look through an entire array, like you would with regular grep(1). I need to iterate through one array, checking each of the values from the id field with the id field from another array.
my $rv_old_ref = \#rv_old;
my $rv_new_ref = \#rv_new;
for my $i ( 0 .. $#rv_old) {
my $match = grep { $rv_new_ref->$_ == $rv_old_ref->$_ } #rv_new;
push #notseen, $match if !$match;
}
I also tried variations on the grep() above:
1) if (($p) = grep ($hash_ref->{id}, #rv_old)) {
2) if ($hash_ref->{id} ~~ #rv_old) {
There are a number of libraries that compare arrays. However, your comparison involves complex data structures (the arrays have hashrefs as elements) and this at least complicates use of all modules that I am aware of.
So here is a way to do it by hand. I use the shown array and its copy with one value changed.
use warnings;
use strict;
use feature 'say';
use List::Util qw(none); # in List::MoreUtils with older Perls
use Data::Dump qw(dd pp);
sub hr_eq {
my ($e1, $e2) = #_;
return 0 if scalar keys %$e1 != scalar keys %$e2;
foreach my $k1 (keys %$e1) {
return 0 if !exists($e2->{$k1}) or $e1->{$k1} ne $e2->{$k1};
}
return 1
}
my #a1 = (
{ 'title' => 'Legal Notice', 'created' => '2004-10-07 00:17:45', 'id' => 14 },
{ 'created' => '2004-11-15 16:04:06', 'id' => 86096, 'title' => 'IRC' },
{ 'id' => 16, 'created' => '2004-10-07 16:15:29', 'title' => 'About' }
);
my #a2 = (
{ 'title' => 'Legal Notice', 'created' => '2004-10-07 00:17:45', 'id' => 14 },
{ 'created' => '2004-11-15 16:xxx:06', 'id' => 86096, 'title' => 'IRC' },
{ 'id' => 16, 'created' => '2004-10-07 16:15:29', 'title' => 'About' }
);
my #only_in_two = grep {
my $e2 = $_;
none { hr_eq($e2, $_) } #a1;
} #a2;
dd \#only_in_two;
This correctly identifies the element in #a2 that doesn't exist in #a1 (with xxx in timestamp).
Notes
This finds what elements of one array are not in another, not the full difference between arrays. It is what the question specifically asks for.
The comparison relies on details of your data structure (hashref); there's no escaping that, unless you want to reach for more comprehensive libraries (like Test::More).
This uses string comparison, ne, even for numbers and timestamps. See whether it makes sense for your real data to use more appropriate comparisons for particular elements.
Searching through a whole list for each element of a list is an O(N*M) algorithm. Solutions of such (quadratic) complexity are usable as long as data isn't too big; however, once data gets big enough so that size increases have clear effects they break down rapidly (slow down to the point of being useless). Time it to get a feel for this in your case.
An O(N+M) approach exists here, utilizing hashes, shown in ikegami answer. This is much better algorithmically, once the data is large enough for it to show. However, as your array carries complex data structure (hashrefs) a bit of work is needed to come up with a working program, specially as we don't know data. But if your data is sizable then you surely want to implement this.
Some comments on filtering.
The question correctly observes that for each element of an array, as it's processed in grep, the whole other array need be checked.
This is done in the body of grep using none from List::Util. It returns true if the code in its block evaluates false for all elements of the list; thus, if "none" of the elements satisfy that code. This is the heart of the requirement: an element must not be found in the other array.
Care is needed with the default $_ variable, since it is used by both grep and none.
In grep's block $_ aliases the currently processed element of the list, as grep goes through them one by one; we save it into a named variable ($e2). Then none comes along and in its block "takes possession" of $_, assigning elements of #a1 to it as it processes them. The current element of #a2 is also available since we have copied it into $e2.
The test performed in none is pulled into a a subroutine, which I call hr_eq to emphasize that it is specifically for equality comparison of (elements in) hashrefs.
It is in this sub where the details can be tweaked. Firstly, instead of bluntly using ne for values for each key, you can add custom comparisons for particular keys (numbers must use ==, etc). Then, if your data structures change this is where you'd adjust specifics.
You could use grep.
for my $new_row (#new_rows) {
say "$new_row->{id} not in old"
if !grep { $_->{id} == $new_row->{id} } #old_rows;
}
for my $old_row (#old_rows) {
say "$old_row->{id} not in new"
if !grep { $_->{id} == $old_row->{id} } #new_rows;
}
But that's an O(N*M) solution, while there exists an O(N+M) solution that would be far faster.
my %old_keys; ++$old_keys{ $_->{id} } for #old_rows;
my %new_keys; ++$new_keys{ $_->{id} } for #new_rows;
for my $new_row (#new_rows) {
say "$new_row->{id} not in old"
if !$old_keys{$new_row->{id}};
}
for my $old_row (#old_rows) {
say "$old_row->{id} not in new"
if !$new_keys{$old_row->{id}};
}
If both of your database connections are to the same database, this can be done far more efficiently within the database itself.
Create a temporary table with three fields, id, old_count (DEFAULT 0) and new_count (DEFAULT 0).
INSERT OR UPDATE from the old table into the temporary table, incrementing old_count in the process.
INSERT OR UPDATE from the new table into the temporary table, incrementing new_count in the process.
SELECT the rows of the temporary table which have 0 for old_count or 0 for new_count.
select id,title,created from mos_content
LEFT JOIN xugc_content USING(id)
WHERE xugc_content.id IS NULL;
Gives you the rows that are in mos_content but not in xugc_content.
That's even shorter than the Perl code.

Merge Perl hashes into one array and loop through it

I'm creating a Perl plugin for cPanel which has to get all domains in the account of a user and display it in a HTML select field. Originally, I'm a PHP developer, so I'm having a hard time understanding some of the logic of Perl. I do know that cPanel plugins can also be written in PHP, but for this plugin I'm limited to Perl.
This is how I get the data from cPanel:
my #user_domains = $cpliveapi->uapi('DomainInfo', 'list_domains');
#user_domains = $user_domains[0]{cpanelresult}{result}{data};
This is what it looks like using print Dumper #user_domains:
$VAR1 = {
'addon_domains' => ['domain1.com', 'domain2.com', 'domain3.com'],
'parked_domains' => ['parked1.com', 'parked2.com', 'parked3.com'],
'main_domain' => 'main-domain.com',
'sub_domains' => ['sub1.main-domain.com', 'sub2.main-domain.com']
};
I want the data to look like this (thanks #simbabque):
#domains = qw(domain1.com domain2.com domain3.com main-domain.com parked1.com parked2.com parked3.com);
So, I want to exclude sub_domains and merge the others in 1 single-dimensional array so I can loop through them with a single loop. I've struggled the past few days with what sounds like an extremely simple task, but I just can't wrap my head around it.
You need something like this
If you find you have a copy of List::Util that doesn't include uniq then you can either upgrade the module or use this definition
sub uniq {
my %seen;
grep { not $seen{$_}++ } #_;
}
From your dump, the uapi call is returning a reference to a hash. That goes into $cp_response and then drilling down into the structure fetches the data hash reference into $data
delete removes the subdomain information from the hash.
The lists you want are the values of the hash to which $data refers, so I extract those. Those values are references to arrays of strings if there is more than one domain in the list, or simple strings if there is only one
The map converts all the domain names to a single list by dereferencing array references, or passing strings straight through. That is what the ref() ? #$_ : $_ is doing. FInally uniq removes multiple occurrences of the same name
use List::Util 'uniq';
my $cp_response = $cpliveapi->uapi('DomainInfo', 'list_domains');
my $data = $cp_response->{cpanelresult}{result}{data};
delete $data->{sub_domains};
my #domains = uniq map { ref() ? #$_ : $_ } values %$data;
output
parked1.com
parked2.com
parked3.com
domain1.com
domain2.com
domain3.com
main-domain.com
That isn't doing what you think it' doing. {} is the anonymous hash constructor, so you're making a 1 element array, with a hash in it.
You probably want:
use Data::Dumper;
my %user_domains = (
'addon_domains' => ['domain1.com', 'domain2.com', 'domain3.com'],
'parked_domains' => ['parked1.com', 'parked2.com', 'parked3.com'],
'main_domain' => 'main-domain.com',
'sub_domains' => ['sub1.main-domain.com', 'sub2.main-domain.com'],
);
print Dumper \%user_domains;
And at which point the 'other' array elements you can iterate through either a double loop:
foreach my $key ( keys %user_domains ) {
if ( not ref $user_domains{$key} ) {
print $user_domains{$key},"\n";
next;
}
foreach my $domain ( #{$user_domains{$key}} ) {
print $domain,"\n";
}
}
Or if you really want to 'flatten' your hash:
my #flatten = map { ref $_ : #$_ ? $_ } values %user_domains;
print Dumper \#flatten;
(You need the ref test, because without it, the non-array main-domain won't work properly)
So for the sake of consistency, you might be better off with:
my %user_domains = (
'addon_domains' => ['domain1.com', 'domain2.com', 'domain3.com'],
'parked_domains' => ['parked1.com', 'parked2.com', 'parked3.com'],
'main_domain' => ['main-domain.com'],
'sub_domains' => ['sub1.main-domain.com', 'sub2.main-domain.com'],
);

Looping through known elements in a hash of hashes of arrays

I have a question I am hoping someone could help with (simplified for the purposes of explaining my question).
I have the following hash of hashes of arrays (I think that is what it is anyway?)
Data structure
{
Cat => {
Height => ["Tiny"],
},
Dog => {
Colour => ["Black"],
Height => ["Tall"],
Weight => ["Fat", "Huge"],
},
Elephant => {
Colour => ["Grey"],
Height => ["Really Big"],
Weight => ["Fat", "Medium", "Thin"],
},
}
What I am trying to do
The program below will print the whole data structure. I want to use this kind of way to do it
my %h;
for my $animal (keys %h) {
print "$animal\n";
for my $attribute ( keys %{$h{$animal}} ) {
print "\t $attribute\n";
for my $i (0 .. $#{$h{$animal}{$attribute}} ) {
print "\t\t$h{$animal}{$attribute}[$i]\n";
}
}
}
The problem I am having
I am trying to access a particular part of the data structure. For example, I want to only print out the Height arrays for each animal as I do not care about the other Colour, Weight attributes in this example.
I'm sure there is a simple answer to this, and I know I need to specify the Height part, but what is the correct way of doing it? I have tried multiple ways that I thought would work without success.
In your code, instead of looping over all the attributes with
for my $attribute ( keys %{ $h{$animal} } ) { ... }
just use the one you are interested in. Like this
for my $animal (keys %h) {
print "$animal\n";
for my $attribute ( 'Height' ) {
print "\t $attribute\n";
for my $i (0 .. $#{$h{$animal}{$attribute}} ) {
print "\t$h{$animal}{$attribute}[$i]\n";
}
}
}
I would choose to loop over the contents of the heights array rather than the indices, making the code look like this:
for my $animal (keys %h) {
print "$animal\n";
print "\t\t$_\n" for #{ $h{$animal}{Height} };
}
Taking a quick look at your data structure: It's a hash of hashes of arrays! Wow. Mind officially blown.
Here's a quick way of printing out all of the data:
use feature qw(say);
# Working with a Hash of Hash of Arrays
for my $animal (keys %h) {
say "Animal: $animal";
# Dereference: Now I am talking about a hash of arrays
my %animal_attributes = %{ $h{$animal} };
for my $attribute (keys %animal_attributes) {
# Dereference: Now I am talking about just an array
my #attribute_value_list = #{ $animal_attributes{$attribute} };
say "\tAttribute: $attribute - " . join ", ", #attribute_value_list;
}
}
Note I use dereferencing. I don't have to do the dereference, but it makes the code a bit easier to work with. I don't have to think of my various levels. I know my animal is a hash of attributes, and those attributes are an array of attribute values. By using dereferencing, it allows me to keep things straight.
Now, let's say you want to print out only a list of desirable attributes. You can use the exists function to see if that attribute exists before trying to print it out.
use feature qw(say);
use constant DESIRED_ATTRIBUTES => qw(weight height sex_appeal);
# Working with a Hash of Hash of Arrays
for my $animal (keys %h) {
say "Animal: $animal";
# Dereference: Now I am talking about a hash of arrays
my %animal_attributes = %{ $h{$animal} };
for my $attribute ( DESIRED_ATTRIBUTES ) {
if ( exists $animal_attributes{$attribute} ) {
# Dereference: Now I am talking about just an array
my #attribute_value_list = #{ $animal_attributes{$attribute} };
say "\tAttribute: $attribute - " . join ", ", #attribute_value_list;
}
}
}
Same code, I just added an if clause.
When you get into these complex data structures, you might be better off using Object Oriented design. Perl has an excellent tutorial on OOP Perl. If you used that, you could have defined a class of animals and have various methods to pull out the data you want. It makes maintenance much easier and allows you to bravely create even more complex data structures without worrying about tracking where you are.
I think sometimes it's easier to use the value directly, if it is a reference to another structure. You could do something like:
my $height = "Height";
while (my ($animal, $attr) = each %h) {
print "$animal\n";
print "\t$height\n";
print "\t\t$_\n" for #{ $attr->{$height} };
}
Using the value of the main keys, you can skip over one step of references and go straight at the Height attribute. The output below is after the format you had in your original code.
Output:
Elephant
Height
Really Big
Cat
Height
Tiny
Dog
Height
Tall
Assuming your variable is called %h:
foreach my $animal (keys %h) {
my $heights = $h{$animal}->{Height}; #gets the Height array
print $animal, "\n";
foreach my $height( #$heights ) {
print " ", $height, "\n";
}
}
I think I have worked it out and found what I was doing wrong?
This is how I think it should be:
my %h;
for my $animal (keys %h) {
print "$animal\n";
for my $i (0 .. $#{$h{$animal}{Height}} ) {
print "\t\t$h{$animal}{Height}[$i]\n";
}
}

How do I consolidate a hash in Perl?

I have an array of hash references. The hashes contain 2 keys, USER and PAGES. The goal here is to go through the array of hash references and keep a running total of the pages that the user printed on a printer (this comes from the event logs). I pulled the data from an Excel spreadsheet and used regexes to pull the username and pages. There are 182 rows in the spreadsheet and each row contains a username and the number of pages they printed on that job. Currently the script can print each print job (all 182) with the username and the pages they printed but I want to consolidate this down so it will show: username 266 (i.e. just show the username once, and the total number of pages they printed for the whole spreadsheet.
Here is my attempt at going through the array of hash references, seeing if the user already exists and if so, += the number of pages for that user into a new array of hash references (a smaller one). If not, then add the user to the new hash ref array:
my $criteria = "USER";
my #sorted_users = sort { $a->{$criteria} cmp $b->{$criteria} } #user_array_of_hash_refs;
my #hash_ref_arr;
my $hash_ref = \#hash_ref_arr;
foreach my $index (#sorted_users)
{
my %hash = (USER=>"",PAGES=>"");
if(exists $index{$index->{USER}})
{
$hash{PAGES}+=$index->{PAGES};
}
else
{
$hash{USER}=$index->{USER};
$hash{PAGES}=$index->{PAGES};
}
push(#hash_ref_arr,{%hash});
}
But it gives me an error:
Global symbol "%index" requires explicit package name at ...
Maybe my logic isn't the best on this. Should I use arrays instead? It seems as though a hash is the best thing here, given the nature of my data. I just don't know how to go about slimming the array of hash refs down to just get a username and the total pages they printed (I know I seem redundant but I'm just trying to be clear). Thank you.
my %totals;
$totals{$_->{USER}} += $_->{PAGES} for #user_array_of_hash_refs;
And then, to get the data out:
print "$_ : $totals{$_}\n" for keys %totals;
You could sort by usage too:
print "$_ : $totals{$_}\n" for sort { $totals{$a} <=> $totals{$b} } keys %totals;
As mkb mentioned, the error is in the following line:
if(exists $index{$index->{USER}})
However, after reading your code, your logic is faulty. Simply correcting the syntax error will not provide your desired results.
I would recommend skipping the use of temporary hash within the loop. Just work with the a results hash directly.
For example:
#!/usr/bin/perl
use strict;
use warnings;
my #test_data = (
{ USER => "tom", PAGES => "5" },
{ USER => "mary", PAGES => "2" },
{ USER => "jane", PAGES => "3" },
{ USER => "tom", PAGES => "3" }
);
my $criteria = "USER";
my #sorted_users = sort { $a->{$criteria} cmp $b->{$criteria} } #test_data;
my %totals;
for my $index (#sorted_users) {
if (not exists $totals{$index->{USER}}) {
# initialize total for this user
$totals{$index->{USER}} = 0;
}
# add to user's running total
$totals{$index->{USER}} += $index->{PAGES}
}
print "$_: $totals{$_}\n" for keys %totals;
This produces the following output:
$ ./test.pl
jane: 3
tom: 8
mary: 2
The error comes from this line:
if(exists $index{$index->{USER}})
The $ sigil in Perl 5 with {} after the name means that you are getting a scalar value out of a hash. There is no hash declared by the name %index. I think that you probably just need to add a -> operator so the problem line becomes:
if(exists $index->{$index->{USER}})
but not having the data makes me unsure.
Also, good on you for using use strict or you would be instantiating the %index hash silently and wondering why your results didn't make any sense.
my %total;
for my $name_pages_pair (#sorted_users) {
$total{$name_pages_pair->{USER}} += $name_pages_pair->{PAGES};
}
for my $username (sort keys %total) {
printf "%20s %6u\n", $username, $total{$username};
}

Resources