automatix
automatix

Reputation: 14532

How to extract only elements with key prefixed by a string efficiently in PHP?

I need a function, that takes an $array and returns it with only the elements, where the key is prefixed by a a given $prefix. The keys of the result array should not contain the prefix. Here is the unit test, that the method has to pass:

class ArrayProcessorTest extends \PHPUnit_Framework_TestCase
{
    /**
     * @dataProvider provideDataForExtractElementsWithKeyPrefixedByString
     */
    public function testExtractElementsWithKeyPrefixedByString($testArray, $prefix, $expectedResult)
    {
        $this->assertEquals($expectedResult, $this->arrayProcessor->extractElementsWithKeyPrefixedByString($testArray, $prefix));
    }
}

public function provideDataForExtractElementsWithKeyPrefixedByString()
{
    $data = [];
    $testArray = [
        'foo__qwe' => '123', 'bar__asd' => '234', 'baz__yxc' => '345', 'buz__lmn' => '456',
        'foo__qsc' => '567', 'bar__wsx' => '678', 'baz__edc' => '789', 'buz__rfv' => '890',
    ];
    $prefixes = [
        'singlePrefixFoo' => 'foo__',
        'arrayPrefixBarBuz' => ['bar__', 'buz__',]
    ];
    $expectedResults = [
        'singlePrefixFoo' => ['qwe' => '123', 'qsc' => '567',],
        'arrayPrefixBarBuz' => ['asd' => '234', 'wsx' => '678', 'lmn' => '456', 'rfv' => '890',]
    ];
    $data = [
        [$testArray, $prefixes['singlePrefixFoo'], $expectedResults['singlePrefixFoo']],
        [$testArray, $prefixes['arrayPrefixBarBuz'], $expectedResults['arrayPrefixBarBuz']]
    ];
    return $data;
}

Here my variant of the method:

class ArrayProcessor
{
    public function extractElementsWithKeyPrefixedByString(array $array, $prefix)
    {
        $filteredArray = [];
        if (is_string($prefix)) {
            array_walk($array, function($value, $keyName) use($prefix, &$filteredArray) {
                if (strpos($keyName, $prefix) === 0) {
                    $filteredArray[str_replace($prefix, '', $keyName)] = $value;
                }
            });
        } elseif (is_array($prefix)) {
            foreach ($prefix as $currentPrefix) {
                $filteredArray = array_merge(
                    $filteredArray, $this->extractElementsWithKeyPrefixedByString($array, $currentPrefix)
                );
            }
        }
        return $filteredArray;
    }
}

How to make this method more efficient?

Upvotes: 0

Views: 236

Answers (3)

Gordon
Gordon

Reputation: 316979

How about

$array = ['foo__x' => 1, 'foo__y' => 2, 'bar__z' => 3, 'baz' => 42];
// For other test arrays and prefixes (of type `string` and `array`) s. the unit test in the question.

public function extractElementsWithKeyPrefixedByString(array $array, $prefix)
{
    if (is_array($prefix)) {
        $prefix = implode('|', $prefix);
    }
    $iterator = new \RegexIterator(
        new \ArrayIterator($array),
        '~^(' . $prefix . ')([a-zA-Z0-9_-]+)$~',
        \RegexIterator::REPLACE,
        \RegexIterator::USE_KEY
    );
    $iterator->replacement = '$2';

    return iterator_to_array($iterator);
}

Output:

Array
(
    [x] => 1
    [y] => 2
    [z] => 3
)

It performs a about 10% better than the original code (for a case with string and array prefixes):

xDebug callgraph before (for the original code):

xDebug callgraph before

xDebug callgraph after:

xDebug callgraph after

Note that removing the prefixes is not necessarily a good idea. If you got foo__x and bar__x and remove both, "foo__" and "bar__", only the last x will make it to the resulting array, e.g. the value of foo__x will be overwritten by bar__x.

Upvotes: 2

automatix
automatix

Reputation: 14532

I could now significantly increase the performance by using the preg_filter(...) ans some native array manipulating functions:

public function extractElementsWithKeyPrefixedByString(array $array, $prefix)
{
    $filteredArray = [];
    if (is_array($prefix)) {
        $prefix = implode('|', $prefix);
    }
    $filteredKeys = preg_filter(['/(' . $prefix . ')([a-zA-Z0-9_-]+)/'], ['$2'], array_keys($array));
    $arrayValues = array_values($array);
    $filteredValues = array_intersect_key($arrayValues, $filteredKeys);
    $filteredArray = array_combine($filteredKeys, $filteredValues);
    return $filteredArray;
}

xDebug callgraph before:

xDebug callgraph before

xDebug callgraph after:

xDebug callgraph after

A disadvantage of this solution is, that I have to explicitly set the regex for the part of array key after the $prefix (here [a-zA-Z0-9_-]+ -- it's not complete!). But it's at least faster (much, much faster!) than the original variant.

Upvotes: 0

Vural
Vural

Reputation: 8748

You can try:

$output = preg_grep('!^foo__!', $array);

or

$output = preg_grep('/foo__(\w+)/', $array);

or for multiple prefixes:

$output = preg_grep('/(foo|bar|baz)__(\w+)/', $array);

Please see for more informations:

Upvotes: 1

Related Questions