How to mutate this array within `array_reduce` - php

Is there a way to mutate an array using array_reduce in PHP?
I'm trying to do something like this:
Given some ordered list of ids:
$array = [["id" => 1], ["id" => 13], ["id" => 4]];
And a tree that has a subtree matching the corresponding ids:
$tree = [
"id" => 2334,
"children" => [
[
"id" => 111,
"children" => []
],
[
"id" => 1, // <- this is a match
"children" => [
[
"id" => 13, // <- this is a match
"children" => [
[
"id" => 4, // <- this is a match
"children" => []
],
[
"id" => 225893,
"children" => []
],
[
"id" => 225902,
"children" => []
]
]
]
]
]
]
];
How can I mutate the arrays in that subtree?
I'm currently trying to use array_reduce to walk down the tree and mutate it. However, the mutation isn't being applied to the originally passed in $tree.
array_reduce($array, function (&$acc, $item) {
$index = array_search($item['id'], array_column($acc['children'], 'id'));
$acc['children'][$index]['mutated'] = true; // mutation here
return $acc['children'][$index];
}, $tree);
echo "<pre>";
var_dump($tree); // $tree is unchanged here
echo "</pre>";
Why is $tree not mutated after the running above array_reduce?
Is there a way to use foreach in this case?

I think this function will do what you want. It recurses down $tree, looking for id values that are in $array and setting the mutation flag for those children:
function mutate(&$tree, $array) {
if (in_array($tree['id'], array_column($array, 'id'))) {
$tree['mutated'] = true;
}
foreach ($tree['children'] as &$child) {
mutate($child, $array);
}
}
mutate($tree, $array);
var_export($tree);
Output:
array (
'id' => 2334,
'children' => array (
0 => array (
'id' => 111,
'children' => array ( ),
),
1 => array (
'id' => 1,
'children' => array (
0 => array (
'id' => 13,
'children' => array (
0 => array (
'id' => 4,
'children' => array ( ),
'mutated' => true,
),
1 => array (
'id' => 225893,
'children' => array ( ),
),
2 => array (
'id' => 225902,
'children' => array ( ),
),
),
'mutated' => true,
),
),
'mutated' => true,
),
),
)
Demo on 3v4l.org

Related

Where two id matching, erase the value in two arrays

I have two arrays like this :
$data = [
0 => ['id' => 123, 'value' => 'Text'],
1 => ['id' => 124, 'value' => 12]
];
$data2 = [
"custom" => [
0 => ['id' => 123, 'name' => 'Custom1', 'value' => null],
1 => ['id' => 124, 'name' => 'Custom2', 'value' => null]
]
];
I would like to put the value in $data in the value of $data2 instead of "null" values where the ID are matching.
How can I do this ?
I try to do this but does not work :
foreach ($data2['custom'] as $d) {
foreach ($data as $cf) {
if ($cf['id'] == $d['id']):
$cf['value'] = $d['value'];
endif;
}
}
Thanks
Here is a suggestion, first transform the $data array into an array with the id as a key to make the following process simple.
Then process over the $data2 array and in the foreach us the & reference indicator, so that amendments inside the foreach loop are applied to the original $data2 array and not the copy of the $data2 array that would normally be created as part of a foreach loop
$data = [ ['id' => 123, 'value' => 'Text'],['id' => 124, 'value' => 12] ];
$data2 = [ "custom" => [
['id' => 123, 'name' => 'Custom1', 'value' => null],
['id' => 124, 'name' => 'Custom2', 'value' => null]]
];
// transform $data into an array with a useful key
foreach( $data as $d){
$useful[$d['id']] = $d['value'];
}
foreach ( $data2['custom'] as &$data ) {
// first check that the id exists in the new useful array
if ( isset($useful[$data['id']]) ) {
// if it does apply the value to the data2 array
$data['value'] = $useful[$data['id']];
}
}
print_r($data2);
RESULT
Array
(
[custom] => Array
(
[0] => Array
(
[id] => 123
[name] => Custom1
[value] => Text
)
[1] => Array
(
[id] => 124
[name] => Custom2
[value] => 12
)
)
)
In reply to your comment about doing it without using the reference in the loop, yes like this
foreach ( $data2['custom'] as $idx => $data ) {
if ( isset($useful[$data['id']]) ) {
$data2['custom'][$idx]['value'] = $useful[$data['id']];
}
}
Check out this one:
$data = [
0 => ['id' => 123, 'value' => 'Text'],
1 => ['id' => 124, 'value' => 12]
];
$data2 = [
"custom" => [
0 => ['id' => 123, 'name' => 'Custom1', 'value' => null],
1 => ['id' => 124, 'name' => 'Custom2', 'value' => null]
]
];
foreach ($data2['custom'] as $d) {
foreach ($data as $key => $cf) {
if ($cf['id'] == $d['id']):
$data2['custom'][$key]['value'] = $cf['value'];
endif;
}
}
print_r($data2);
You could try to iterate the first array and put the values of it inside the second. If they're always the same size.
Using a foreach go trough the elements on the first array and get the values:
$data = [
0 => ['id' => 123, 'value' => 'Text'],
1 => ['id' => 124, 'value' => 12
]
];
$data2 = [
"custom" => [
0 => ['id' => 123, 'name' => 'Custom1', 'value' => null],
1 => ['id' => 124, 'name' => 'Custom2', 'value' => null]
]
];
foreach ($data as $key=>$singleData){
$data2['custom'][$key]['value'] = $singleData['value'];
}
var_dump($data2);
The ouptup from var_dump() will be:
array(1) {
["custom"]=>
array(2) {
[0]=>
array(3) {
["id"]=>
int(123)
["name"]=>
string(7) "Custom1"
["value"]=>
string(4) "Text"
}
[1]=>
array(3) {
["id"]=>
int(124)
["name"]=>
string(7) "Custom2"
["value"]=>
int(12)
}
}
}

Use recursion to accumulate rows without depending on class property variable

Having this array :
[
"id" => 5,
"name" => "Item 5",
"all_parents" => [
"id" => 4,
"name" => "Item 4",
"all_parents" => [
"id" => 3,
"name" => "Item 3",
"all_parents" => [
"id" => 2,
"name" => "Item 2",
"all_parents" => [
"id" => 1,
"name" => "Item 1",
"all_parents" => null
]
]
]
]
]
I created a recursive php function that transform that array to this:
[
["id" => 1, "name" => "Item 1"],
["id" => 2, "name" => "Item 2"],
["id" => 3, "name" => "Item 3"],
["id" => 4, "name" => "Item 4"],
["id" => 5, "name" => "Item 5"],
]
The code is this:
private array $breadcrumb = [];
private function generateBreadcrumb($structure) : array
{
if($structure) {
$this->breadcrumb[] = array(
"id" => $structure['id'],
"name" => $structure['name'],
);
$this->generateBreadcrumb($structure['all_parents'] ?? []);
}
return array_reverse($this->breadcrumb);
}
How can I redesign this method without depending on class property $breadcrumb?
By following your initial code, you could do:
function generateBreadcrumb($structure, &$output = []) : array
{
if ($structure) {
$output[] = array(
"id" => $structure['id'],
"name" => $structure['name'],
);
$this->generateBreadcrumb($structure['all_parents'] ?? [], $output);
}
return array_reverse($output);
}
However it could be improved, at least by avoiding to call array_reverse() each time, but only for the root call.
Instead of implementing a recursive function there is the possibility of using the built-in array_walk_recursive function:
$arr = [
'id' => 5,
'name' => 'Item 5',
'all_parents' => [
'id' => 4,
'name' => 'Item 4',
'all_parents' => [
'id' => 3,
'name' => 'Item 3',
'all_parents' => [
'id' => 2,
'name' => 'Item 2',
'all_parents' => [
'id' => 1,
'name' => 'Item 1',
'all_parents' => null
]
]
]
]
];
function generateBreadcrumb($structure): array {
$retval = [];
array_walk_recursive($structure, function ($item, $key) use (&$retval) {
if ($key === 'id') {
$retval[] = [$key => $item];
} elseif ($key === 'name') {
$retval[array_key_last($retval)][$key] = $item;
}
});
return array_reverse($retval);
}
$result = generateBreadcrumb($arr);
Note that array_walk_recursive only visits leafs, so with the exception of the innermost 'all_parents', the other ones are not visited.
A none-recursive version would be this:
function generateBreadcrumb(array $arr): array {
$retval = [];
$temp = &$arr;
do {
$retval[] = [ 'id' => $temp['id'], 'name' => $temp['name'] ];
$temp = &$temp['all_parents'];
} while ($temp !== null);
return array_reverse($retval);
}
You can accumulate the indeterminate-depth data by merging as you recurse the tree. You do not need to introduce any new variables to carry the data while recursing nor do you need to array_reverse() the returned data.
The below technique will prioritize recursion while $structure['all_parents'] is truthy (not null) and cease recursion once it encounters the null all_parents value in the deepest subarray. From the bottom, the id and name elements will be accessed and merged into the empty or accumulated array of row data.
Code: (Demo)
class Recursing
{
public function generateBreadcrumb(array $structure): array
{
return array_merge(
$structure['all_parents']
? $this->generateBreadcrumb($structure['all_parents'])
: [],
[
['id' => $structure['id'], 'name' => $structure['name']]
]
);
}
}
$test = new Recursing;
var_export($test->generateBreadcrumb($arr));
Output:
array (
0 =>
array (
'id' => 1,
'name' => 'Item 1',
),
1 =>
array (
'id' => 2,
'name' => 'Item 2',
),
2 =>
array (
'id' => 3,
'name' => 'Item 3',
),
3 =>
array (
'id' => 4,
'name' => 'Item 4',
),
4 =>
array (
'id' => 5,
'name' => 'Item 5',
),
)

Merge/Flatten 3rd level data to create an array of arrays

I have an array with 3 levels and I'd like to merge/flatten all 3rd level subarrays into one subarray/row on the 2nd level.
$array = [
[
'User' => [
'id' => 57341,
'updated' => null,
'userId' => 57341,
'value' => null,
'lat' => 53.4537812,
'lon' => -2.1792437,
],
[
'feed_likes' => 'NA',
'category_idx' => -1,
'type' => 'User'
]
],
[
'User' => [
'id' => 57336,
'updated' => null,
'userId' => 57336,
'value' => null,
'lat' => 53.473684,
'lon' => -2.2399827,
],
[
'feed_likes' => 'NA',
'category_idx' => -1,
'type' => 'User'
]
],
];
The deep User-keyed subarrays (having 6 elements) should be merged with its sibling/indexed subarray (having 3 elements) to form a 9-element row on the second level.
Desired result:
[
[
'id' => 57341,
'updated' => null,
'userId' => 57341,
'value' => null,
'lat' => 53.4537812,
'lon' => -2.1792437,
'feed_likes' => 'NA',
'category_idx' => -1,
'type' => 'User'
],
[
'id' => 57336,
'updated' => null,
'userId' => 57336,
'value' => null,
'lat' => 53.473684,
'lon' => -2.2399827,
'feed_likes' => 'NA',
'category_idx' => -1,
'type' => 'User'
]
]
You can use splat ... operator with array_merge
foreach($a as $child){
$flatten[] = array_merge(...$child);
}
Working example :- https://3v4l.org/HkUh6
To merge and flatten the 3rd level data sets into consolidated 2nd level rows with functional style programming, make iterated calls of array_merge() which receive all 3rd level payloads at once. The spread operator (...) is a concise technique used to unpack multiple elements in an array. A special consideration is needed for this case because spreading elements which have non-numeric keys will cause code breakage. To overcome this, simply call array_values() to "index" the array (replace all keys with sequenial numbers) before spreading.
Code: (Demo)
var_export(
array_map(
fn($rows) => array_merge(...array_values($rows)),
$array
)
);
Slightly different to your example output, but you can merge your inner arrays.
<?php
$data =
[
[
'animal' => [
'type' => 'fox'
],
[
'colour' => 'orange'
]
],
[
'animal' => [
'type' => 'panda'
],
[
'colour' => 'black and white'
]
]
];
$result =
array_map(
function($v) {
return array_merge($v['animal'], $v[0]);
},
$data
);
var_export($result);
Output:
array (
0 =>
array (
'type' => 'fox',
'colour' => 'orange',
),
1 =>
array (
'type' => 'panda',
'colour' => 'black and white',
),
)
If I understood you correctly you need to merge User array and in this case with the second array in that key. it that case something like this should work
foreach($array as $key=>$deep1){
$newArray = [];
foreach($deep1 as $deep2){
$newArray = array_merge($newArray,$deep2)
}
$array[$key] = $newArray;
}
Did II understand your question correctly?

Recursively Create an Array from another Array

I am trying to make a multi-dimensional array build an array path adding the hr field so it looks like this:
I just can't figure out how to add the totals, nor create a sub-array so the dot notation in an option too. My goal is to get something like this:
[1] => [1][2][1][5][0][6] = 35 (the second child path "1")
[1] => [1][2][1][5][0][7] = 25
or Something like this:
array (
[children.0.children.0.children.0.total] = 20
[children.0.children.1.children.1.total] = 35
// etc
)
The complicated part is that it goes in different directions and I want to know what is the highest and lowest total based on the path:
==> Run Code Here or Copy/Paste
// -------------
// The Flattener
// -------------
function doit($myArray) {
$iter = new RecursiveIteratorIterator(new RecursiveArrayIterator($myArray));
$result = array();
foreach ($iter as $leafKey => $leafValue) {
$keys = array();
foreach (range(0, $iter->getDepth()) as $depth) {
$keys[] = $iter->getSubIterator($depth)->key();
}
$result[ join('.', $keys) ] = $leafValue;
}
return $result;
}
// -------------
// Example Tree
// -------------
$tree = [
'id' => 1,
'type' => 'note',
'data' => [],
'children' => [
[
'id' => 2,
'type' => 'wait',
'data' => [
'hr' => 10,
],
'children' => [
[
'id' => 3,
'type' => 'wait',
'data' => [
'hr' => 10,
],
'children' => [
'id' => 4,
'type' => 'exit',
'data' => [],
'children' => []
]
],
[
'id' => 5,
'type' => 'note',
'data' => [
'hr' => 10,
],
'children' => [
[
'id' => 6,
'type' => 'wait',
'data' => [
'hr' => 10,
],
'children' => []
],
[
'id' => 7,
'type' => 'exit',
'data' => [],
'children' => []
],
]
]
],
]
]
];
$result = doit($tree);
print_r($result);
This seems to work, I found it somewhere googling all day.
array_reduce(array_reverse($keys), function($parent_array, $key) {
return $parent_array ? [$key => $parent_array] : [$key];
}, null);

PHP Merge by values in same array

So I have this array in PHP.
$arr = [
[ 'sections' => [1], 'id' => 1 ],
[ 'sections' => [2], 'id' => 1 ],
[ 'sections' => [3], 'id' => NULL ],
[ 'sections' => [4], 'id' => 4 ],
[ 'sections' => [5], 'id' => 4 ],
[ 'sections' => [6], 'id' => 4 ]
];
I want to merge on 'id' and get something like
$arr = [
[ 'sections' => [1, 2], 'id' => 1 ],
[ 'sections' => [3], 'id' => NULL ],
[ 'sections' => [4, 5, 6], 'id' => 4 ]
];
Just struggling to get my head around this one. Any Ideas
I've created this quick function that might work for you
<?php
// Your array
$arr = array(
array( 'elem1' => 1, 'elem2' => 1 ),
array( 'elem1' => 2, 'elem2' => 1 ),
array( 'elem1' => 3, 'elem2' => NULL ),
array( 'elem1' => 4, 'elem2' => 4 ),
array( 'elem1' => 5, 'elem2' => 4 ),
array( 'elem1' => 6, 'elem2' => 4 )
);
print_r($arr);
function mergeBy($arr, $elem2 = 'elem2') {
$result = array();
foreach ($arr as $item) {
if (empty($result[$item[$elem2]])) {
// for new items (elem2), just add it in with index of elem2's value to start
$result[$item[$elem2]] = $item;
} else {
// for non-new items (elem2) merge any other values (elem1)
foreach ($item as $key => $val) {
if ($key != $elem2) {
// cast elem1's as arrays, just incase you were lazy like me in the declaration of the array
$result[$item[$elem2]][$key] = $result[$item[$elem2]][$key] = array_merge((array)$result[$item[$elem2]][$key],(array)$val);
}
}
}
}
// strip out the keys so that you dont have the elem2's values all over the place
return array_values($result);
}
print_r(mergeBy($arr));
?>
Hopefully it'll work for more than 2 elements, and you can choose what to sort on also....

Categories