Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Convert array keys from underscore_case to camelCase recursively

I had to come up with a way to convert array keys using undescores (underscore_case) into camelCase. This had to be done recursively since I did not know what arrays will be fed to the method.

I came up with this:

private function convertKeysToCamelCase($apiResponseArray)
{
    $arr = [];
    foreach ($apiResponseArray as $key => $value) {
        if (preg_match('/_/', $key)) {
            preg_match('/[^_]*/', $key, $m);
            preg_match('/(_)([a-zA-Z]*)/', $key, $v);
            $key = $m[0] . ucfirst($v[2]);
        }


        if (is_array($value))
            $value = $this->convertKeysToCamelCase($value);

        $arr[$key] = $value;
    }
    return $arr;
}

It does the job, but I think it could be done much better and more concisely. Multiple calls to preg_match and then concatenation just look weird.

Do you see a way to tidy up this method? And more importantly, is it possible at all to do the same operation with just one call to preg_match ? How would that look like?

like image 296
luqo33 Avatar asked Jul 07 '15 16:07

luqo33


4 Answers

I can quickly spot two separate tasks. One is to convert strings to camel-case format, and the other is to map the keys of a multi-dimensional array. These tasks have nothing to do with the other, and so it is best to implement them as separate functions.

Lets start with a higher-order function mapArrayKeys. It will accept a mapping function and apply this function to each key of the array, producing a new array. We must expect the mapping function to be injective (one-to-one).

function mapArrayKeys(callable $f, array $xs) {
  $out = array();
  foreach ($xs as $key => $value) {
    $out[$f($key)] = is_array($value) ? mapArrayKeys($f, $value) : $value;
  }
  return $out;
}

There are a few fiddly bits that I do not consider so important. You may not want to do type hinting on the parameters, okay fine. Maybe you'd rather if/then/else instead of a ternary operator, okay fine. What is important is that with mapArrayKeys you can apply any (injective) mapping function to array keys.

The second task is to convert strings to camel-case. You might use PCRE functions for this, that's fine. I am going to use explode to do the splitting.

function underToCamel($str) {
  return lcfirst(implode('', array_map('ucfirst', explode('_', $str))));
}

Now these two functions can be used in tandem to achieve the overall goal of converting array keys from underscore to camel-case format.

mapArrayKeys('underToCamel', array('foo_bar' => array ('baz_qux' => 0)));

A note on injectivity. The function underToCamel is not necessarily injective, so you have to take special care. You have to assume that for all x_y and all xY (where Y is the capitalized version of y) that exactly one of x_y, xY, x_Y is an underscore format (same follows more more underscores).

So for example, underToCamel("foo_bar") == "fooBar" and underToCamel("fooBar") == "fooBar" and underToCamel("foo_Bar") == "fooBar" and therefore only one can be a valid underscore format.

Readability of nested functions

This is in response to a comment by luqo33.

What I meant by 'too complex' (at least in my view) is that this solution uses a lot of nested functions (e.g. four functions called in underToCamel, all nested - hinders readibility).

The line of code in question is this.

lcfirst(implode('', array_map('ucfirst', explode('_', $str))));

I contest that this is readable. I do acknowledge that this style is not typical for PHP, and I think that is why the PHP reader may be put off.

First it should be noted that nested functions are not actually as abnormal as you'd think. Consider a math expression.

(-$b + sqrt($b*$b - 4*$a*$c)) / (2*$a)

This is an expression that uses a lot of nested functions: +, -, *, /. If you pretend you haven't had BEDMAS (or equivalent) embedded into your subconscious, this is actually a complicated expression to understand -- there are implicit rules you are subconsciously applying to know that first you do the stuff in parentheses, then the multiplications, and so on. None of this seems complicated because you have learned how to read such expressions and it is now part of your repertoire. The same goes for reading expressions like the one I used.

I can rewrite the expression so that there is one function used per line.

$a = explode('_', $str);
$b = array_map('ucfirst', $a);
$c = implode('', $b);
$d = lcfirst($c);

Now the execution order is read top-to-bottom. I can also write it to read bottom-to-top.

lcfirst(
implode('',
array_map('ucfirst',
explode('_',
$str
))));

Lastly I can write it to read right-to-left or inside-to-out (if you consider the parentheses), which is how it was originally written.

lcfirst(implode('', array_map('ucfirst', explode('_', $str))));

All of these versions use a simple pattern called function composition, which is another reason it is easy to read and understand. With function composition you can build up a sequence of functions where each function feeds from the output of the previous function.

To explain this scenario, my sequence of functions in left-to-right order is explode '_', array_map 'ucfirst', implode '', lcfirst. The way it works can be clearly seem from the version which uses the variables $a through $d. You throw something into explode '_', and the result from that is passed into array_map 'ucfirst', and then into implode '', and finally into lcfirst. You could think of this as a pipeline, or an assembly line, or something like that.

like image 82
erisco Avatar answered Oct 16 '22 05:10

erisco


You can use preg_replace_callback and change all the keys without to loop on each using array_keys and array_combine:

private function convertKeysToCamelCase($apiResponseArray) {
    $keys = preg_replace_callback('/_(.)/', function($m) {
        return strtoupper($m[1]);
    }), array_keys($apiResponseArray));

    return array_combine($keys, $apiResponseArray);
}

or without regex:

private function convertKeysToCamelCase($apiResponseArray) {
    $keys = array_map(function ($i) {
        $parts = explode('_', $i);
        return array_shift($parts). implode('', array_map('ucfirst', $parts));
    }, array_keys($apiResponseArray));

    return array_combine($keys, $apiResponseArray);
}

You can modify the second function to deal with multidimensional arrays:

private function convertKeysToCamelCase($apiResponseArray) {
    $keys = array_map(function ($i) use (&$apiResponseArray) {
        if (is_array($apiResponseArray[$i]))
            $apiResponseArray[$i] = $this->convertKeysToCamelCase($apiResponseArray[$i]);

        $parts = explode('_', $i);
        return array_shift($parts) . implode('', array_map('ucfirst', $parts));
    }, array_keys($apiResponseArray));

    return array_combine($keys, $apiResponseArray);
}
like image 36
Casimir et Hippolyte Avatar answered Oct 16 '22 06:10

Casimir et Hippolyte


The recursive part cannot be further simplified or prettified.

But the conversion from underscore_case (also known as snake_case) and camelCase can be done in several different ways:

$key = 'snake_case_key';
// split into words, uppercase their first letter, join them, 
// lowercase the very first letter of the name
$key = lcfirst(implode('', array_map('ucfirst', explode('_', $key))));

or

$key = 'snake_case_key';
// replace underscores with spaces, uppercase first letter of all words,
// join them, lowercase the very first letter of the name
$key = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $key))));

or

$key = 'snake_case_key':
// match underscores and the first letter after each of them,
// replace the matched string with the uppercase version of the letter
$key = preg_replace_callback(
    '/_([^_])/',
    function (array $m) {
        return ucfirst($m[1]);
    },
    $key
);

Pick your favorite!

like image 45
axiac Avatar answered Oct 16 '22 05:10

axiac


Here is another approach taking advantage of array_walk_recursive() and preg_replace_callback methods in the simplest possible way :)

function convertKeysToCamelCase($array)
{
    $result = [];

    array_walk_recursive($array, function ($value, &$key) use (&$result) {
        $newKey = preg_replace_callback('/_([a-z])/', function ($matches) {
            return strtoupper($matches[1]);
        }, $key);

        $result[$newKey] = $value;
    });

    return $result;
}
like image 41
Ali Avatar answered Oct 16 '22 06:10

Ali