-2
$arr = array(
    array("one" => 1, "two-two" => 2, "four" => 4),
    array("two-two" => 22, "three" => 33, "four" => 44)
);

$keys = array_flip(
    array_keys(
        call_user_func_array('array_merge', $arr)
    )
);
array_walk(
    $keys,
    function(&$val, $key) {
         $val = ucwords(
             str_replace(array("_", "-" ), " ", $key)
         );
    }
);
print_r( $keys );

Result:

Array (
    [one] => One
    [two-two] => Two Two
    [four] => Four
    [three] => Three
)

The code:

  1. Flattens and merges the 2-dimensional array so there are only unique keys
  2. Flips array keys so the keys are once again where they ought to be (keys, not values)
  3. Sets the value of each element to a string transformed version of the key (upper case and replace dashes and underscores with spaces).

I feel there should be a one-liner in here somewhere and can't see it. Can you craft a one-liner?

mickmackusa
  • 43,625
  • 12
  • 83
  • 136
Tom Auger
  • 19,421
  • 22
  • 81
  • 104
  • 1
    I ran it just to see what the hell you were talking about. – AbraCadaver May 12 '16 at 21:02
  • 1
    i wish i had the time to care about such micro optermisation, rather than just getting the job dome –  May 12 '16 at 21:03
  • 2
    Even if you could do it as a 'one liner'. Should you? How does that help the person to follow you, to understand it? Imagine you are the person who has to change it? You are ok with that? honest? imo, Make code understandable? It is easy to write code that no one can understand - not even you - even when it is your code to change in a few weeks time.;-/ – Ryan Vincent May 12 '16 at 21:22
  • 1
    I'm voting to close this question as off-topic because this probably belongs on http://codereview.stackexchange.com/ – RiggsFolly May 12 '16 at 21:23
  • 1
    This would not be very well received on Code Review unless and until the OP looks at this: [How do I ask a good question?](http://codereview.stackexchange.com/help/how-to-ask) Also, chances are that we would recommend ways to make your code better, not necessarily shorter. – Phrancis May 12 '16 at 21:28
  • I understand both the suggestion to move to Code Review, as well as the reason why it would probably not fly there either. I had considered code review first, but was sure I would get nothing but comments on how to make the code more readable, self-documenting, etc, which is the opposite of what I was trying to achieve. This is a personal interest question and frankly I can't think of any community better equipped to answer this question than this one! – Tom Auger May 16 '16 at 16:29

5 Answers5

2

Well, you can use array_walk_recursive. Though it is not a one-liner, you call less functions with this one.

$arr = array( array( "one" => 1, "two-two" => 2, "four" => 4),
        array( "two-two" => 22, "three" => 33, "four" => 44));

$res = array();
array_walk_recursive($arr, function ($val, $key) use (&$res) {
    $res[$key] = ucwords(str_replace(array('_', '-'), ' ', $key));
});
Mikey
  • 6,728
  • 4
  • 22
  • 45
1

from a functional programmer's perspective

I'll go thru a couple iterations of your code and explain the steps I'm taking as I go. Most of my decisions are born out of the need to always be reducing complexity. For example, when you use array_walk, do you have to think about how to iterate thru the array? Do you think about array indexes or making sure you increment after each iteration? No. array_walk is powerful because it hides those nasty details away from the programmer – however that stuff happens is not for us to worry about.

So why do you smash your brain with worries like this?

ucwords( str_replace( array( "_", "-" ), " ", $key ) )

That is a function of its own, let's call it humanIdentifier. It takes some programmatic $key value and returns a nice, human-friendly string

function humanIdentifier ($x) {
  return ucwords(str_replace('-', ' ', $x));
}

With this one tiny change, we will have already simplified your code a lot – the complexity goes down because we no longer have to worry about how to make the key-to-human-readable-string conversion. Those yucky details have been abstracted away.

// things are improving ...
array_walk( $keys, function( &$val, $key ){ $val = humanIdentifier($val); } );

This is the approach I'm going to take as I continue to work on other parts of this answer


array_map sucks

many of the functional residents (eg) array_reduce, array_map, array_filter, array_walk are a disaster in PHP. The interfaces are horribly inconsistent and the behaviours are sometimes just downright wonky. As you and others have identified, array_map does not give us a way to access the key, but there's nothing stopping you from making a generic function that makes it accessible.

function array_kmap (callable $f, iterable $xs) {
  return array_reduce(array_keys($xs), function ($acc, $k) use ($xs, $f) {
    return array_merge($acc, [$k => call_user_func($f, $k, $xs[$k], $xs)]);
  }, []);
}

Using this combined with your humanIdentifier function, we could come up with a solution pretty easily

$a = [
  ['one' => 1, 'two-two' => 2, 'four' => 4],
  ['two-two' => 22, 'three' => 33, 'four' => 44]
];

$b = array_reduce($a, function ($acc, $x) {
  return array_merge($acc, array_kmap(function ($k, $v) {
    // we don't actually use `$v`, so we can ignore it
    return humanIdentifier($k);
  }, $x));
}, []);

print_r($b);
// Array
// (
//     [one] => One
//     [two-two] => Two Two
//     [four] => Four
//     [three] => Three
// )

hidden complexity

There's still some complexity hiding within the transformation of $a to $b. Can you spot it? array_reduce is pretty much the grandfather of most functionals intended for use with iterables – it's immensely powerful, but it has to attribute that power to its extremely generic interface. Our use of array_reduce is pretty much a wrapper around array_merge and our mapping function, $f. We can derive a new function that does just this that acts as a sort of specialized array_reduce - most functional languages call this flat map.

function array_flatmap (callable $f, iterable $xs) {
  return array_reduce(array_map($f, $xs), 'array_merge', []);
}

$a = [
  ['one' => 1, 'two-two' => 2, 'four' => 4],
  ['two-two' => 22, 'three' => 33, 'four' => 44]
];

$b = array_flatmap(function ($x) {
  return array_kmap(function ($k) {
    return humanIdentifier($k);
  }, $x);
}, $a);

print_r($b);
// Array
// (
//     [one] => One
//     [two-two] => Two Two
//     [four] => Four
//     [three] => Three
// )

eta conversion

What? Eta conversion comes from lambda calculus and says that

 function ($x) { return $f($x); }      === $f

(function ($x) { return $f($x); })($y) === $f($y)
                        $f($y)         === $f($y)
                        $f             === $f

I mention this because we can eta-convert some of the code to reduce even more complexity. There are two eta conversions that could help our program. Do you see where?

array_kmap(function ($k) {
  return humanIdentifier($k);
}, $x)

This dangling $k can easily be removed – here is simplified but equivalent code (Note: this is possible because we're discarding the $v value from your callable – only the key is necessary to compute our transformation)

array_kmap('humanIdentifier', $x)

Now, if we zoom out a little bit, we see this!

function ($x) {
  return array_kmap('humanIdentifier', $x);
}

Another little dangling $x on the end of our array_kmap function! If we were to partially apply our array_kmap function, we could remove the $x point which would get rid of function ($x) { ... } altogether.

Of course PHP doesn't have any means of partially applying a function, so we have to make that

function partial (callable $f, ...$xs) {
  return function (...$ys) use ($f, $xs) {
    return call_user_func($f, ...$xs, ...$ys);
  };
}

And now our resulting transformation is a thing of beauty

$a = [
  ['one' => 1, 'two-two' => 2, 'four' => 4],
  ['two-two' => 22, 'three' => 33, 'four' => 44]
];

$b = array_flatmap(partial('array_kmap', 'humanIdentifier'), $a);

print_r($b);
// Array
// (
//     [one] => One
//     [two-two] => Two Two
//     [four] => Four
//     [three] => Three
// )

code reflects data reflects code

... data reflects code reflects data ... Take a look at our final bit of code there:

$b = array_flatmap(partial('array_kmap', 'humanIdentifier'), $a);

We're doing an array_map of an array_map - this makes sense because our initial data is an array of arrays! Here the design of our code is a reflection of the shape of the data it operates on.

This is great because even if we didn't write this, we could look at this code and immediately know the shape of the data it's mean to work on


putting it all together

Just to save you the time of gathering all of the snippets above, here's a complete runnable script with verified output

function array_kmap (callable $f, iterable $xs) {
  return array_reduce(array_keys($xs), function ($acc, $k) use ($xs, $f) {
    return array_merge($acc, [$k => call_user_func($f, $k, $xs[$k], $xs)]);
  }, []);
}

function humanIdentifier ($x) {
  return ucwords(str_replace('-', ' ', $x));
}

function array_flatmap (callable $f, iterable $xs) {
  return array_reduce(array_map($f, $xs), 'array_merge', []);
}

function partial (callable $f, ...$xs) {
  return function (...$ys) use ($f, $xs) {
    return call_user_func($f, ...$xs, ...$ys);
  };
}

$a = [
  ['one' => 1, 'two-two' => 2, 'four' => 4],
  ['two-two' => 22, 'three' => 33, 'four' => 44]
];

$b = array_flatmap(partial('array_kmap', 'humanIdentifier'), $a);

print_r($b);
// Array
// (
//     [one] => One
//     [two-two] => Two Two
//     [four] => Four
//     [three] => Three
// )

remarks

We wrote a lot of code here compared to the original code posted in your question. So maybe you're wondering how this is an improvement. Well, that's a qualitative measure, and in a lot of areas (eg speed, efficiency) this answer is probably worse. But in other areas (eg readability, maintainability) I see a dramatic improvement.

Like others, when I first read your code, I was scratching my head over what the heck it did. Looking at the resulting transformation, I can reason about what's happening more easily because I'm less focused on how things are being transformed and I can just focus on the parts that matter.

If you squint your eyes, this is basically all we have to be concerned about

// input is array of arrays
$a = array(array( ... ))

// output requires map of map of input
$b = map(map( ... humanIdentifier ))

We did a bunch of other stuff, too, like avoided unnecessary assignments, reassignments, or mutations. $a is untouched as a result of creating $b. Avoiding side-effects like this aids in increasing readability and decreasing complexity as our program continues to grow. Anyway, those are out-of-scope for this answer, but I figured I'd mention them.

Hope to have helped ^_^

Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Absolutely amazing answer (and kindly written - thank you). I will be coming back to this for a long time to come to extract nuggets of wisdom. It's too bad more people will probably not see this answer because my original question was downvoted so many times. – Tom Auger May 09 '17 at 18:39
0

Not one line, but you can still remove the line breaks.

$output = [];
foreach ($arr as $subarray) { foreach ($subarray as $key => $value) {
    $output[$key] = ucwords(str_replace(array("_", "-"), " ", $key));
} }
clemens321
  • 2,103
  • 12
  • 18
0
$arr = array(
    array("one" => 1, "two-two" => 2, "four" => 4),
    array("two-two" => 22, "three" => 33, "four" => 44)
);

In clear functional way:

$keys = array_keys(call_user_func_array('array_merge', $arr));
$formattedKeys = array_map(
    function($key) {return ucwords(str_replace(array('_', '-'), ' ', $key));},
    $keys
);
print_r(array_combine($keys, $formattedKeys));

Or just in short way:

$keys = array();
foreach(array_keys(call_user_func_array('array_merge', $arr)) as $key)
    $keys[$key] = ucwords(str_replace(array('_', '-'), ' ', $key));
print_r($keys);

Or:

$keys = array();
foreach(call_user_func_array('array_merge', $arr) as $key=>$unused)
    $keys[$key] = ucwords(str_replace(array('_', '-'), ' ', $key));
print_r($keys);

If $arr always contains two items, you can also replace call_user_func_array('array_merge', $arr) with $arr[0]+$arr[1] (as array_merge($a, $b) and $a+$b differ only in values, not in keys).

Sasha
  • 3,599
  • 1
  • 31
  • 52
  • Does `array_map()` work on keys? I thought it worked on values only, though I think I saw somewhere that in later versions of PHP, there was a flag that could be passed to array_map to make it work on keys. – Tom Auger May 16 '16 at 16:35
  • @TomAuger, `array_map` works only on values. But you can extract keys of an array with `array_keys($array)` and then pass these keys-turned-into-values into `array_map`. Or you can extract both keys (with `array_keys($array)`) and values (just using `$array`) and pass them both simultaneously into `array_map` (i.e. `array_map($callback, array_keys($array), $array)`, where `$callback` is `function($key, $value) {…;}`). – Sasha May 16 '16 at 21:54
  • @TomAuger, i.e. `array_map($callback, [10, 20], ['hello', 'world'], [false, true]);` will do `$callback(10, 'hello', false); $callback(20, 'world', true);`. All arrays passed to single `array_map` should be of same length. – Sasha May 16 '16 at 22:01
  • @TomAuger, [array_map](http://php.net/manual/en/function.array-map.php) doesn't have a flag to work on keys. But you can pass keys manually -- either *instead* of values, or *in addition* to values. – Sasha May 16 '16 at 22:03
0

If you are looking for a sleek, functional-style script that returns the new array structure, then I recommend array_reduce() so that you can control the first level keys (array_map() does not afford this luxury).

The following can be smashed into a one-liner, but for readability, I'll break it into separate lines.

The data passed into array_reduce() is already flattened and contains all of the original keys as values.

Inside the custom function body, array_merge() is used to allow the more modern and concise "arrow function syntax" which became available in PHP7.4.

Code: (Demo)

var_export(
    array_reduce(
        array_keys(array_merge(...$array)),
        fn($result, $key) =>
            array_merge(
                $result,
                [$key => ucwords(str_replace(['_', '-'], ' ', $key))]
            ),
        []
    )
);

Below PHP7.4 (Demo)

var_export(
    array_reduce(
        array_keys(array_merge(...$array)),
        function($result, $key) {
            $result[$key] = ucwords(str_replace(['_', '-'], ' ', $key));
            return $result;
        }
    )
);

If you are really interested in getting the visible characters to a minimum, you can use strtr() instead of str_replace(). (Demo)

ucwords(strtr($key, '_-', '  '))
mickmackusa
  • 43,625
  • 12
  • 83
  • 136