str_repeat反向(收缩字符串)

str_repeat(A, B) repeat string A, B times:

$string = "This is a " . str_repeat("test", 2) . 
          "! " . str_repeat("hello", 3) . " and Bye!";  

// Return "This is a testtest! hellohellohello and Bye!"

I need reverse operation:

str_shrink($string, array("hello", "test")); 
// Return "This is a test(x2)! hello(x3) and Bye!" or
//        "This is a [test]x2! [hello]x3 and Bye!"

Best and efficient way for create str_shrink function?

Here are two versions that I could come up with.

The first uses a regular expression and replaces duplicate matches of the $needle string with a single $needle string. This is the most vigorously tested version and handles all possibilities of inputs successfully (as far as I know).

function str_shrink( $str, $needle)
{
    if( is_array( $needle))
    {
        foreach( $needle as $n)
        {
            $str = str_shrink( $str, $n);   
        }
        return $str;
    }
    $regex = '/(' . $needle . ')(?:' . $needle . ')+/i';
    return preg_replace_callback( $regex, function( $matches) { return $matches[1] . '(x' . substr_count( $matches[0], $matches[1]) . ')'; }, $str);
}

The second uses string manipulation to continually replace occurrences of the $needle concatenated with itself. Note that this one will fail if $needle.$needle occurs more than once in the input string (The first one does not have this problem).

function str_shrink2( $str, $needle)
{
    if( is_array( $needle))
    {
        foreach( $needle as $n)
        {
            $str = str_shrink2( $str, $n);   
        }
        return $str;
    }
    $count = 1; $previous = -1;
    while( ($i = strpos( $str, $needle.$needle)) > 0)
    {
        $str = str_replace( $needle.$needle, $needle, $str);
        $count++;
        $previous = $i;
    }
    if( $count > 1)
    {
        $str = substr( $str, 0, $previous) . $needle .'(x' . $count . ')' . substr( $str, $previous + strlen( $needle));
    }
    return $str;
}

See them both in action

Edit: I didn't realize that the desired output wanted to include the number of repetitions. I've modified my examples accordingly.

I think you can try with:

<?php
$string = "This is a testtest! hellohellohello and Bye!";

function str_shrink($string, $array){
    $tr = array();
    foreach($array as $el){
        $n = substr_count($string, $el);
        $tr[$el] = $el.'(x'.$n.')';
        $pattern[] = '/('.$el.'\(x'.$n.'\))+/i';
    }
    return preg_replace($pattern, '${1}', strtr($string,$tr));
}

echo $string;
echo '<br/>';
echo str_shrink($string,array('test','hello'));  //This is a test(x2)! hello(x3) and Bye!
?>

I have a second version in order to works with strings:

<?php
$string = "This is a testtest! hellohellohello and Bye!";

function str_shrink($string, $array){
    $tr = array();
    $array = is_array($array) ? $array : array($array);
    foreach($array as $el){
        $sN = 'x'.substr_count($string, $el);
        $tr[$el] = $el.'('.$sN.')';
        $pattern[] = '/('.$el.'\('.$sN.'\))+/i';
    }
    return preg_replace($pattern, '${1}', strtr($string,$tr));
}

echo $string;
echo '<br/>';
echo str_shrink($string,array('test','hello'));  //This is a test(x2)! hello(x3) and Bye!
echo '<br/>';
echo str_shrink($string,'test');  //This is a test(x2)! hellohellohello and Bye!
?>

You can play around with tis one, not tested a lot though

function shrink($s, $parts, $mask = "%s(x%d)"){

            foreach($parts as $part){
                    $removed = 0;

                    $regex = "/($part)+/";

                    preg_match_all($regex, $s, $matches, PREG_OFFSET_CAPTURE);
                    if(!$matches)
                            continue;

                    foreach($matches[0] as $m){
                            $offset = $m[1] - $removed;
                            $nb = substr_count($m[0], $part);
                            $counter = sprintf($mask, $part, $nb);
                            $s = substr($s, 0, $offset) . $counter . substr($s, $offset + strlen($m[0]));
                            $removed += strlen($m[0]) - strlen($part);    
                    }

            }
            return $s;
    }

I kept it short:

function str_shrink($haystack, $needles, $match_case = true) {
    if (!is_array($needles)) $needles = array($needles);
    foreach ($needles as $k => $v) $needles[$k] = preg_quote($v, '/');
    $regexp = '/(' . implode('|', $needles) . ')+/' . ($match_case ? '' : 'i');
    return preg_replace_callback($regexp, function($matches) {
        return $matches[1] . '(x' . (strlen($matches[0]) / strlen($matches[1])) . ')';
    }, $haystack);
}

The behavior of cases like str_shrink("aaa", array("a", "a(x3)")) is it returns "a(x3)", which I thought was more likely intended if you're specifying an array. For the other behavior, giving a result of "a(x3)(x1)", call the function with each needle individually.

If you don't want multiples of one to get "(x1)" change:

        return $matches[1] . '(x' . (strlen($matches[0]) / strlen($matches[1])) . ')';

to:

        $multiple = strlen($matches[0]) / strlen($matches[1]);
        return $matches[1] . (($multiple > 1) ? '(x' . $multiple . ')' : '');