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

Hello monks,
I am trying to parse out a very long string and replace all occurences of about 20 or so different strings with their updates.

Technically I have got it working with 20 seperate

s/thing/newthing/g;
but there has got to be a better way....right?
So I was wondering, for curiousity sake, if there was a way to do all of those substutions and only pass through the string once?
Code example:
$string =~ s/foo/bar/g; $string =~ s/2003/2004/g; $string =~ s/cow/pig/g;
Any ideas how to make that one expression? Thanks!

Replies are listed 'Best First'.
Re: multi find/replace
by Enlil (Parson) on Feb 10, 2004 at 21:13 UTC
    You could do something like the following:
    my %changes = ( foo => 'bar', 2003 => 2004, cow => 'pig'); $string = 'foo 2003 cow cow 2003 foo whatever'; $string =~ s/(foo|2003|cow)/$changes{$1}/g; print $string; __END__ bar 2004 pig pig 2004 bar whatever

    -enlil

      Ah, beat me to the punch :) If you wanted to make sure you don't miss any matches in case you modify the hash in the future, you could make the following mods to Enlil's code:
      ... my $changeList = join('|',(keys %changes)); ... $string =~ s/($changeList)/$changes{$1}/g;


      -jbWare

        In case you want the keys to include meta-characters that you don't want to treat as such, you might want to "protect" them:

        #!/usr/bin/perl -w use strict; my %changes=( foo => 'toto', bar => 'tata', 'fo*' => 'to*', ); my $changeList = join('|', map { "\Q$_\E"} keys %changes); my $string= "foo bar fo*"; $string =~ s/($changeList)/$changes{$1}/g; print $string;

        This way $changeList looks like (\Qkey1\E|\Qkey2|...), which instructs the regexp engine to treat everything between \Q and \E as a literal.

Re: multi find/replace
by Limbic~Region (Chancellor) on Feb 10, 2004 at 21:18 UTC
    Anonymous Monk,
    Assuming you do not care about performance and are only looking to reduce 20 lines of code down to 1, you could do the following:
    $string =~ s/$_/$table{$_}/g for keys %table;
    Where the key in %table is the thing to replace and the value is what to replace it with.

    Cheers - L~R

    Update: It looks like Enlil and I had about the same idea. The difference is his regex only runs once while mine runs as many times as there are keys in hash. I know that alternation really slows down a regex, but his method is still probably faster overall though you could benchmark it if you wanted to know for sure.

      Going through it all just once has more benefits than just speed. Imagine for example, that you have to replace every occurrence of "A" with "B", of "B" with "C", and of "C" with "A" — thus swapping places. That's extremely easy if you do it in one go, and quite impossible to do it in three iterative substitutions.

      Good:

      %replace = ( A => 'B', B => 'C', C => 'A'); s/(A|B|C)/$replace{$1}/g;

      Bad:

      s/A/B/; s/B/C/; s/C/A/;
      For the latter, everything that was "A", "B" or "C", will now be "A".
        bart,
        While you have a valid point, it is not applicable to this problem. The code you state as bad is what the AM already stated as working. While I do not promote my solution as the best one to use, it certainly does save keystrokes.

        Cheers - L~R