1

I want to replace all strings in square brackets ([]) with a randomly chosen item from an array that's named that string.

It's very similar to this issue, but with a twist, in that I want to replace different brackets' contents with strings from arrays named that.

An example should make this a bit clearer.

So say I've got the string

"This is a very [adjective] [noun], and this is a [adjective] [noun]."

And the variables:

$adjective = array("big","small","good","bad");
$noun      = array("house","dog","car");

And we want it to return "This is a very big house, and this is a good dog." or whatever, by choosing randomly. That is, I want to write a PHP function that will replace each [string] with a randomly chosen item from the array named $string. For now it doesn't matter if by randomly choosing it ends up repeating choices, but it must make a fresh choice for each [] item.

I hope I've explained this clearly. If you get what I'm trying to achieve and can think of a better way to do it I'd be very grateful.

Community
  • 1
  • 1
  • 1
    This should be pretty trivial. What have you tried till now ? – DhruvPathak Aug 07 '12 at 10:26
  • 1
    to remove repeats: pass the function a reference to the global word list, randomise the order and pop one off the end (reducing the number by one) and put in into the used list. If the original list is empty, set it to the used list to replenish it. – Waygood Aug 07 '12 at 10:38

5 Answers5

8

Algorithm

  1. Match for this regex: (\[.*?\])
  2. For each match group pick an item from the related array.
  3. Replace in string by order.

Implementation

$string    = "This is a very [adjective] [noun], and this is a [adjective] [noun].";
$adjective = array("big","small","good","bad");
$noun      = array("house","dog","car");

// find matches against the regex and replaces them the callback function.
$result    = preg_replace_callback(

                 // Matches parts to be replaced: '[adjective]', '[noun]'
                 '/(\[.*?\])/',

                 // Callback function. Use 'use()' or define arrays as 'global'
                 function($matches) use ($adjective, $noun) {

                     // Remove square brackets from the match
                     // then use it as variable name
                     $array = ${trim($matches[1],"[]")};

                     // Pick an item from the related array whichever.
                     return $array[array_rand($array)];
                 },

                 // Input string to search in.
                 $string
             );

print $result;

Explanation

preg_replace_callback function performs a regular expression search and replace using provided callback function.

  • First parameter is regular expression to match (enclosed between slashes): /(\[.*?\])/

  • Second parameter is callback function to call for each match. Takes the current match as parameter.

    • We have to use use() here to access the arrays from inside the function, or define the arrays as global: global $adjective = .... Namely, we have to do one of the followings:

      a) Define arrays as global:

        ...
        global $adjective = array("big","small","good","bad");
        global $noun      = array("house","dog","car");
        ...
        function($matches) {
        ...
      

      b) Use use:

        ...
        $adjective = array("big","small","good","bad");
        $noun      = array("house","dog","car");
        ...
        function($matches) use ($adjective, $noun) {
        ...
      
    • First line of the callback function:

      • trim: Removes square brackets ([]) from the match using trim function.

      • ${}: Creates a variable to use as array name with the match name. For example, if the $match is [noun] then trim($matches[1],"[]") returns noun (without brackets) and ${noun} becomes the array name: $noun. For more information on the topic, see variable variables.

    • Second line randomly picks an index number available for the $array and then returns the element at this position.

  • Third parameter is the input string.

mmdemirbas
  • 9,060
  • 5
  • 45
  • 53
  • What does it mean using `${}` in this line: `${trim($matches[1],"[]")}`? – Kerem Aug 07 '12 at 10:58
  • It is [variable of variable](http://php.net/manual/en/language.variables.variable.php). Updating answer with detailed information. – mmdemirbas Aug 07 '12 at 11:02
  • Yea, smart solution. But if the array names change then the pattern names must be changed too, so [adjective] [noun] etc (just a notice). – Kerem Aug 07 '12 at 11:16
  • Yes, I think OP wants exactly this solution. If this is not the case, then a mapping between array names and match names can be added easily. – mmdemirbas Aug 07 '12 at 11:20
  • I'd even go a bit more far and say: Because those are parameter values, they should not be hard-encoded as variable names. Sure this is no difference for the outcome, just for the approach. – hakre Aug 07 '12 at 13:35
  • So this is very close to doing what I'd like, thanks a lot. One issue though, is that there'll likely be hundreds of these variables. Is the only way to call of them within the function? It'll be called iteratively, so some of the arrays might e.g. have the options "a house","a [adjective] house", and the script will comb back through again to replace these. Is there a better way of doing this? – MHG Aug 07 '12 at 14:12
  • Right so, I'm trying to set this up so that I can just set all the arrays first and not clog up that function with them. I'm trying to do something along the lines of this in the function you gave: // Remove square brackets from the match // then use it as variable name $array = $GLOBALS[{trim($matches[1],"[]")}]; But that throws an error. Any ideas? – MHG Aug 07 '12 at 14:36
  • @HughGrigg: I clarified what I meant and gave details. Please, see the answer again. – mmdemirbas Aug 07 '12 at 14:48
1

The code below will do the work:

$string = "This is a very [adjective] [noun], and this is a [adjective] [noun]."

function replace_word ( $matches )
{
    $replaces = array(
        '[adjective]'  =>  array("big", "small", "good", "bad"),
        '[noun]'  =>  array("house", "dog", "car")
    );

    return $replaces[$matches[0]][array_rand($replaces[ $matches[0] ])];
}

echo preg_replace_callback("(\[.*?\])", "replace_word", $string);

First, we regular expression match on the [something] parts of the word, and call the replace_word() callback function on it with preg_replace_callback(). This function has an internal $replaces two dimension deep array defined inside, each row defined in a [word type] => array('rep1', 'rep2', ...) format.

The tricky and a bit obfuscated line is the return $replaces[$matches[0]][array_rand($replaces[ $matches[0] ])];. If I chunk it down a bit, it'll be a lot more parsable for you:

$random = array_rand( $replaces[ $matches[0] ] );

$matches[0] is the word type, this is the key in the $replaces array we are searching for. This was found by regular expression in the original string. array_rand() basically selects one element of the array, and returns its numerical index. So $random right now is an integer somewhere between 0 and the (number of elements - 1) of the array containing the replaces.

return $replaces[ $matches[0] ][$random];

This will return the $randomth element from the replace array. In the code snippet, these two lines are put together into one line.

Showing one element only once

If you want disjunct elements (no two adjective or noun repeated twice), then you will need to do another trick. We will set the $replaces array to be defined not inside the replace_word() function, but outside it.

$GLOBALS['replaces'] = array(
    '[adjective]'  =>  array("big", "small", "good", "bad"),
    '[noun]'  =>  array("house", "dog", "car")
);

Inside the function, we will set the local $replaces variable to be a reference to the newly set array, with calling $replaces = &$GLOBALS['replaces'];. (The & operator sets it a reference, so everything we do with $replaces (remove and add elements, for example) modifies the original array too. Without it, it would only be a copy.)

And before arriving on the return line, we call unset() on the currently to-be-returned key.

unset($replaces[$matches[0]][array_rand($replaces[ $matches[0] ])]);

The function put together now looks like this:

function replace_word ( $matches )
{
    $replaces = &$GLOBALS['replaces'];
    unset($replaces[$matches[0]][array_rand($replaces[ $matches[0] ])]);

    return $replaces[$matches[0]][array_rand($replaces[ $matches[0] ])];
}

And because $replaces is a reference to the global, the unset() updates the original array too. The next calling of replace_word() will not find the same replace again.

Be careful with the size of the array!

Strings containing more replace variables than the amount of replace values present will throw an Undefined index E_NOTICE. The following string won't work:

$string = "This is a very [adjective] [noun], and this is a [adjective] [noun]. This is also an [adjective] [noun] with an [adjective] [noun].";

One of the outputs look like the following, showing that we ran out of possible replaces:

This is a very big house, and this is a big house. This is also an small with an .

Whisperity
  • 3,012
  • 1
  • 19
  • 36
1

Another good (easier) method of doing this (not my solution)

https://stackoverflow.com/a/15773754/2183699

Using a foreach to check on which variables you want to replace and replacing them with

str_replace();
Community
  • 1
  • 1
Miguel Stevens
  • 8,631
  • 18
  • 66
  • 125
0

You can use preg_match and str_replace function to achive this goal.

  • First find the matches using preg_match function and then create search & replace array from the result.
  • Call str_replace function by passing the previous arrays as parameters.
monish
  • 182
  • 2
  • 10
0

This is my minor update to mmdemirbas' answer above. It lets you set the variables outside of the function (i.e. use globals, as said).

$result    = preg_replace_callback(

                 // Matches parts to be replaced: '[adjective]', '[noun]'
                 '/(\[.*?\])/',

                 // Callback function. Use 'use()' or define arrays as 'global'
                 function($matches) use ($adjective, $noun) {

                     // Remove square brackets from the match
                     // then use it as variable name
                    $arrayname = trim($matches[1],"[]");
                    $array = $GLOBALS[$arrayname];

                     // Pick an item from the related array whichever.
                     return $array[array_rand($array)];
                 },

                 // Input string to search in.
                 $string
             );

print $result;
MHG
  • 1,410
  • 3
  • 19
  • 32
  • I also have a little question, though. Would it be possible to prevent the same item being used twice by unsetting it from the array when it gets used? I'm trying to do it with this, inserted just before "Input string to search in.": unset($GLOBALS[$arrayname[$item]]); Unfortunately this doesn't seem to work. As far as I can tell it has no effect? – MHG Aug 11 '12 at 15:02