How do I use the default values ​​for references that do not match in the php preg_replace function?

advertisements

What I would like to do

In php, something like this:

$input = '23:10';
$pattern = '/^(\d?\d):(\d?\d)(:(\d?\d))?$/';
$rewrite = '$1h$2m$4s';

$output = preg_replace( $pattern, $rewrite, $input );
echo $output;

$input can be a time with or without the seconds, but without seconds it should return '0' as default for $4 in the replacement string. So the output, which is now:

23h10ms

should be:

23h10m0s

Important:

For the solution it's really important that the input of the final function consists of an arbitrary $pattern and $rewrite string that transform the $input string to the output format. So no hard-coded checks for that seconds part, but really a general way of inserting a default value for references in the $rewrite string that refer to optional parts in the $pattern string. The function should also work for a case with (for example):

$input = '3 hours';
$pattern = '/^(\d+) hours( and (\d+) minutes)?( and (\d+) seconds)?$/';
$rewrite = '$1h$3m$5s';

As this example illustrates, I have no control over the desired formats ($pattern + $rewrite), so they can vary quite a lot. Most important reason for this: input and output can/will be in other languages too and therefore $pattern and $rewrite are obtained from language files (Joomla).

Possible direction to solution

The closest I have come so far is the following:

// Get a $matches array:
preg_match( $pattern, $input, $matches );

// Replace the references ($1, $2, etc.) in the rewrite string by these matches:
$output = preg_replace_callback(
  '/\$(\d+)/g',
  function( $m ) {
    $i = $m[1];
    return ( isset( $matches[$i] ) ? $matches[$i] : '0' );
  },
  $rewrite
);

Two things about this: 1. It does not work yet, because $matches is not available in the callback function. 2. It becomes very tricky using the rewrite string as input for preg_replace_callback().


Update

For the wanted functionality to be dynamic, I'd suggest writing a new class in which all needed information are attributes. Inside that class you can call preg_replace_callback with a member method and there check for the existence of all needed groups - if one does not exist, it shall be replaced with the defined $defaultValue.

class ReplaceWithDefault {

    private $pattern, $replacement, $subject, $defaultValue;

    public function __construct($pattern, $replacement, $subject, $defaultValue) {
        $this->pattern = $pattern;
        $this->replacement = $replacement;
        $this->subject = $subject;
        $this->defaultValue = $defaultValue;
    }

    public function replace() {
        return preg_replace_callback($this->pattern, 'self::callbackReplace', $this->subject);
        //alternative: return preg_replace_callback($this->pattern, array($this, 'callbackReplace'), $this->subject);
    }

    private function callbackReplace($match) {
        // fill not found groups with defaultValue
        if (preg_match_all('/\$\d{1,}/i', $this->replacement, $values)) {
            foreach ($values[0] as $value) {
                $index = substr($value, 1);//get rid of $ sign
                if (!array_key_exists($index, $match)) {
                    $match[$index] = $this->defaultValue;
                }
            }
        }

        $result = $this->replacement;
        // do the actual replacement
        krsort($match);
        foreach ($match as $key=>$value) {
            $result = str_replace('$'.$key, $value, $result);
        }
        return $result;
    }

}

You'll need krsort(), so it doesn't mistake e.g. $15 for {$1}5.

The class can now be used like this

$originalQuestion = new ReplaceWithDefault('/^(\d?\d):(\d?\d)(:(\d?\d))?$/',
    '$1h$2m$4s',
    '23:10',
    '00');
$updatedQuestion = new ReplaceWithDefault('/^(\d+) hours( and (\d+) minutes)?( and (\d+) seconds)?$/',
    '$1h$3m$5s',
    '3 hours',
    '0');

print "<pre>";
var_dump($originalQuestion->replace());
var_dump($updatedQuestion->replace());

And will produce the wanted result.


original post

I'm not sure if this can be done with a regular preg_replace(). However, PHP offers another possibility: preg_replace_callback(). When defining the function, you can use more complex logic.

$input = '23:10';
$pattern = '/^(\d?\d):(\d?\d)(:(\d?\d))?$/';

function correctFormat($hit) {

    if (isSet($hit[4]))
        $seconds = $hit[4];
    else
        $seconds = "0";

    return $hit[1]."h".$hit[2]."m".$seconds."s";
}

$output = preg_replace_callback( $pattern, "correctFormat" , $input );
echo $output;


I'd suggest a solution as shown by Chinnu R, since a regex is obviously not necessary and has no advantages to a regular explode in this case.