perlguyjoe has asked for the wisdom of the Perl Monks concerning the following question:

Hello Monks, thank you in advance for your insight into my problem. I am trying to make a simple script that reads in a list of users data and populates a hash for each row, inside a main hash. So working with Hash of Hashes. I have it printing but its only printing the last row. Seems I am overwriting my hash, instead of adding a new hash. Can you please show me what I am doing wrong? The list format is like this:
ID=1 First=John Last=Doe AGE=42 ID=2 First=Jane Last=Doe AGE=35 ID=3 First=Jack Last=Doe AGE=17 ID=4 First=Jill Last=Doe AGE=15
Here is the script:
#!/usr/bin/perl use Data::Dumper; use strict; use warnings; getPeople(); sub getPeople { my ( $id, $first, $last, $age ); my $file = 'list.txt'; my $people; my $cntr; open( LIST, "< $file" ) or die "Can't open $file : $!"; #my @lines = split /\n/, $str; #foreach my $line (@lines) { while (my $row = <LIST> ) { $cntr++; my ($id, $first, $last, $age ) = split( /\s/, $row ); $id = (split( /=/, $id ))[1]; $first = (split( /=/, $first ))[1]; $last = (split( /=/, $last ))[1]; $age = (split( /=/, $age ))[1]; $people = { "$cntr" => { 'id' => "$id", 'first' => "$first", 'last' => "$last", 'age' => "$age" } }; } print Dumper $people; close LIST; }
Here is what I am getting as output:
$VAR1 = { '4' => { 'first' => 'Jill', 'last' => 'Doe', 'id' => '4', 'age' => '15' } };
I would expect to get something like this:
$VAR1 = { '4' => { 'first' => 'Jill', 'last' => 'Doe', 'id' => '4', 'age' => '15' } '3' => { 'first' => 'Jack', 'last' => 'Doe', 'id' => '3', 'age' => '17' } '2' => { 'first' => 'Jane', 'last' => 'Doe', 'id' => '2', 'age' => '35' } '1' => { 'first' => 'John', 'last' => 'Doe', 'id' => '1', 'age' => '42' } };

Replies are listed 'Best First'.
Re: Having an Issue updating hash of hashes
by NetWallah (Canon) on Jul 05, 2014 at 15:55 UTC
    $people = { "$cntr" => { ..
    That overwrites the contents of $people in each loop. Try:
    $people -> { $cntr } = { whatever...};
    Also, if $cntr is always a unique integer, an array-ref may be a better choice, to keep the information in sequence:
    $people ->[ $cntr] = { whatever .. };

            What is the sound of Perl? Is it not the sound of a wall that people have stopped banging their heads against?
                  -Larry Wall, 1992

      Thank you. That worked. For clarity here is the final code:
      #!/usr/bin/perl use Data::Dumper; use strict; use warnings; getPeople(); sub getPeople { my ( $id, $first, $last, $age ); my $file = 'list.txt'; my $people; my $cntr; open( LIST, "< $file" ) or die "Can't open $file : $!"; #my @lines = split /\n/, $str; #foreach my $line (@lines) { while (my $row = <LIST> ) { $cntr++; my ($id, $first, $last, $age ) = split( /\s/, $row ); $id = (split( /=/, $id ))[1]; $first = (split( /=/, $first ))[1]; $last = (split( /=/, $last ))[1]; $age = (split( /=/, $age ))[1]; $people -> { "$id" } = { 'id' => "$id", 'first' => "$first", 'last' => "$last", 'age' => "$age" }; } print Dumper($people); print "The person with ID 3 is $people->{'3'}{'first'} $people->{'3'}{ +'last'}\n"; close LIST; }
Re: Having an Issue updating hash of hashes
by 2teez (Vicar) on Jul 05, 2014 at 17:12 UTC
    hi perlguyjoe,
    Seems I am overwriting my hash, instead of adding a new hash

    Am afraid to say you are right. Something like this could show some light, I suppose!
    use warnings; use strict; use Data::Dumper; my %person; my $key; while (<DATA>) { for ( split /\s+/, $_ ) { if (/^(ID)=(.)/) { $key = $2; $person{$key} = { lc $1 => $2 }; } else { my ( $new_key, $value ) = split /=/, $_; $person{$key}{ lc $new_key } = $value; } } } print Dumper \%person; __DATA__ ID=1 First=John Last=Doe AGE=42 ID=2 First=Jane Last=Doe AGE=35 ID=3 First=Jack Last=Doe AGE=17 ID=4 First=Jill Last=Doe AGE=15
    And if I may add see this also perldsc

    Update
    In fact, you can remove all the drama of if/else within the for loop and do:
    $key = $1 if /^ID=(.)/; my ( $new_key, $value ) = split /=/, $_; $person{$key}{ lc $new_key } = $value;
    If you tell me, I'll forget.
    If you show me, I'll remember.
    if you involve me, I'll understand.
    --- Author unknown to me

      This bit will stop working once you start getting multi-digit IDs:

      $key = $1 if /^ID=(.)/;

      You'll want this instead:

      $key = $1 if /^ID=([^\s]+)/;

      Otherwise you'll end up silently overwriting previous entries.

        This bit will stop working once you start getting multi-digit IDs:

        $key = $1 if /^ID=(.)/;
        Obviously, BUT the OP data set doesn't say otherwise and I don't intend to think for the OP!
        Am only using the OP dataset provided with that the line suffices!

        Update:

        You'll want this instead:

        $key = $1 if /^ID=([^\s]+)/;
        Otherwise you'll end up silently overwriting previous entries.

        And what happens if the OP has two or ID with the same value, even with the code above?! Oops!!
        Maybe the OP first intention of using a counter will do for all intent and purpose!

        If you tell me, I'll forget.
        If you show me, I'll remember.
        if you involve me, I'll understand.
        --- Author unknown to me
Re: Having an Issue updating hash of hashes
by Laurent_R (Canon) on Jul 05, 2014 at 17:33 UTC
    Given that $cntr is just a counter that is incremented for each data element of your list, you should probably use an array of hashes, rather than a reference to an hash of hashes. For example you could change your main procedure as follows (untested):
    sub getPeople { my ( $id, $first, $last, $age ); my $file = 'list.txt'; my @people; # using directly an array # my $cntr; -- now useless open( LIST, "< $file" ) or die "Can't open $file : $!"; while (my $row = <LIST> ) { # $cntr++; -- now no longer useful my ($id, $first, $last, $age ) = split( /\s/, $row ); $id = (split( /=/, $id ))[1]; $first = (split( /=/, $first ))[1]; $last = (split( /=/, $last ))[1]; $age = (split( /=/, $age ))[1]; push @people, { 'id' => "$id", 'first' => "$first", 'last' => "$last", 'age' => "$age" }; } }
    I think that getting the individual values to be stored in the hashes could be made significantly simpler, but that's not what you asked for, I don't want to get off-topic at this point. If you're interested, other monks and myself can of course help you on that.
      I would love a way to neaten up the splits to get the proper data for the fields! I did it this way because it works, but I definitely am not getting brownie points for beauty.
        OK, one possible way:
        while (my $row = <LIST> ) { my ($id, $first, $last, $age) = (split /[\s=]/, $row)[1, 3, +5, 7]; push @people, { 'id' => "$id", 'first' => "$first", 'last' => "$last", 'age' => "$age" }; }
        It could be done in an even shorter way (one single instruction), but I do not think this would be a good idea, because it would become somewhat more difficult to understand and to maintain. Whereas I think the above remains fairly clear and quite easy to understand and to maintain. Using a regex could also do the job, but I doubt it could be clearer or simpler than the above.

        Contrary to Laurent_R's aversion to using a single regex to extract data fields from a record expressed herein, I find it's often both more robust and more maintainable.

        The trick is to combine record validation and record field extraction in one operation. Of course, in the words of the famous witticism, now you have two problems: coming up with a regex to match an entire data record may not be easy (and robustly matching, e.g., a name, even if the nationality domain is well defined, can be quite tricky, so you often end up with a hack like  \S+ as a 'temporary' expedient), but once defined, the regex, properly factored, can be quite clear and fairly easy to maintain.

        The example below takes liberties with names, those tricky devils, and otherwise assumes much about the OPed dataset, but shows the basic idea.

        c:\@Work\Perl>perl -wMstrict -le "my $record = do { my $id = qr{ \d+ }xms; my $name = qr{ [[:upper:]] [[:lower:]]+ }xms; my $first = $name; my $last = $name; my $age = qr{ \d+ }xms; qr{ \A ID= ($id) \s+ First= ($first) \s+ Last= ($last) \s+ AGE= ($age) \z }xms; }; ;; for my $rec ('ID=1 First=John Last=Doe AGE=42', @ARGV) { my ($id, $first_name, $last_name, $age) = $rec =~ m{ $record }xms or die qq{malformed record: '$rec'}; print qq{id '$id' first '$first_name' last '$last_name' age '$ag +e'}; } " "ID=2 First=Joe Last=42 AGE=Doe" id '1' first 'John' last 'Doe' age '42' malformed record: 'ID=2 First=Joe Last=42 AGE=Doe' at -e line 1.