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

I have a script that parses through a database of my music to create dynamic playlists. It reads in a text file with input criteria. At one point, my code parses through the text file looking for input variables, like so:

### Find values to be included. Maybe there is a way to do this in a + loop? ($inc_albums)=$content=~/inc_albums=(.*?)\n/; ($inc_genres)=$content=~/inc_genres=(.*?)\n/; ($inc_artists)=$content=~/inc_artists=(.*?)\n/; ($inc_songs)=$content=~/inc_songs=(.*?)\n/; ($inc_ratings)=$content=~/inc_ratings(.*?)\n/; ($inc_year)=$content=~/inc_year(.*?)\n/; ### Find values to be excluded ($exc_albums)=$content=~/exc_albums=(.*?)\n/; ($exc_genres)=$content=~/exc_genres=(.*?)\n/; ($exc_artists)=$content=~/exc_artists=(.*?)\n/; ($exc_songs)=$content=~/exc_songs=(.*?)\n/; ($exc_ratings)=$content=~/exc_ratings(.*?)\n/; ($exc_year)=$content=~/exc_year(.*?)\n/;

you can see the repetitive pattern here. I have this suspicion, that I could could all of this with a 2 or 3 line loop. I also suspect that using references will help me do that. The problem is: I'm still fairly new to Perl, and wading through "Intermediate Perl", and it hasn't quite clicked in my head if:

  • I can do execute this repetitive code in a loop, just replacing the variable "$inc_****" and "$exc__***" on each pass (maybe with a list of values from an array?).
  • references are actually useful in doing this
  • I appreciate any tips anyone can offer that might help me along.

    Replies are listed 'Best First'.
    Re: Changing variable names in a loop
    by morgon (Priest) on Oct 15, 2010 at 19:56 UTC
      If you don't use individual variables but collect them all into a %inc-hash you can do it in one go: (untested)
      my %inc = map { $content =~ /inc_$_(.*?)\n/; $_ => $1 } qw(albums genres artists sonfs ratings year);
      Now rather than using e.g. $inc_year, you use $inc{year}.

      The same pattern can be used for the exclusions.

        A couple of (tested) minor variations on this solution.

        This one mimics your original by avoiding $1:

        my %inc = map { my ($v) = $content =~ /inc_$_=(.*?)\n/; $_ => $v } qw(albums genres artists songs ratings year);
        while this one does not insert into the hash if the regular expression does not match:
        my %inc = map { $content =~ /inc_$_=(.*?)\n/ ? ($_ => $1) : () } qw(albums genres artists songs ratings year);

          If you want to avoid $1 you can also do it like this (then you don't need a temporary variable):
          my %inc = map { $_ => ($content =~ /inc_$_=(.*?)\n/)[0]; } qw(albums genres artists songs ratings year);
    Re: Changing variable names in a loop
    by ig (Vicar) on Oct 15, 2010 at 21:06 UTC

      If you want a parser that you don't have to change when you change your set of input criteria, you can do something like the following:

      use strict; use warnings; use Data::Dumper; my %config = map { /^((?:inc|exc)_\w+)=(.*)$/ } <DATA>; print Dumper(\%config); __DATA__ inc_albums= a list of albums to include inc_genres= a list of genres to include inc_artists= a list of artists to include inc_songs= a list of songs to include inc_year= a list of years to include exc_albums= a list of albums to exclude exc_genres= a list of genres to exclude exc_artists= a list of artists to exclude exc_songs= a list of songs to exclude exc_year= a list of years to exclude

      Which gives:

      $VAR1 = { 'inc_genres' => ' a list of genres to include', 'inc_year' => ' a list of years to include', 'exc_albums' => ' a list of albums to exclude', 'inc_albums' => ' a list of albums to include', 'exc_year' => ' a list of years to exclude', 'exc_songs' => ' a list of songs to exclude', 'inc_artists' => ' a list of artists to include', 'inc_songs' => ' a list of songs to include', 'exc_artists' => ' a list of artists to exclude', 'exc_genres' => ' a list of genres to exclude' };

        NICE! That's even simpler than what I was expecting. :-D

        Thanks for the help.

        So, the values of the hash are a list separated by semicolons (just because I picked semicolons for the delimiter in my input text file). I need to put the list of values into an array, so I can use them to build my SQL statements. Sort of like this:

        # Maybe I can do this as part of the original parsing? # Or maybe I can loop over the %config hash and convert # the scalar values to an array, and put a reference # to that array back into the hash? @inc_albums=split(/; /, $inc_albums) unless $inc_albums=~/^\s$/; @inc_genres=split(/; /, $inc_genres) unless $inc_genres=~/^\s$/; @inc_artists=split(/; /, $inc_artists) unless $inc_artists=~/^\s$/; @inc_songs=split(/; /, $inc_songs) unless $inc_songs=~/^\s$/; @exc_albums=split(/; /, $exc_albums) unless $exc_albums=~/^\s$/; @exc_genres=split(/; /, $exc_genres) unless $exc_genres=~/^\s$/; @exc_artists=split(/; /, $exc_artists) unless $exc_artists=~/^\s$/; @exc_songs=split(/; /, $exc_songs) unless $exc_songs=~/^\s$/; my $i_albumcriteria=q{(Songs.Album LIKE '%'||}.join(q{||'%' OR Songs.A +lbum LIKE '%'||},map {"?"} @inc_albums).q{||'%')} unless (scalar(@inc +_albums)==0) ; my $i_artistcriteria=q{(Songs.Artist LIKE '%'||}.join(q{||'%' OR Songs +.Artist LIKE '%'||},map {"?"} @inc_artists).q{||'%')} unless (scalar( +@inc_artists)==0); my $i_songcriteria=q{(Songs.SongTitle LIKE '%'||}.join(q{||'%' OR Song +s.SongTitle LIKE '%'||},map {"?"} @inc_songs).q{||'%')} unless (scala +r(@inc_songs)==0); my $i_genrecriteria=q{(Songs.Genre LIKE '%'||}.join(q{||'%' OR Songs.G +enre LIKE '%'||},map {"?"} @inc_genres).q{||'%')} unless (scalar(@inc +_genres)==0); my $i_ratingcriteria=qq{Songs.Rating $inc_ratings} unless length($inc_ +ratings)<2; my $i_datecriteria=qq{Songs.Year $inc_year} unless length($inc_year)<2 +;

        I'm now trying to work out how to either do this while I'm parsing the input text the first time around, or by looping over the %confighash recommended by ig. I could use a little bit more help on this.

        EDIT

        I think I got it ...

        foreach my $k (keys %config) { $config{$k}=[split(/; /, $config{$k})] unless $config{$k}=~/^\s$/; }

        Thanks again for the help.

          You could do something like:

          use strict; use warnings; use Data::Dumper; my %config = map { /^((?:inc|exc)_\w+)=(.*)$/; ( $1, [ split(/\s*;\s*/,$2) ] ) } <DATA>; print Dumper(\%config); __DATA__ inc_albums=a; list; of; albums; to; include inc_genres=a; list; of; genres; to; include inc_artists=a; list; of; artists; to; include inc_songs=a; list; of; songs; to; include inc_year=a; list; of; years; to; include exc_albums=a; list; of; albums; to; exclude exc_genres=a; list; of; genres; to; exclude exc_artists=a; list; of; artists; to; exclude exc_songs=a; list; of; songs; to; exclude exc_year=a; list; of; years; to; exclude

          But I find such code too cryptic and would probably do it more like the following myself:

    Re: Changing variable names in a loop
    by superfrink (Curate) on Oct 15, 2010 at 20:02 UTC
      I prefer to use hash table keys when the idea of dynamic variable names comes up.
      #! /usr/bin/perl -w use strict; use Data::Dumper; my @fields = qw/ inc_albums inc_genres /; my %record; my $content = "inc_albums=asdf\n"; foreach my $field (@fields) { ($record{$field}) = $content=~/${field}=(.*?)\n/; } print Dumper(\%record);

      The output is:
      $VAR1 = { 'inc_genres' => undef, 'inc_albums' => 'asdf' };