2
$string='print the imprint with the imprinted printing paper';

$pattern=array('/print/','/imprint/','/paper/',);
$replacement=array('imprint','print','machine');

Output:

print the imprint with the imprinted printing machine

I think I understand correctly that the first two patterns are overriding each other. I am thinking of making this even more complex, but REGEX is still voodoo to me. With the input string shown, I'd like to get this back: imprint the print with the printed imprinting machine. If I could also see how to make it output imprint the print with the imprinted printing machine, that would be great too.

If you could explain your regex, that would be more helpful. Maybe I'll be able to do more on my own afterward.

TecBrat
  • 3,643
  • 3
  • 28
  • 45

2 Answers2

4

Do all of this replacement in a single regex, and you're fine, because in one pass the regex will continue after one replacement and not attempt to match against the replacement again:

$string = 'print the imprint with the imprinted printing paper';

// A single array of find => replace
$replacements = array( 
    'print'   => 'imprint', 
    'imprint' => 'print', 
    'paper'   => 'machine'
);

// Dynamically form the regex, properly escaping it
$delimiter = '/';
$words = array_keys( $replacements);
$regex = $delimiter . '\b(' . implode('|', array_map( 'preg_quote', $words, array_fill( 0, count( $words), $delimiter))) . ')\b' . $delimiter;

The regex that is formed looks like this:

/\b(print|imprint|paper)\b/

Where:

  1. \b is a word boundary.
  2. () is a capturing group.
  3. print|imprint|paper is an or matching one of those words

Finally, to do the replacement:

$result = preg_replace_callback( $regex, function( $match) use( $replacements) {
    return $replacements[$match[1]];
}, $string);
echo $result;

This will output:

imprint the print with the printed imprinting machine
nickb
  • 59,313
  • 13
  • 108
  • 143
  • I think this is what I'll use, with a dynamic $replacements array to go with it. Once I've done some more testing, I'll probably accept this answer. – TecBrat Jul 09 '13 at 16:53
  • I tried this code at an [online testbed](http://writecodeonline.com/php/) and it worked GREAT! I think I might be running php 5.2, and the anon function did not work. I tried this workaround: `function for_callback( $match) { global $replacements; return $replacements[$match[1]]; } $result = preg_replace_callback( $regex, 'for_callback', $string); echo $result;` But something isn't right. You have any ideas? – TecBrat Jul 10 '13 at 20:56
  • @TecBrat - I made your changes to remove the anonymous function and [it works for me](http://viper-7.com/LwELBW) – nickb Jul 10 '13 at 20:59
  • Ok, I'll try it again. Maybe I mistyped something else in my test. – TecBrat Jul 10 '13 at 22:28
2

If you need to do straightforward string replacement that doesn't reiterate the result string multiple times you should use strtr() instead:

strtr($string, array(
    'imprint' => 'print',
    'print' => 'imprint',
    'paper' => 'machine',
));

The words to replace are ordered by string length, the most specific first.

Note: this is of course not as flexible as regular expressions, especially when it comes to replacing only full words, i.e. /\bword\b/ will match word only if it stands by itself; this is not something you can do with strtr() and friends.

Using regular expressions

To make preg_replace() perform only a single pass over the string, you need to combine your replacement keys together into a single expression, i.e.

/imprint|print|paper/

This expression uses alternation, effected by the pipe character in between the search strings. To match only whole words you will need to add boundary matches, a special \b sequence that matches the transition between words and non-words.

/\b(?:imprint|print|paper)\b/

This will match "imprint" but not "pimprint".

If you're going down this route, performing the replacement needs to be done using preg_replace_callback(); for each match it finds a custom function gets executed in which you can determine what to replace it with. You would need to create a replacement map for it, much like the one I've used for my earlier strtr() example.

$map = array(
    'imprint' => 'print',
    'print' => 'imprint',
    'paper' => 'machine',
);

$replacer = function($match) use ($map) {
    // $match[0] holds the found word
    return $map[$match[0]];
};

preg_replace_callback('/\b(?:imprint|print|paper)\b/', $string, $replacer);

Making it dynamic

I've created the regular expression by hand, but to make this flexible you need to generate this dynamically, based off the replacement map. To do this, we need to:

  1. Extract the keys from the replacement map;
  2. Escape any special characters;
  3. Build the final expression.

This is how you would build the expression:

// step 1
$replacement_keys = array_keys($map);
// step 2
$escaped_keys = array_map(function($key) {
    return preg_quote($key, '/');
}, $replacement_keys);
// step 3
$pattern = '/\b(?:' . join('|', $escaped_keys) . ')\b/';
Ja͢ck
  • 170,779
  • 38
  • 263
  • 309