Beefy Boxes and Bandwidth Generously Provided by pair Networks
Perl-Sensitive Sunglasses
 
PerlMonks  

Building an arbitrary-depth, multi-level hash

by Anonymous Monk
on Feb 28, 2009 at 21:52 UTC ( [id://747210]=perlquestion: print w/replies, xml ) Need Help??

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

O Monks! Cometh one bearing a great problem and seeking help... I've got a bunch of files that I'm reading with lines like this:
... Name=foo Icon=bar Categories=a;b;c ...
I read this in, parse it, and store it in a hash like so:
#!/usr/bin/perl -w use strict; chdir "/usr/share/applications"; my (%collect, $out); while (our $Fn = <*.desktop>){ chomp $Fn; open Fn or die "$Fn: $!\n"; while (<Fn>){ $collect{name} = "$1" if /^Name=(.*)$/; $collect{icon} = "$1" if /^Icon=(.*)$/; $collect{exec} = "$1" if /^Exec=(.*)$/; $collect{categories} = "$1" if /^Categories=(.*);?\s*$/; } $collect{categories} ||= "Misc"; $collect{icon} ||= "-"; for (qw/name exec categories/){ print "Bad value for '$Fn' ($_)\n" unless defined $collect{$_}; } my $val = qq#prog "$collect{name}" $collect{icon} $collect{exec}#; ### More code follows
This, however, is where things break down. What I want is to split the categories into a list, then create a hash that works like this:
# 'categories' entry is 'GNOME;Games;Action' push $out{GNOME}{Games}{Action}, $val; # 'GNOME;Games;Action;FPS' push $out{GNOME}{Games}{Action}{FPS}, $val; # ...and so on.
That's the part where I've been tearing out my hair: I've been trying to sensibly construct this thing, and... I'm completely stumped. I've been trying for hours non-stop. The most frustrating part is that I feel I should know how to do this: I've been working with Perl for years, and using hashes and refs, etc. - and my brain just refuses to give up the answer. What I have so far - this comes just below the first code chunk that I showed above - is this:
my @levels = split /;/, $collect{categories}; for (0 .. $#levels) { $out->{$levels[$_]} = {} unless exists $out->{$levels[$_]}; $out->{$levels[$_]} = $val if $_ == $#levels; } close Fn; } use Data::Dumper; print Dumper($out);
...which, of course, does not work. Please HELP! Thanks in advance.

Replies are listed 'Best First'.
Re: Building an arbitrary-depth, multi-level hash
by GrandFather (Saint) on Feb 28, 2009 at 22:16 UTC

    Rather than describing a solution that you can't find, how about describing the problem you are trying to solve. Not the little piece you are having trouble with, but the whole thing.

    You seem to be scanning files and extracting some information from them, but you only give us a portion of a single file as sample data and don't show us enough to demonstrate the problem you are having.

    You may find it helps to create a stand alone script to demonstrate the problem. The following starting point may help:

    use strict; use warnings; use Data::Dump::Streamer; my %files; $files{wibble} = <<'END_FILE'; Name=foo Icon=bar Categories=a;b;c END_FILE $files{floop} = <<'END_FILE'; Name=baz Icon=boo Categories=x;y;z END_FILE my %collection; for my $file (keys %files) { open my $inFile, '<', \$files{$file} or die "Can't open $file: $!" +; my %collect; while (my $line = <$inFile>){ chomp $file; my ($var, $value) = split '=', $line, 2; $collect{$var} = $value if defined $value; } $collect{categories} ||= "Misc"; $collect{icon} ||= "-"; $collection{$file} = \%collect; } Dump \%collection;

    Prints:

    $HASH1 = { floop => { Categories => "x;y;z\n", categories => 'Misc', Icon => "boo\n", icon => '-', Name => "baz\n" }, wibble => { Categories => "a;b;c\n", categories => 'Misc', Icon => "bar\n", icon => '-', Name => "foo\n" } };

    True laziness is hard work
      Rather than describing a solution that you can't find, how about describing the problem you are trying to solve. Not the little piece you are having trouble with, but the whole thing.
      Well, OK. Since I might run into problems at the far end of it as well, I guess it makes sense to do that. I'm trying to grab a list of all apps registered in the GNOME applications list and convert it to a menu format used by several other window managers. The first format, or at least the parts of it that mean anything, you've already seen (and used in your script); the second one looks like this:
      menu Applications folder { menu Editors folder { prog vim vim vim prog Lyx emacs lyx } menu "Mail Agents" folder { prog Mutt mutt xterm -e mutt } menu "WWW Browsers" folder { prog Mozilla mozilla mozilla prog Firefox /usr/share/pixmaps/firefox.png firefox prog Seamonkey /usr/share/pixmaps/firefox.png seamonkey prog w3m lynx xterm -e w3m prog Links lynx xterm -e links } menu Graphics folder { prog Gimp gimp gimp } menu Development folder { prog ddd ddd ddd } prog "Acrobat Reader" pdf acroread prog "DVI Previewer" xdvi xdvi } menu Games folder { prog "Koules for X" koules xkoules -f prog Xboing xboing xboing prog Xboard xboard xboard prog XGalaga xgalaga xgal prog XDemineur xdemineur xdemineur prog ppracer /usr/share/pixmaps/ppracer.xpm /usr/games/ppracer prog Arena arena openarena } ...
      Please note that folders (e.g., Applications) can contain other folders (e.g., Editors). Any folder can also contain program entries, which are structured as
      prog "Program name" icon.ext executable_name
      where the program name must be quoted if it contains whitespace, the icon name must be either a filename or a dash (i.e., no icon), and the executable name can be a PATHed program, an absolute path to a program, or a shell construct to be executed. The menu can contain a few other things, but I'm not concerned with any of those at the moment. The overall problem I'm having is creating the folder/program structure that I want reflected in the menus. That's a chain of categories that terminates in one or more programs belonging in that category - i.e.
      menu "Games" folder { menu "GNOME" folder { menu "Action" folder { prog "Arena" arena.png openarena prog "ppracer" /usr/share/pixmaps/ppracer.xpm /usr/game +s/ppracer } } menu "KDE" { ...
      If you can help with the overall problem, I'd appreciate it.
Re: Building an arbitrary-depth, multi-level hash
by samtregar (Abbot) on Feb 28, 2009 at 22:13 UTC
    It's a little tricky, but not hard once you've seen it done once:

    my %hash; my $value = "foo"; my @cats = qw(a b c d); my $p = \%hash; foreach my $item (@cats) { $p->{$item} ||= {}; $p = $p->{$item}; } $p->{_val} = $value; use Data::Dumper; print Dumper(\%hash);

    Output:

    $VAR1 = { 'a' => { 'b' => { 'c' => { 'd' => { '_val' => 'foo' } } } } };

    The trick here is to keep a pointer ($p) to the insertion-point for the next category as you walk through the structure. You also need some way to distinguish values from categories - in this case I used a special "_val" key.

    -sam

        Wow, no. This is basic Perl and shouldn't require a module! I love modules, probably much more than the next guy, but there's a point at which it becomes silly. What's next Scalar::Increment? Loop::For?

        UPDATE: Just looked at the code for Data::Diver. Double wow!

        -sam

      my $p = \%hash; ... $p = $p->{$item};
      AH! Thank you, sir - you're a scholar and a gentleman. *THAT* is the thing that I'd been struggling toward. A thousand thanks, and I'm going to save this thing (I don't think I'm going to go as far as tattooing it on my forehead, but you never know...)

        does not leave around refs to empty hashes (it leaves undef's instead).

        What does that mean? In fact, vivification is required here.

        use strict; use warnings; use Data::Dumper qw( Dumper ); sub samtregar { my $p = shift; my $val = pop; for my $item (@_) { $p->{$item} ||= {}; $p = $p->{$item}; } $p->{_val} = $val; } sub repellent { my $p = \shift; my $val = pop; $p = \$$p->{$_} for @_; $$p->{_val} = $val; } { samtregar(my $samtregar={}, qw( a b c d ), 'foo'); repellent(my $repellent, qw( a b c d ), 'foo'); { local $Data::Dumper::Useqq = 1; local $Data::Dumper::Terse = 1; local $Data::Dumper::Indent = 0; print("samtregar: ", Dumper($samtregar), "\n"); print("repellent: ", Dumper($repellent), "\n"); } }
        samtregar: {"a" => {"b" => {"c" => {"d" => {"_val" => "foo"}}}}} repellent: {"a" => {"b" => {"c" => {"d" => {"_val" => "foo"}}}}}

        And for a getter, what you said is backwards. Yours is the one that vivifies.

        use strict; use warnings; use Data::Dumper qw( Dumper ); sub samtregar_fetcher { my $p = shift; for my $item (@_) { last if !$p->{$item}; $p = $p->{$item}; } return $p->{_val} } sub repellent_fetcher { my $p = \shift; my $val = pop; $p = \$$p->{$_} for @_; return $$p->{_val}; } { samtregar_fetcher(my $samtregar={}, qw( a b c d )); repellent_fetcher(my $repellent, qw( a b c d )); { local $Data::Dumper::Useqq = 1; local $Data::Dumper::Terse = 1; local $Data::Dumper::Indent = 0; print("samtregar: ", Dumper($samtregar), "\n"); print("repellent: ", Dumper($repellent), "\n"); } }
        samtregar: {} repellent: {"a" => {"b" => {"c" => {}}}}
Re: Building an arbitrary-depth, multi-level hash
by ikegami (Patriarch) on Feb 28, 2009 at 22:11 UTC

    For starters, the syntax would be

    # 'categories' entry is 'GNOME;Games;Action' push @{ $out{GNOME}{Games}{Action} }, $val; # 'GNOME;Games;Action;FPS' push @{ $out{GNOME}{Games}{Action}{FPS} }, $val; # ...and so on.

    But you got another problem. You're trying to get an array reference and a hash reference to coexist in the same variable.

    $out{GNOME}{Games}{Action}[0] = $val; $out{GNOME}{Games}{Action}{FPS}[0] = $val; ^ | XXX

    Perhaps you could use the items of a specific category could be placed into a special* key. Say "_".

    $out{GNOME}{Games}{Action}{_}[0] = $val; $out{GNOME}{Games}{Action}{FPS}{_}[0] = $val; ^ | Ok

    * — Special to you, not to Perl.

      I see what you're saying - and thanks, I didn't think about trying to stuff both into the same variable! I still don't see how to resolve the original problem, however.
Re: Building an arbitrary-depth, multi-level hash
by ELISHEVA (Prior) on Mar 01, 2009 at 06:43 UTC

    ++ to GrandFather for asking about the wider problem.

    It seems like the first question here is figuring out how the data in file format 1 maps to the data in file formats 2,3,4, etc. To work out this problem I would:

    1. Do a data model of each file format
    2. Note differences in terminology and cardinality of relationships. For example, is "category" in format 1 equivalent to "menu" in format 2? Does format X allow for recursive menus? Does format Y? Can a menu item belong to more than one menu? And so on.
    3. Note differences in data available for each format. For example, file format 2 appears to have four bits of information per menu item, whereas format 1 has only two (assuming each name/icon corresponds to a menu item)
    4. Define a standard data model that represents all available data
    5. For formats with missing information define defaults

    And now that you have worked out the design issues, you can start thinking about how to morph file formats one into another. For a given format X and Y, this will involve two steps:

    1. Read in a file in format X and convert its data to the standard data model
    2. Traverse the standard data model to output the data in format Y.

    You will need a read_format_to_standard() subroutine for each input format and a write_format_from_standard() routine for each output format. But without a good idea of the standard and how each format maps to it, you will be running in circles writing any of these.

    Best, beth

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://747210]
Approved by ikegami
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others rifling through the Monastery: (2)
As of 2024-04-24 14:23 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found