http://www.perlmonks.org?node_id=43860

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

Suppose we have a tab-separated data file like this:
fruit apples fruit oranges fruit bananas meat sausages dairy eggs dairy milk meat bacon
And we have this program:
my( @fruit, @meat, @dairy ); while (<>) # input redirected appropriately { chomp; my( $category, $item ) = split /\t/; push ???$category???, $item; # what now?? }
The idea is to push the item into the array whose name is the same as the value in $category. I don't want to have an if else statement for each array.

Originally posted as a Categorized Question.

Replies are listed 'Best First'.
Re: How can I use the value of a scalar as the name of an array variable?
by runrig (Abbot) on Nov 29, 2000 at 21:59 UTC
    One, rather direct, answer: Use symbolic references.

    Another, possibly better, answer: Use a hash of arrays instead.

    There are a couple issues with using symbolic references for this. One is that symbolic references only work with global variables, so you'll need to declare them with our rather than my. Another is that you'll have to turn off strict refs when accessing the reference. Note that, with this approach, the data file essentially contains variable names! One consequence of this is that if any data row contains a name not declared in your program, you will get a fatal runtime error. Another consequence, which is more of a security hole, is that you could munge your run-time environment if you have a bad data, like

    ENV foo INC bar

    This program illustrates the symbolic ref approach:

    use strict; our( @fruit, @meat, @dairy ); while (<>) { chomp; my( $category, $item ) = split /\t/; no strict 'refs'; push @$category, $item; } print "Fruits are: @fruit\n";
    The hash-of-arrays approach is safer, and arguably cleaner, though potentially more code-heavy:
    use strict; my %food; while (<>) { chomp; my( $category, $item ) = split /\t/; push @{$food{$category}}, $item; } print "Fruits are: @{$food{'fruit'}}\n";
    Lastly, here's a solution using the hash-of-arrays approach but preserving your desire to put each food item into an array named for the corresponding category. To keep the data clean, the category names are normalized on input, and unrecognized category names are discarded.
    use strict; my( @fruit, @meat, @dairy ); my %food; $food{'fruit'} = \@fruit; $food{'meat'} = \@meat; $food{'dairy'} = \@dairy; while (<>) { chomp; my( $category, $item ) = split /\t/; $category = lc $category; # normalize next unless exists $food{$category}; # skip unknown push @{$food{$category}}, $item; } print "Fruits are: @fruit\n";
Re: How can I use the value of a scalar as the name of an array variable?
by Russ (Deacon) on Nov 30, 2000 at 00:55 UTC
    Let's do this without soft references:
    my %Categories; while (<>) { my( $category, $item ) = split /\t/, $_, 2; push @{$Categories{$category}}, $item; } print "$_\n" for sort @{$Categories{'dairy'}};
    Prints:
    eggs milk

    If you must have the arrays as separate variables (not just as values in the %Categories hash), you could define

    %Categories = ( fruit => \@fruit, meat => \@meat, dairy => \@dairy, );
Re: How can I use the value of a scalar as the name of an array variable?
by ambrus (Abbot) on Mar 30, 2012 at 15:49 UTC
Re: How can I use the value of a scalar as the name of an array variable?
by arturo (Vicar) on Nov 30, 2000 at 00:10 UTC
    I'd use a different kind of data structure: a hash of anonymous arrays. That way, you can just push the new type of food onto the array and look up that array by the hash key, which will be the category of food.

    Here's how the data structure looks:

    my %foods = ( fruit => [ "apples", "oranges" ], dairy => [ "eggs", "milk" ], meat => [ "sausage", "bacon" ] );
    my %foods; while (<DATA>) { chomp; my( $category, $item ) = split /\t/; push @{$foods{$category}} ,$item; } for my $category ( keys %foods ) { print "$category : \n"; for my $item ( @{$foods{$category}} ) { print "\t$item\n"; } print "\n"; }
Re: How can I use the value of a scalar as the name of an array variable?
by eg (Friar) on Dec 03, 2000 at 15:51 UTC
    An alternative method is to use eval. In the while loop, try something like this in place of the bare push:
    eval "push \@$category, $item;";
Re: Can I determine if the value of a variable is the same as an arrays name?
by jreades (Friar) on Nov 29, 2000 at 21:16 UTC

    You can do this using symbolic references, but running under -w or use strict this will often generate a number of warnings.

    Here's an example:

    $two = 'test'; @three = ('test'); my @array = ('one', 'two', 'three'); foreach (@array) { if (@$_) { print "Array exists: $_\n"; } if ($$_) { print "Scalar exists: $_\n"; } }

    And this is how it would apply to your problem:

    my @fruit = (); my @meat = (); my @dairy = (); open (FOOD, "./food.txt") while (<FOOD>) { chomp; ($category, $item) = split(/\$/, $_); push(@{$category}, $item) if (@{$category}); }

    So yes, it's do-able, but it's not always the best programming method although it seems to fit here and wouldn't generate warnings as you've declard the arrays you're trying to assign to and not initializing them on the fly.

    Originally posted as a Categorized Answer.

Re: Can I determine if the value of a variable is the same as an arrays name?
by extremely (Priest) on Nov 29, 2000 at 10:53 UTC
    Learn about a Hash of Arrays:
    my %HoA; while (<>) { chomp; my( $category, $item ) = split /\t/, $_, 2; push @{$HoA{$category}}, $item; } for my category ( sort keys %HoA ) { local $" = ", "; print "$category: @{$HoA{$category}}\n"; }
      For the text file example in the "Q", that split should be my ($cat, $item) = split ' ', $_; so that the magic split on space will be in force, why the original "Q"er was doing it that way, I have no idea.

      --
      $you = new YOU;
      honk() if $you->love(perl)

Re: Can I determine if the value of a variable is the same as an arrays name?
by cephas (Pilgrim) on Nov 29, 2000 at 23:32 UTC
    You could try something like this...

    @fruit = (); @meat = (); @dairy = (); open (FOOD, "./food.txt"); while(<FOOD>) { chomp; ($category,$item) = split(/\s+/,$_); push(@{"$category"},$item); }


    But of course, that all assumes you trust your input.

    cephas

    Originally posted as a Categorized Answer.

Re: Can I determine if the value of a variable is the same as an arrays name?
by mnperez (Sexton) on Nov 29, 2000 at 14:09 UTC
    This solution uses a hash of arrays (HoA) instead of symbolic references.
    my %hoa; while (<>) { chomp; my( $category, $item ) = split /\t/; push @{$hoa{$category}}, $item; } for my $cat ( sort keys %hoa ) { print "The following are in category $cat:\n"; for ( @{$hoa{$cat}} ) { print "\t$_\n"; } }

    Originally posted as a Categorized Answer.

Re: Can I determine if the value of a variable is the same as an arrays name?
by jreades (Friar) on Nov 30, 2000 at 20:29 UTC
    What you're looking for are symbolic (or as Russ called them, soft) references.
    my( @fruit, @meat, @dairy ); while (<>) { chomp; my( $category, $item ) = split /\t/; push @{$category}, $item if defined @$category; }
    Note that this version would only push items with pre-declared categories, the rest would be silently discarded -- this might be the behaviour you want, it might not. Your call.

    Update: fixed test per chipmunk's reply.

      The big problem with soft references is that they make things very difficult to debug, and if the user has any control of the variable name, they can cause all sorts of horrible things to happen. What if your program (by accident in your code, by design of somebody malicious, or by the seemingly innocuous mistake of a user) ended up trying to make changes to a variable named "/"? $/ has a certain meaning that can cause your script to behave strangely. All of the sudden your files aren't being read normally.

      Try debugging THAT!

      Generally most uses of variables with soft references can be done much more cleanly by using hashes.

      Quote:
          push(@{$category}, $item) if (@{$category});
      ...
          Note that this version would only push items with pre-declared categories, the rest would be silently discarded

      I'm afraid that version will silently discard everything. @{$category} will never be true, because all of your pre-declared arrays start with zero elements.

      You could do something like this:

      @fruit = (undef); @meat = (undef); @dairy = (undef); while (<FOOD>) { chomp; ($category, $item) = split(' ', $_); push(@{$category}, $item) if (defined @{$category}); } shift @fruit; shift @meat; shift @dairy;
      But really, why would you want to? I think it's much simpler to restrict the categories if you use a hash of lists.
A reply falls below the community's threshold of quality. You may see it by logging in.