How can I split a single array into it's sub-keys?
$arr = array(
0 => array(
'foo' => '1',
'bar' => 'A'
),
1 => array(
'foo' => '2',
'bar' => 'B'
),
2 => array(
'foo' => '3',
'bar' => 'C'
)
);
What is the most efficient way to return an array of foo and bar separately?
I need to get here:
$foo = array('1','2','3');
$bar = array('A','B','C');
I'm hoping there's a clever way to do this using array_map or something similar. Any ideas?
Or do I have to loop through and build each array that way? Something like:
foreach ($arr as $v) {
$foo[] = $v['foo'];
$bar[] = $v['bar'];
}
In a lucky coincidence, I needed to do almost the exact same thing earlier today. You can use array_map() in combination with array_shift():
$foo = array_map('array_shift', &$arr);
$bar = array_map('array_shift', &$arr);
Note that $arr is passed by reference! If you don't do that, then each time it would return the contents of $arr[<index>]['foo']. However, again because of the reference - you won't be able to reuse $arr, so if you need to do that - copy it first.
The downside is that your array keys need to be ordered in the same way as in your example, because array_shift() doesn't actually know what the key is. It will NOT work on the following array:
$arr = array(
0 => array(
'foo' => '1',
'bar' => 'A'
),
1 => array(
'bar' => 'B',
'foo' => '2'
),
2 => array(
'foo' => '3',
'bar' => 'C'
)
);
Update:
After reading the comments, it became evident that my solution triggers E_DEPRECATED warnings for call-time-pass-by-reference. Here's the suggested (and accepted as an answer) alternative by #Baba, which takes advantage of the two needed keys being the first and last elements of the second-dimension arrays:
$foo = array_map('array_shift', $arr);
$bar = array_map('array_pop', $arr);
$n = array();
foreach($arr as $key=>$val) {
foreach($val as $k=>$v) {
$n[$k][] = $v;
}
}
array_merge_recursive will combine scalar values with the same key into an array. e.g.:
array_merge_recursive(array('a',1), array('b',2)) === array(array('a','b'),array(1,2));
You can use this property to simply apply array_merge_recursive over each array in your array as a separate argument:
call_user_func_array('array_merge_recursive', $arr);
You will get this result:
array (
'foo' =>
array (
0 => '1',
1 => '2',
2 => '3',
),
'bar' =>
array (
0 => 'A',
1 => 'B',
2 => 'C',
),
)
It won't even be confused by keys in different order.
However, every merged value must be scalar! Arrays will be merged instead of added as a sub-array:
array_merge_recursive(array(1), array(array(2)) === array(array(1,2))
It does not produce array(array(1, array(2)))!
Related
Consider this collection below:
$collection = [
[1 => 10.0, 2 => 20.0, 3 => 50.0, 4 => 80.0, 5 => 100.0],
[3 => 20.0, 5 => 20.0, 6 => 100.0, 7 => 10.0],
[1 => 30.0, 3 => 30.0, 5 => 10.0, 8 => 10.0]
];
Consider this theorical output based on the intersection of the Arrays contained into $collection, considering their array keys with respective values based on the average of the single values:
$output = Array ( 3 => 33.3333, 5 => 43.3333 );
Can this problem be resolved with a native PHP function like array_intersect_* in an elegant way?
If not, can you suggest me an elegant solution that doesn't necessarily need an outer ugly foreach?
Keep in mind that the number of arrays that need to be intersected is not fixed. It can be 2 input arrays as it can be 1000 input arrays.
Keys will be integers at all times, and Values will be floats or integers at all times.
In other words:
$collection = [
$arr1 = [ ... ];
$arr2 = [ ... ];
$arr3 = [ ... ];
...
$arrn = [ ... ];
];
$output = [ intersected and weighted array based (on comparison) on keys from $arr1 to $arrn, and (on values) from the value averages ];
Count the input array once.
$n = count($collection);
Compute the intersection of all the sub-arrays by key.
$intersection = array_intersect_key(...$collection);
// PHP5: $intersection = call_user_func_array('array_intersect_key', $input);
Build your result by averaging the column from the input array for each key from the intersection.
$output = [];
foreach ($intersection as $key => $value) {
$output[$key] = array_sum(array_column($collection, $key)) / $n;
}
If you really want to completely avoid foreach you can use array_map instead.
$output = array_map(function($key) use ($collection, $n) {
return array_sum(array_column($collection, $key)) / $n;
}, array_keys($intersection));
But in my opinion, this just adds unnecessary complexity.
Note: The values in $intersection will be single values from the first sub-array, but they don't really matter; they're disregarded when generating the output. If it bothers you to have a useless $value variable in the foreach, then you can do foreach (array_keys($intersection) as $key) instead, but I opted for avoiding an unnecessary function call.
Can this problem be resolved with a native PHP function like array_intersect_* in an elegant way?
Well, elegance is in the eye of the developer. If functional-style programming with no new globally-scoped variables equals elegance, then I have something tasty for you. Can a native array_intersect_*() call be leveraged in this task? You bet!
There's a big lack in PHP native functions on intersects - #Maurizio
I disagree. PHP has a broad suite of powerful, optimized, native array_intersect*() and array_diff*() functions. I believe that too few developers are well-acquainted with them all. I've even build a comprehensive demonstration of the different array_diff*() functions (which can be easily inverted to array_intersect*() for educational purposes).
Now, onto your task. First, the code, then the explanation.
Code: (Demo)
var_export(
array_reduce(
array_keys(
array_intersect_ukey(
...array_merge($collection, [fn($a, $b) => $a <=> $b])
)
),
fn($result, $k) => $result + [$k => array_sum(array_column($collection, $k)) / count($collection)],
[]
)
);
The first subtask is to isolate the keys which are present in every row. array_intersect_ukey() is very likely the best qualified tool. The easy part is the custom function -- just write the two parameters with the spaceship in between. The hard part is setting up the variable number of leading input parameters followed by the closure. For this, temporarily merge the closure as an array element onto the collection variable, then spread the parameters into the the native function.
The payload produced by #1 is an array consisting of the associative elements from the first row where the keys were represented in all rows ([3 => 50.0, 5 => 100.0]). To prepare the data for the next step, the keys must be converted to values -- array_keys() is ideal because the float value are of no further use.
Although there is an equal number of elements going into and returning in the final "averaging step", the final result must be a flat associative array -- so array_map() will not suffice. Instead, array_reduce() is better suited. With the collection variable accessible thanks to PHP7.4's arrow function syntax, array_column() can isolate the full column of data then the averaging result pushed as an associative element into the result array.
I guess it could be done like this:
<?php
$intersecting_arrays = Array (
0 => Array ( 'one' => 10, 'two' => 20, 'three' => 50, 'four' => 80, 'five' => 100 ),
1 => Array ( 'three' => 20, 'five' => 20, 'six' => 100, 'seven' => 10 ),
2 => Array ( 'one' => 30, 'three' => 30, 'five' => 10, 'eight' => 10 )
);
$temp = $intersecting_arrays[0];
for($i = 1; $i < count($intersecting_arrays); $i++) {
$temp = array_intersect_key($temp, $intersecting_arrays[$i]);
}
$result = Array();
foreach(array_keys($temp) as $key => $val) {
$value = 0;
foreach($intersecting_arrays as $val1) {
$value+= $val1[$val];
}
$result[$key] = $value / count($intersecting_arrays);
}
print_r($temp);
print_r($result);
https://3v4l.org/j8o75
In this manner it doesn't depend on how much arrays you have.
Here you get the intersection of keys in all arrays and then count an average using collected keys.
Ok, with an unknown number of input arrays, I would definitively go with two nested foreach loops to combine them first - getting an unknown number into array_merge_recursive or similar is going to be difficult.
$input = [
0 => [ 'one' => 10, 'two' => 20, 'three' => 50, 'four' => 80, 'five' => 100],
1 => [ 'three' => 20, 'five' => 20, 'six' => 100, 'seven' => 10],
2 => [ 'one' => 30, 'three' => 30, 'five' => 10, 'eight' => 10]
];
$combined = [];
foreach($input as $array) {
foreach($array as $key => $value) {
$combined[$key][] = $value;
}
}
$averages = array_map(function($item) {
return array_sum($item)/count($item);
}, $combined);
var_dump($averages);
https://3v4l.org/hmtj5
Note that this solution doesn't need to check for array vs single integer in the array_map callback, because unlike array_merge_recursive, $combined[$key][] inside the loops sees to it that even the keys with just one value will have that value in an array.
EDIT:
but keep in mind that not all the keys are going to be taken into account
Ah, ok, so you want averages only for those keys that occurred more than once. That can easily be fixed by filtering the combined array before using array_map on it:
$combined = array_filter($combined, function($v, $k) {
return count($v) != 1;
}, ARRAY_FILTER_USE_BOTH );
Integrated into above solution: https://3v4l.org/dn5ro
EDIT #2
[Andreas' comment] I think "one" should not be in output since it is not in all three arrays.
Ah, I see ... couldn't tell that was the actually desired result even from the example :-) Then my filtering has to be modified a little bit again, and take the number of input arrays into account:
$combined = array_filter($combined, function($v, $k) use($input) {
return count($v) == count($input);
}, ARRAY_FILTER_USE_BOTH );
https://3v4l.org/9H086
You can merge the arrays to one and use array_sum and count() to get the average.
$arr1 = Array ( 'one' => 10, 'two' => 20, 'three' => 50, 'four' => 80, 'five' => 100 );
$arr2 = Array ( 'three' => 20, 'five' => 20, 'six' => 100, 'seven' => 10 );
$arr3 = Array ( 'one' => 30, 'three' => 30, 'five' => 10, 'eight' => 10 );
$array = array_merge_recursive($arr1,$arr2,$arr3);
$key= "two";
If(is_array($array[$key])){
$avg = array_sum($array[$key])/count($array[$key]);
}Else{
$avg = $array[$key];
}
Echo $avg;
https://3v4l.org/pa3PH
Edit to follow $collection array.
Try this then. Use array column to grab the correct key and use array_sum and count to get the average.
$collection = array(
Array ( 'one' => 10, 'two' => 20, 'three' => 50, 'four' => 80, 'five' => 100 ),
Array ( 'three' => 20, 'five' => 20, 'six' => 100, 'seven' => 10 ),
Array ( 'one' => 30, 'three' => 30, 'five' => 10, 'eight' => 10 ));
$key= "three";
$array = array_column($collection, $key);
If(count($array) != 1){
$avg = array_sum($array)/count($array);
}Else{
$avg = $array[0];
}
Echo $avg;
https://3v4l.org/QPsiS
Final edit.
Here I loop through the first subarray and use array column to find all the matching keys.
If the count of keys is the same as the count of collection the key exsists in all subarrays and should be "saved".
$collection = array(
Array ( 'one' => 10, 'two' => 20, 'three' => 50, 'four' => 80, 'five' => 100 ),
Array ( 'three' => 20, 'five' => 20, 'six' => 100, 'seven' => 10 ),
Array ( 'one' => 30, 'three' => 30, 'five' => 10, 'eight' => 10 ));
Foreach($collection[0] as $key => $val){
$array = array_column($collection, $key);
If(count($array) == count($collection)){
$avg[$key] = array_sum($array)/count($array);
}
}
Var_dump($avg);
https://3v4l.org/LfktH
i have array something like this
$arr =
['0' =>
['0' => 'zero',
'1' => 'test',
'2' =>'testphp',
'test'=>'zero',
'test1'=>'test',
'test2'=>'testphp'],
'1' =>
['0' => 'z',
'1' => 'x',
'2' =>'c',
'test'=>'z',
'test1'=>'x',
'test2'=>'c']
];
and 0,1,2 is this same as test,test1,test2. I need remove keys where is string like test,test1,test2.
I know the way
foreach($arr as $a){
unset($arr['test']);
unset($arr['test1']);
unset($arr['test2']);
}
but it is possible find keys without specifying the exact name, because i want only number keys.
A solution would be:
Assuming you know it will only have 2 layers.
$arr =
['0' =>
['0' => 'zero',
'1' => 'test',
'2' =>'testphp',
'test'=>'zero',
'test1'=>'test',
'test2'=>'testphp'],
'1' =>
['0' => 'z',
'1' => 'x',
'2' =>'c',
'test'=>'z',
'test1'=>'x',
'test2'=>'c']
];
foreach($arr as $parentKey=>$arrayItem){
foreach($arrayItem as $key=>$subArrayItem){
if(!is_int($key)){
unset($arr[$parentKey][$key]);
}
}
}
var_dump($arr);
Why is it though that such arrays have been generated?
edit: after reading Valdorous answer realized it is multidimensional array. the following should handle recursively a multi-dimensional array.
call the function (see below)
remove_non_numeric_keys($arr)
function remove_non_numeric_keys($arr)
{
foreach($arr as $key=>$val)
{
if(!is_numeric($key)) // if not numeric unset it regardless if it is an array or not
{
unset($arr[$key]);
}else{
if(is_array($val) // if it is an array recursively call the function to check the values in it
{
remove_non_numeric_keys($val);
}
}
}
}
This should remove only non-numeric keys.
http://php.net/manual/en/function.is-numeric.php
Hope it helps
I've created a method that allows me to assign keys to rows of values, and appends extra keys and values.
It adds all the new keys to a keys array, then adds all new values to the values array, and then combines all the keys and values.
How can I shrink it to make it smaller and more efficient?
$valores = array(array("1","1","1","1"),array("2","2","2","2"));//array of values
$keys = array('k1','k2','k3','k4'); //array of keys
$id = array('SpecialKey' => 'SpecialValue');//new array of items I want to add
function formatarArray($arrValores,$arrKeys,$identificadores){
foreach($identificadores as $k => $v){
array_push($arrKeys, $k);
}
foreach($arrValores as $i => $arrValor)
{
foreach($identificadores as $k => $v){
array_push($arrValor, $v);
}
$arrValores[$i] = array_combine($arrKeys, $arrValor);
}
var_export($arrValores);
}
Output:
array (
0 =>
array (
'k1' => '1',
'k2' => '1',
'k3' => '1',
'k4' => '1',
'SpecialKey' => 'SpecialValue',
),
1 =>
array (
'k1' => '2',
'k2' => '2',
'k3' => '2',
'k4' => '2',
'SpecialKey' => 'SpecialValue',
),
)
Viper-7(code debug):
http://viper-7.com/hbE1YF
function formatarArray($arrValores, $arrKeys, $identificadores)
{
foreach ($arrValores as &$arr)
$arr = array_merge(array_combine($arrKeys, $arr), $identificadores);
print_r($arrValores);
}
Could even be done in one line...
function formatarArray($arrValores, $arrKeys, $identificadores)
{
print_r(array_map(function ($arr) use ($arrKeys, $identificadores) { return array_merge(array_combine($arrKeys, $arr), $identificadores); }, $arrValores));
}
As a modernized form of #havenard's answer, I'd use PHP7.4's arrow function syntax to avoid the need for use() and I would use the array union operator (+) to avoid the iterated array_merge() calls. The array union operator is appropriate because it is adding an associative array to another array.
Code: (Demo)
var_export(
array_map(
fn($row) => array_combine($keys, $row) + $id,
$valores
)
);
I have an array like below
$old = array(
'a' => 'blah',
'b' => 'key',
'c' => 'amazing',
'd' => array(
0 => 'want to replace',
1 => 'yes I want to'
)
);
I have another array having keys to replace with key information.
$keyReplaceInfoz = array('a' => 'newA', 'b' => 'newB', 'c' => 'newC', 'd' => 'newD');
I need to replace all keys of array $old with respective values in array $keyReplaceInfo.
Output should be like this
$old = array(
'newA' => 'blah',
'newB' => 'key',
'newC' => 'amazing',
'newD' => array(
0 => 'want to replace',
1 => 'yes I want to'
)
);
I had to do it manually as below. I am expecting better option. can anyone suggest better way to accomplish this?
$new = array();
foreach ($old as $key => $value)
{
$new[$keyReplaceInfoz[$key]] = $value;
}
I know this can be more simpler.
array_combine(array_merge($old, $keyReplaceInfoz), $old)
I think this looks easier than what you posed.
array_combine(
['newKey1', 'newKey2', 'newKey3'],
array_values(['oldKey1' => 1, 'oldKey2' => 2, 'oldKey3' => 3])
);
This should do the trick as long as you have the same number of values and the same order.
IMO using array_combine, array_merge, even array_intersect_key is overkill.
The original code is good enough, and very fast.
Adapting #shawn-k solution, here is more cleaner code using array_walk, it will only replace desired keys, of course you can modify as per your convenience
array_walk($old, function($value,$key)use ($keyReplaceInfoz,&$old){
$newkey = array_key_exists($key,$keyReplaceInfoz)?$keyReplaceInfoz[$key]:false;
if($newkey!==false){$old[$newkey] = $value;unset($old[$key]);
}
});
print_r($old);
I just solved this same problem in my own application, but for my application $keyReplaceInfoz acts like the whitelist- if a key is not found, that whole element is removed from the resulting array, while the matching whitelisted keys get translated to the new values.
I suppose you could apply this same algorithm maybe with less total code by clever usage of array_map (http://php.net/manual/en/function.array-map.php), which perhaps another generous reader will do.
function filterOldToAllowedNew($key_to_test){
return isset($keyReplaceInfoz[$key_to_test])?$keyReplaceInfoz[$key_to_test]:false;
}
$newArray = array();
foreach($old as $key => $value){
$newkey = filterOldToAllowedNew($key);
if($newkey){
$newArray[$newkey] = $value;
}
}
print_r($newArray);
This question is old but since it comes up first on Google I thought I'd add solution.
// Subject
$old = array('foo' => 1, 'baz' => 2, 'bar' => 3));
// Translations
$tr = array('foo'=>'FOO', 'bar'=>'BAR');
// Get result
$new = array_combine(preg_replace(array_map(function($s){return "/^$s$/";},
array_keys($tr)),$tr, array_keys($old)), $old);
// Output
print_r($new);
Result:
Array
(
[FOO] => 1
[baz] => 2
[BAR] => 3
)
This the solution i have implemented for the same subject:
/**
* Replace keys of given array by values of $keys
* $keys format is [$oldKey=>$newKey]
*
* With $filter==true, will remove elements with key not in $keys
*
* #param array $array
* #param array $keys
* #param boolean $filter
*
* #return $array
*/
function array_replace_keys(array $array,array $keys,$filter=false)
{
$newArray=[];
foreach($array as $key=>$value)
{
if(isset($keys[$key]))
{
$newArray[$keys[$key]]=$value;
}
elseif(!$filter)
{
$newArray[$key]=$value;
}
}
return $newArray;
}
This works irrespective of array order & array count. Output order & value will be based on replaceKey.
$replaceKey = array('a' => 'newA', 'b' => 'newB', 'c' => 'newC', 'd' => 'newD', 'e' => 'newE','f'=>'newF');
$array = array(
'a' => 'blah',
'd' => array(
0 => 'want to replace',
1 => 'yes I want to'
),
'noKey'=>'RESIDUAL',
'c' => 'amazing',
'b' => 'key',
);
$filterKey = array_intersect_key($replaceKey,$array);
$filterarray = array_intersect_key(array_merge($filterKey,$array),$filterKey);
$replaced = array_combine($filterKey,$filterarray);
//output
var_export($replaced);
//array ( 'newA' => 'blah', 'newB' => 'key', 'newC' => 'amazing', 'newD' => array ( 0 => 'want to replace', 1 => 'yes I want to' ) )
If you're looking for a recursive solution to use on a multidimensional array, have a look at the below method. It will replace all keys requested, and leave all other keys alone.
/**
* Given an array and a set of `old => new` keys,
* will recursively replace all array keys that
* are old with their corresponding new value.
*
* #param mixed $array
* #param array $old_to_new_keys
*
* #return array
*/
function array_replace_keys($array, array $old_to_new_keys)
{
if(!is_array($array)){
return $array;
}
$temp_array = [];
$ak = array_keys($old_to_new_keys);
$av = array_values($old_to_new_keys);
foreach($array as $key => $value){
if(array_search($key, $ak, true) !== false){
$key = $av[array_search($key, $ak)];
}
if(is_array($value)){
$value = array_replace_keys($value, $old_to_new_keys);
}
$temp_array[$key] = $value;
}
return $temp_array;
}
Using OP's example array:
$old = array(
'a' => 'blah',
'b' => 'key',
'c' => 'amazing',
'd' => array(
0 => 'want to replace',
1 => 'yes I want to'
)
);
$replace = ["a" => "AA", 1 => 11];
var_export(array_replace_keys($old, $replace));
Gives the following output:
array (
'AA' => 'blah',
'b' => 'key',
'c' => 'amazing',
'd' =>
array (
0 => 'want to replace',
11 => 'yes I want to',
),
)
DEMO
Inspired by the following snippet.
This uses #Summoner's example but keeps #Leigh's hint in mind:
$start = microtime();
$array = [ "a" => 1, "b" => 2, "c" => 3 ];
function array_replace_key($array, $oldKey, $newKey) {
$keys = array_keys($array);
$idx = array_search($oldKey, $keys);
array_splice($keys, $idx, 1, $newKey);
return array_combine($keys, array_values($array));
}
print_r(array_replace_key($array, "b", "z"));
<?php
$new = array();
foreach ($old as $key => $value)
{
$new[$keyReplaceInfoz][$key] = $value;
}
?>
Imagine that I want to create an array from another array like this:
$array = array('bla' => $array2['bla'],
'bla2' => $array2['bla2'],
'foo' => $array2['foo'],
'Alternative Bar' => $array['bar'],
'bar' => $array2['bar']);
What is the best way to test either the $array2 has that index I'm passing to the other array, without using an if for each index I want to add.
Note that the key from the $array can be different from the $array2
What I did was, creating a template to the array with the keys that I want e.g.
$template = array('key1', 'key2', 'key3');
Then, I would merge the template with the other array, if any key was missing in the second array, then the value of the key would be null, this way I don't have the problem of warnings telling me about offset values.
$template = array('key1', 'key2', 'key3');
$array = array_merge($template, $otherarray);
if i understood right...
$a = array('foo' => 1, 'bar' => 2, 'baz' => 3);
$keys = array('foo', 'baz', 'quux');
$result = array_intersect_key($a, array_flip($keys));
this will pick only existing keys from $a
A possibility would be:
$array = array(
'bla' => (isset($array2['bla']) ? $array2['bla'] : ''),
'bla2' => (isset($array2['bla2']) ? $array2['bla2'] : ''),
'foo' => (isset($array2['foo']) ? $array2['foo'] : ''),
'xxx' => (isset($array2['yyy']) ? $array2['yyy'] : ''),
'bar' => (isset($array2['bar']) ? $array2['bar'] : '')
);
If this shoud happen more dynamically, I would suggest to use array_intersect_key, like soulmerge posted. But that approach would have the tradeoff that only arrays with the same keys can be used.
Since your keys in the 2 arrays can vary, you could build something half-dynamic using a mapping array to map the keys between the arrays. You have at least to list the keys that are different in your arrays.
//key = key in $a, value = key in $b
$map = array(
'fooBar' => 'bar'
);
$a = array(
'fooBar' => 0,
'bla' => 0,
'xyz' => 0
);
$b = array(
'bla' => 123,
'bar' => 321,
'xyz' => 'somevalue'
);
foreach($a as $k => $v) {
if(isset($map[$k]) && isset($b[$map[$k]])) {
$a[$k] = $b[$map[$k]];
} elseif(isset($b[$k])){
$a[$k] = $b[$k];
}
}
That way you have to only define the different keys in $map.
You want to extract certain keys from array1 and set non-existing keys to the empty string, if I understood you right. Do it this way:
# Added lots of newlines to improve readability
$array2 = array_intersect_key(
array(
'bla' => '',
'bla2' => '',
'foo' => '',
# ...
),
$array1
);
Perhaps this...
$array = array();
foreach ( $array2 as $key=>$val )
{
switch ( $key )
{
case 'bar':
$array['Alternative bar'] = $val;
break;
default:
$array[$key] = $val;
break;
}
}
For any of the "special" array indexes, use a case clause, otherwise just copy the value.