hash with array of hashes in perl - arrays

I know this topic has been covered but other posts usually has static hashes and arrays and the don't show how to load the hashes and arrays.
I am trying to process a music library. I have a hash with album name and an array of hashes that contain track no, song title and artist. This is loaded from an XML file generated by iTunes.
the pared down code follows:
use strict;
use warnings;
use utf8;
use feature 'unicode_strings';
use feature qw( say );
use XML::LibXML qw( );
use URI::Escape;
my $source = "Library.xml";
binmode STDOUT, ":utf8";
# load the xml doc
my $doc = XML::LibXML->load_xml( location => $source )
or warn $! ? "Error loading XML file: $source $!"
: "Exit status $?";
my %hCompilations;
my %track;
# extract xml fields
my #album_nodes = $doc->findnodes('/plist/dict/dict/dict');
for my $album_idx (0..$#album_nodes) {
my $album_node = $album_nodes[$album_idx];
my $trackName = $album_node->findvalue('key[text()="Name"]/following-sibling::*[position()=1]');
my $artist = $album_node->findvalue('key[text()="Artist"]/following-sibling::*[position()=1]');
my $album = $album_node->findvalue('key[text()="Album"]/following-sibling::*[position()=1]');
my $compilation = $album_node->exists('key[text()="Compilation"]');
# I only want compilations
if( ! $compilation ) { next; }
%track = (
trackName => $trackName,
trackArtist => $artist,
);
push #{$hCompilations{$album}} , %track;
}
#loop through each album access the album name field and get what should be the array of tracks
foreach my $albumName ( sort keys %hCompilations ) {
print "$albumName\n";
my #trackRecs = #{$hCompilations{$albumName}};
# how do I loop through the trackrecs?
}

This line isn't doing what you think it is:
push #{$hCompilations{$album}} , %track;
This will unwrap your hash into a list of key/value pairs and will push each of those individually onto your array. What you want is to push a reference to your hash onto the array.
You could do that by creating a new copy of the hash:
push #{$hCompilations{$album}} , { %track };
But that takes an unnecessary copy of the hash - which will have an effect on your program's performance. A better idea is to move the declaration of that variable (my %track) inside the loop (so you get a new variable each time round the loop) and then just push a reference to the hash onto your array.
push #{$hCompilations{$album}} , \%track;
You already have the code to get the array of tracks, so iterating across that array is simple.
my #trackRecs = #{$hCompilations{$albumName}};
foreach my $track (#trackRecs) {
print "$track->{trackName}/$track->{trackArtist}\n";
}
Note that you don't need the intermediate array:
foreach my $track (#{$hCompilations{$albumName}}) {
print "$track->{trackName}/$track->{trackArtist}\n";
}

first of all you want to push the hash as a single element, so instead of
push #{$hCompilations{$album}} , %track;
use
push #{$hCompilations{$album}} , {%track};
in the loop you can access the tracks with:
foreach my $albumName ( sort keys %hCompilations ) {
print "$albumName\n";
my #trackRecs = #{$hCompilations{$albumName}};
# how do I loop through the trackrecs?
foreach my $track (#trackRecs) {
print $track->{trackName} . "/" . $track->{trackArtist} . "\n";
}
}

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?

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'],
);

Accessing returned values as an array

I have simple XML that I want to read in Perl and make hash containing all read keys.
Consider this code:
my $content = $xml->XMLin("filesmap.xml")->{Item};
my %files = map { $_->{Path} => 1 } #$content;
This snippet works great when XML file contains many Item tags. Then $content is a reference to array. But when there is only one Item I get an error when dereferencing as array. My assumption is that $content is a reference to the scalar, not array.
What is the practice to make sure I get array of values read from XML?
What you need is to not use XML::Simple and then it's really trivial. My favourite for fairly straightforward XML samples is XML::Twig
use XML::Twig;
my $twig = XML::Twig -> new -> parsefile ( 'filesmap.xml' );
my #files = map { $_ -> trimmed_text } $twig -> get_xpath ( '//Path' );
With a more detailed XML sample (and desired result) I'll be able to give you a better answer.
Part of the problem with XML::Simple is it tries to turn an XML data structure into perl data structures, and - because hashes are key-values and unordered but arrays are ordered, it has to guess. And sometimes it does it wrong, and other times it's inconsistent.
If you want it to be consistent, you can set:
my $xml = XMLin( "filesmap.xml", ForceArray => 1, KeyAttr => [], ForceContent => 1 );
But really - XML::Simple is just a route to pain. Don't use it. If you don't like XML::Twig, try XML::LibXML instead.
What I would say you need is a flatten-ing step.
my %files
= map { $_->{Path} => 1 }
# flatten...
map { ref() eq 'ARRAY' ? #$_ : $_ }
$xml->XMLin("filesmap.xml")->{Item}
;
You can do a check and force the return into an array reference if necessary:
my $content = $xml->XMLin("filesmap.xml")->{Item};
$content = ref $content eq 'ARRAY'
? $content
: [$content];

Hash in array in a hash

I'm trying to identify the output of Data::Dumper, it produces the output below when used on a hash in some code I'm trying to modify:
print Dumper(\%unholy_horror);
$VAR1 = {
'stream_details' => [
{
'file_path' => '../../../../tools/test_data/',
'test_file' => 'test_file_name'
}
]
};
Is this a hash inside an array inside a hash? If not what is it? and what is the syntax to access the "file path" and "test_file" keys, and their values.
I want to iterate over that inner hash like below, how would I do that?
while ( ($key, $value) = each %hash )
{
print "key: $key, value: $hash{$key}\n";
}
You're correct. It's a hash in an array in a hash.
my %top;
$top{'stream_details'}[0]{'file_path'} = '../../../../tools/test_data/';
$top{'stream_details'}[0]{'test_file'} = 'test_file_name';
print Dumper \%top;
You can access the elements as above, or iterate with 3 levels of for loop - assuming you want to iterate the whole thing.
foreach my $topkey ( keys %top ) {
print "$topkey\n";
foreach my $element ( #{$top{$topkey}} ) {
foreach my $subkey ( keys %$element ) {
print "$subkey = ",$element->{$subkey},"\n";
}
}
}
I would add - sometimes you get some quite odd seeming hash topologies as a result of parsing XML or JSON. It may be worth looking to see if that's what's happening, because 'working' with the parsed object might be easier.
The above might be the result of:
#JSON
{"stream_details":[{"file_path":"../../../../tools/test_data/","test_file":"test_file_name"}]}
Or something similar from an API. (I think it's unlikely to be XML, since XML doesn't implicitly have 'arrays' in the way JSON does).

How to create objects out of each element in array?

I have a module with a new constructor:
package myClass;
sub new
{
my $class = shift;
my $arrayreference = shift;
bless $arrayreference, $class;
return $arrayreference;
};
I want to do something like:
foreach $ref (#arrayref)
{
$array1 = myClass->new($ref);
}
$array1 is being rewritten each time, but I want each element in the array to have a distinct object name (ex. $array1, $array2, $array3 etc.)
If you are working with a plural data structure (an array), then you need to store the result into a plural container (or multiple scalar containers). The idomatic way to do this is to use the map function:
my #object_array = map {myClass->new($_)} #source_array;
If you know that #source_array contains a fixed number of items, and you want scalars for each object:
my ($foo, $bar, $baz) = map {myClass->new($_)} #source_with_3_items;
I think you should use some hash or array to contain the objects.
foreach $ref (#arrayref)
{
push #array, myClass->new($ref);
$hash{$key++} = myClass->new($ref);
}
thus you can access them with $array[42] or $hash{42}.
There is essentially no name difference between $array[1] and $array1. There is a programmatic difference in that $array[1] can be "pieced together" and, under modern Perl environments $array1 can't. Thus I can write $array[$x] for any valid $x and get an item with a "virtual name" of $array.$x.
my #objects = map { MyClass->new( $_ ); } #data_array;
Thus, if you just want to append a number, you probably just want to collect your objects in an array. However, if you want a more complex naming scheme, one or more levels of hashes is probably a good way to go.
If you had a way to derive the name from the object data once formed, and had a method called name, you could do this:
my %object_map
= map { my $o = MyClass->new( $_ ); ( $o->name => $o ); } #data_array
;
Are you are trying to do it in place?
my #objects = (
{ ...args for 1st object... },
{ ...args for 2nd object... },
...
);
$_ = Class->new($_) for #objects;
However, you should avoid reusing variables like that.
my #object_data = (
{ ...args for 1st object... },
{ ...args for 2nd object... },
...
);
my #objects = map Class->new($_), #object_data;
I agree with Ade YU and Eric Strom, and have +1'd their answers: you should use one of their approaches. But what you ask is technically possible, using symbolic references, so for completeness' sake:
foreach my $i (0 .. $#arrayref)
{
no strict refs;
my $varname = 'array' . ($i + 1);
${$varname} = myClass->new($arrayref[$i]);
}

Resources