How to recursively sort associative array - php

I'd like to sort the following associative array:
$tree = [
"id" => 245974,
"children" => [
[
"id" => 111
],
[
"id" => 245982,
"children" => [
[
"id" => 246093,
"children" => [
[
"id" => 225892
],
[
"id" => 225893
],
[
"id" => 225902
]
]
]
]
]
]
];
Desired sort order after the "search value" of id => 225902:
[
"id" => 245974,
"children" => [
[
"id" => 245982, // <-- this is moved up
"children" => [
[
"id" => 246093,
"children" => [
[
"id" => 225902 // <-- this is moved up
],
[
"id" => 225892
],
[
"id" => 225893
]
]
]
]
],
[
"id" => 111
]
]
];
What I've tried:
<?php
$category_id = 225902;
function custom_sort(&$a, &$b) {
global $category_id;
if ($a['id'] === $category_id) {
return -1;
}
if ($b['id'] === $category_id) {
return 1;
}
if (array_key_exists('children', $a)) {
if (usort($a['children'], "custom_sort")) {
return -1;
}
}
if (array_key_exists('children', $b)) {
if (usort($b['children'], "custom_sort")) {
return 1;
}
}
return 0;
}
function reorder_tree($tree) {
usort($tree['children'], "custom_sort");
return $tree;
}
echo "<pre>";
var_dump(reorder_tree($tree));
echo "</pre>";
However, that returns:
[
"id" => 245974,
"children" => [
[
"id" => 245982, // <- this is moved up
"children" => [
[
"id" => 246093,
"children" => [
[
"id" => 225892
],
[
"id" => 225893
],
[
"id" => 225902 // <- this is *not* moved up
]
]
]
]
],
[
"id" => 111
],
]
];
How would I be able to also sort the children arrays?

Great attempt and very much on the right track. The problem with recursion in the comparator is that usort will not call the comparator function when the array length is 1, so whether or not you explore the whole tree is at the whim of usort. This will abandon id => 245982's branch of the tree.
The solution is to avoid recursing in the usort's comparator function directly. Rather, use a regular recursive function that calls usort as needed, namely, the current array or a child array contains the target id. I use a separate array to keep track of which elements should be moved forward, but you can break out of the loop and splice/unshift a single element to the front if you prefer.
We can also make $category_id a parameter to the function.
Here's one approach:
function reorder_tree_r(&$children, $target) {
$order = [];
$should_sort = false;
foreach ($children as $i => &$child) {
$order[$i] = false;
if (array_key_exists("children", $child) &&
reorder_tree_r($child["children"], $target) ||
$child["id"] === $target) {
$order[$i] = true;
$should_sort = true;
}
}
if ($should_sort) {
$priority = [];
$non_priority = [];
for ($i = 0; $i < count($children); $i++) {
if ($order[$i]) {
$priority[]= $children[$i];
}
else {
$non_priority[]= $children[$i];
}
}
$children = array_merge($priority, $non_priority);
}
return $should_sort;
}
function reorder_tree($tree, $target) {
if (!$tree || !array_key_exists("children", $tree)) {
return $tree;
}
reorder_tree_r($tree["children"], $target);
return $tree;
}
var_export(reorder_tree($tree, 225902));
Output:
array (
'id' => 245974,
'children' =>
array (
0 =>
array (
'id' => 245982,
'children' =>
array (
0 =>
array (
'id' => 246093,
'children' =>
array (
0 =>
array (
'id' => 225902,
),
1 =>
array (
'id' => 225892,
),
2 =>
array (
'id' => 225893,
),
),
),
),
),
1 =>
array (
'id' => 111,
),
),

Related

PHP array diff: merge arrays recursively and show 'new' vs 'old' values in result

I would like to merge two arrays to compare old vs new values. For example, $arr1 is old values $arr2 is new values.
In case when the data is deleted $arr2 is an empty array. Example:
$arr1 = [
"databases" => [
0 => [
"id" => 1
"name" => "DB1"
"slug" => "db1"
"url" => "https://www.db1.org"
]
]
];
$arr2 = [];
For this my expected output after merge is
$merged = [
"databases" => [
0 => [
"id" => [
'old' => 1,
'new' => null
],
"name" => [
'old' => "DB1",
'new' => null
],
"slug" => [
'old' => "db1",
'new' => null
],
"url" => [
'old' => "https://www.db1.org",
'new' => null
],
]
]
];
if arr2 is different then the values should be present in the new field instead of null.
For example:
$arr1 = [
"databases" => [
0 => [
"id" => 1
"name" => "DB1"
"slug" => "db1"
"url" => "https://www.db1.org"
]
]
];
$arr2 = [
"databases" => [
0 => [
"id" => 5
"name" => "DB2"
"slug" => "db2"
"url" => "https://www.db2.com"
]
]
];
expected output:
$merged = [
"databases" => [
0 => [
"id" => [
'old' => 1,
'new' => 5
],
"name" => [
'old' => "DB1",
'new' => "DB2"
],
"slug" => [
'old' => "db1",
'new' => "db2"
],
"url" => [
'old' => "https://www.db1.org",
'new' => "https://www.db2.com"
],
]
]
];
Case 3 is when $arr1 is empty but $arr2 is populated:
$arr1 = [];
$arr2 = [
"databases" => [
0 => [
"id" => 1
"name" => "DB1"
"slug" => "db1"
"url" => "https://www.db1.org"
]
]
];
and the expected output is:
$merged = [
"databases" => [
0 => [
"id" => [
'old' => null,
'new' => 1
],
"name" => [
'old' => null,
'new' => "DB1"
],
"slug" => [
'old' => null,
'new' => "db1"
],
"url" => [
'old' => null,
'new' => "https://www.db1.org"
],
]
]
];
The inbuilt php functions cannot format the data in old vs new format so was wondering how to go about this? Any solutions/suggestions would be appreciated.
Update
Here is what I had tried before:
I had tried simple array_merge_recursive but it does not store the source array. So if you have $arr1 key not there, the final merged array will only have one value.
I tried some more recursive functions late in the night but failed so in essence didn't have anything to show for what I had tried. However, this morning, I came up with the solution and have posted it as an answer in case anyone needs to use it.
Interesting question, as long as a (non-empty) array on one side means to traverse into it and any skalar or null is a terminating node (while if any of old or new being an array would enforce traversing deeper so dropping the other non-array value):
It works by mapping both old and new on one array recursively and when the decision is to make to traverse to offer null values in case a keyed member is not available while iterating over the super set of the keys of both while null would represent no keys:
$keys = array_unique(array_merge(array_keys($old ?? []), array_keys($new ?? [])));
$merged = [];
foreach ($keys as $key) {
$merged['old'] = $old[$key] ?? null;
$merged['new'] = $new[$key] ?? null;
}
This then can be applied recursively, for which I found it is easier to handle both $old and $new as ['old' => $old, 'new' => $new] for that as then the same structure can be recursively merged:
function old_and_new(array $old = null, array $new = null): array
{
$pair = get_defined_vars();
$map =
static fn(callable $map, array $arrays): array => in_array(true, array_map('is_array', $arrays), true)
&& ($parameter = array_combine($k = array_keys($arrays), $k))
&& ($keys = array_keys(array_flip(array_merge(...array_values(array_map('array_keys', array_filter($arrays, 'is_array'))))))
)
? array_map(
static fn($key) => $map($map, array_map(static fn($p) => $arrays[$p][$key] ?? null, $parameter)),
array_combine($keys, $keys)
)
: $arrays;
return $map($map, $pair);
}
print_r(old_and_new(new: $arr2));
Online demo: https://3v4l.org/4KdLs#v8.0.9
The inner technically works with more than two arrays, e.g. three. And it "moves" the array keys upwards, similar to a transpose operation. Which btw. there is a similar question (but only similar, for the interesting part in context of your question it is not answered and my answer here doesn't apply there directly):
Transposing multidimensional arrays in PHP
After reviewing my own code here is the solution I came up with. I am posting it here in case someone else needs a solution for this:
/**
* Function to merge old and new values to create one array with all entries
*
* #param array $old
* #param array $new
* #return void
*/
function recursiveMergeOldNew($old, $new) {
$merged = array();
$array_keys = array_keys($old) + array_keys($new);
if($array_keys) {
foreach($array_keys as $key) {
$oldChildArray = [];
$newChildArray = [];
if(isset($old[$key])) {
if(!is_array($old[$key])) {
$merged[$key]['old'] = $old[$key];
} else {
$oldChildArray = $old[$key];
}
} else {
$merged[$key]['old'] = null;
}
if(isset($new[$key])) {
if( !is_array($new[$key])) {
$merged[$key]['new'] = $new[$key];
} else {
$newChildArray = $new[$key];
}
} else {
$merged[$key]['new'] = null;
}
if($oldChildArray || $newChildArray) {
$merged[$key] = recursiveMergeOldNew($oldChildArray, $newChildArray);
}
}
}
return $merged;
}
Note - this solution needs testing.

PHP get value from armultidimensional array based on array with keys [duplicate]

This question already has answers here:
How to access and manipulate multi-dimensional array by key names / path?
(10 answers)
Closed 2 years ago.
I'm trying to get reach a point in a dynamicly generated multidimensional array based on a array with keys.
Basicly I have the following array:
$arr = [
"something" => [
'something_else' => [
"another_thing" => "boo"
]
],
"something2" => [
'something_elseghf' => [
"another_thingfg" => [
"hi" => "bye"
]
]
],
"info" => [
'something_else2' => [
"another_thingh" => "boo"
]
],
];
Now I want to set a value in the array based on the keys in a different array:
$keyArr = ["something2", 'something_elseghf' "another_thingfg", "hi"];
So the above array means that I need to set the hi key to some value. How can I reach that part of the array with these random keys, note that the length of $keyArr is dynamic aswell. So I can't reach it with:
$arr[$keyArr[0]][$keyArr[1]][$keyArr[2]][$keyArr[3]] =
Hope anyone has an idea on how to solve this!
Try this approach:
$arr = [
"something" => [
'something_else' => [
"another_thing" => "boo"
]
],
"something2" => [
'something_elseghf' => [
"another_thingfg" => [
"hi" => "bye"
]
]
],
"info" => [
'something_else2' => [
"another_thingh" => "boo"
]
],
];
$keyArr = ["something2", 'something_elseghf', "another_thingfg", "hi"];
$cursor = $arr;
foreach ($keyArr as $key) {
$cursor = $cursor[$key];
}
echo $cursor;
Will echo
bye
UPDATE:
If you want to change a value within multi-dimentional array, then use a recursive function, like this:
function changeValue($array, $path, $value) {
if (empty($path)) {
return $value;
}
$key = array_shift($path);
$array[$key] = changeValue($array[$key], $path, $value);
return $array;
}
$arr = [
"something" => [
'something_else' => [
"another_thing" => "boo"
]
],
"something2" => [
'something_elseghf' => [
"another_thingfg" => [
"hi" => "bye"
]
]
],
"info" => [
'something_else2' => [
"another_thingh" => "boo"
]
],
];
$keyArr = ["something2", 'something_elseghf', "another_thingfg", "hi"];
$changedArray = changeValue($arr, $keyArr, 'New value!');
print_r($changedArray);
Will output
Array
(
[something] => Array
(
[something_else] => Array
(
[another_thing] => boo
)
)
[something2] => Array
(
[something_elseghf] => Array
(
[another_thingfg] => Array
(
[hi] => New value!
)
)
)
[info] => Array
(
[something_else2] => Array
(
[another_thingh] => boo
)
)
)

PHP Array function to compare and merge values

Appreciate your time!
After reviewing several 'Compare and Merge' threads, finally, I am going to request someone to help with this very specific scenario.
$input = array(
[ 2616 ] => array(
[ 9878767654 ] => array(
[ 987987987 ] => 987987987,
[ 987987986 ] => 987987986,
),
),
[ 2618 ] => array(
[ 9878767654 ] => array(
[ 987987987 ] => 987987987,
),
),
[ 'tmp-9878767654' ] => array(
[ 9878767654 ] => array(
[ 987987985 ] => 987987985,
[ 987987987 ] => 987987987,
),
),
[ 'tmp-9878767655' ] => array(
[ 9878767655 ] => array(
[ 987987975 ] => 987987975,
),
),
);
$desired_output = array(
[ 2616 ] => array(
[ 9878767654 ] => array(
[ 987987987 ] => 987987987,
[ 987987986 ] => 987987986,
[ 987987985 ] => 987987985,
),
),
[ 2618 ] => array(
[ 9878767654 ] => array(
[ 987987987 ] => 987987987,
[ 987987986 ] => 987987986,
[ 987987985 ] => 987987985,
),
),
[ 'tmp-9878767655' ] => array(
[ 9878767655 ] => array(
[ 987987975 ] => 987987975,
),
),
);
This is the inventory of products (listed by Product ID and Model ID) by Store ID. I want to merge the Model ID values WHERE the product id is the same FROM the array with store-ID starting with 'tmp-'. If product ID is not matched then I want that array to stay as it is. I hope I am making some sense.
Please help.
Here is a snippet to solve the specific problem posed by your example:
$temporaryStores = [];
$prefix = 'tmp-';
$prefixLength = strlen($prefix);
// extract the temporary store structures
foreach ($input as $storeId => $store) {
if (is_string($storeId) && strpos($storeId, $prefix) === 0) {
$productId = (int) substr($storeId, $prefixLength);
$temporaryStores[$productId] = $store;
unset($input[$storeId]);
}
}
// merge matching temporary store structures into the actual ones
$mergedProductIds = [];
foreach ($temporaryStores as $temporaryProductId => $temporaryModels) {
$temporaryModels = reset($temporaryModels); // Incompatible array structure
foreach ($input as $storeId => $store) {
foreach ($store as $productId => $models) {
if ($productId === $temporaryProductId) {
$modelsIds = array_merge($temporaryModels, $models);
$modelsIds = array_unique($modelsIds);
$input[$storeId][$productId] = $modelsIds;
$mergedProductIds[] = $temporaryProductId;
unset($temporaryStores[$temporaryProductId]);
}
}
}
}
// append leftover temporary store structures to the result
foreach ($temporaryStores as $temporaryProductId => $temporaryModels) {
if (!in_array($temporaryProductId, $mergedProductIds, true)) {
$input[$prefix . $temporaryProductId] = $temporaryModels;
}
}
var_dump($input);
This snippet might work for you or not. Either way, I strongly suggest you refactor this code into using a more object oriented design. Where it is made obvious what each value/structure represents, and validation can occur in isolation.
Now you are left having to deal with incompatible array structures that visually look like an incomprehensible mess.

Group subarray data by one column and form comma-separated values from the secondary value in each group [duplicate]

This question already has answers here:
Group subarrays by one column, make comma-separated values from other column within groups
(2 answers)
Closed last month.
I have a two dimensional array which needs to be restructured. Rows must be grouped by date values, and within each group, the name values should be formed into a single comma-delimited string.
My input:
$missedFridgeLog = [
[
"date" => "01/01/18",
"name" => "Medicine"
],
[
"date" => "01/01/18",
"name" => "Drugs"
],
[
"date" => "02/01/18",
"name" => "Medicine"
],
[
"date" => "02/01/18",
"name" => "Drugs"
]
];
I have tried implementing a solution from Implode or join multidimentional array with comma, but it did not work as desired.
Desired output:
[
[
'date' => '01/01/18',
'name' => 'Medicine,Drugs',
],
[
'date' => '02/01/18',
'name' => 'Medicine,Drugs',
]
]
$missedFridgeLog = [
[
"date" => "01/01/18",
"name" => "Medicine"
],[
"date" => "01/01/18",
"name" => "Drugs"
]
[
"date" => "02/01/18",
"name" => "Medicine"
],
[
"date" => "02/01/18",
"name" => "Drugs"
]
];
$byDates = [];
foreach ($missedFridgeLog as $mfg) {
$byDates[$mfg['date']][] = $mfg['name'];
}
$res = [];
foreach ($byDates as $date => $name) {
$res[] = [
'name' => join(',',$name),
'date' => $date
];
}
var_dump($res);
You need to search key, value pair everytime you make a insert or update to result array. use this function() to search associate array. If match then update with additional name info else make a new insert to result array.
function filter_array($array){
///$array your previous array data
$result = array();
foreach($array["missedFridgeLog"] as $m){
$flag = true;
foreach($result as $k=>$r) {
if (is_in_array($r, "date", $m["date"]) == "yes") {
$result[$k]["name"] = $r["name"] . ',' . $m["name"];
$flag = false;
break;
}
}
if($flag==true){
$result[] = $m;
}
}
return array("missedFridgeLog"=>$result);
}
function is_in_array($array, $key, $key_value){
$within_array = 'no';
foreach( $array as $k=>$v ){
if( is_array($v) ){
$within_array = is_in_array($v, $key, $key_value);
if( $within_array == 'yes' ){
break;
}
} else {
if( $v == $key_value && $k == $key ){
$within_array = 'yes';
break;
}
}
}
return $within_array;
}
May be this is not the best way to do this, but it will help
$arr['missedFridgeLog'] = [
[
'date' => '01/01/18',
'name' => 'Medicine1'
],
[
'date' => '02/01/18',
'name' => 'New Medicine2'
],
[
'date' => '01/01/18',
'name' => 'Drugs1'
],
[
'date' => '02/01/18',
'name' => 'Medicine2'
],
[
'date' => '01/01/18',
'name' => 'New Drugs1'
],
[
'date' => '02/01/18',
'name' => 'Drugs2'
]
];
echo "<pre>";
$new_arr = array();
$date = array();
foreach ($arr['missedFridgeLog'] as $k => $a) {
if (in_array($a['date'], $date))
continue;
$new_arr[$k]['name'] = $a['name'];
$new_arr[$k]['date'] = "";
foreach ($arr[missedFridgeLog] as $key => $val) {
if ($key != $k) {
if ($a['date'] == $val['date']) {
$date[] = $new_arr[$k]['date'] = $a['date'];
$new_arr[$k]['name'] .= ", " . $val['name'];
}
}
}
if (empty($new_arr[$k]['date']))
unset($new_arr[$k]);
}
print_r($new_arr);
You can try below :-
Input array :-
$chkArray = [
'missedFridgeLog' => [
'0' => [
'date' => '01/01/18',
'name' => 'Medicine'
],
'1' => [
'date' => '01/01/18',
'name' => 'Drugs'
],
'2' => [
'date' => '02/01/18',
'name' => 'Medicine'
],
'3' => [
'date' => '02/01/18',
'name' => 'Drugs'
],
'4' => [
'date' => '02/01/18',
'name' => 'My Drugs'
]
]
];
$i = 0;
$finalarray = array();
foreach($chkArray['missedFridgeLog'] as $key=>$value) {
$checkExist = array_search($value['date'], array_column($finalarray, 'date'), true);
if($checkExist !== false) {
$finalarray[$checkExist]['name'] = $finalarray[$checkExist]['name'].','.$value['name'];
}
else {
$finalarray[$i]['date'] = $value['date'];
$finalarray[$i]['name'] = $value['name'];
$i++;
}
}
print_r($finalarray);
OutPut :-
Array
(
[0] => Array
(
[date] => 01/01/18
[name] => Medicine,Drugs
)
[1] => Array
(
[date] => 02/01/18
[name] => Medicine,Drugs,My Drugs
)
)
Do not use:
More than one loop,
Nested loops,
in_array(), or
array_search().
You only need one loop to apply temporary grouping keys. When a date/group is encountered after the first time, append its name value to the group's name value. When finished iterating, re-index the array.
Code: (Demo)
$result = [];
foreach ($missedFridgeLog as $row) {
if (!isset($result[$row['date']])) {
$result[$row['date']] = $row;
} else {
$result[$row['date']]['name'] .= ",{$row['name']}";
}
}
var_export(array_values($result));
Output:
array (
0 =>
array (
'date' => '01/01/18',
'name' => 'Medicine,Drugs',
),
1 =>
array (
'date' => '02/01/18',
'name' => 'Medicine,Drugs',
),
)

PHP: How to rename all keys of a Nested List (Array) to 'items'

I need to "reformat" some data coming from an external API so it works with the nested list module of Sencha touch. I cannot change the data output of that external API. Here's an example of the data I get from the API:
$quest = array(
'gastronomy' => [
'restaurants' => [
'italians' => [
[
'title' => 'Al Castello',
'leaf' => true
],
[
'title' => 'Italia',
'leaf' => true
]
],
'asians' => [
[
'title' => 'Gautam',
'leaf' => true
],
[
'title' => 'Wok',
'leaf' => true
]
]
]
]
);
In order to make it work with sencha touch the data must look like this after "reformatting" it with a PHP Service:
$result = array(
'items' => [
[
'title' => 'gastronomy',
'items' => [
[
'title' => 'restaurants',
'items' => [
[
'title' => 'italians',
'items' => [
[
'title' => 'Al Castello',
'leaf' => true
],
[
'title' => 'Italia',
'leaf' => true
]
]
],
[
'title' => 'asians',
'items' => [
[
'title' => 'Gautam',
'leaf' => true
],
[
'title' => 'Wok',
'leaf' => true
]
]
]
]
]
]
]
]
);
I have tried every way I could think of but with no success. What really bugs me is that all keys must be renamed to items. (It's hard for me to access the deeper nested items because of that when I'm using a recursive function)
Haven't tested it, but it seems like a fairly simple recursive function should handle it.
For example:
function parseApi($arr) {
$result = array();
foreach ($arr as $key => $value) {
if (isset($value['leaf'])) {
$result[] = $value;
} else {
$result[] = array(
'title' => $key,
'items' => parseApi($value)
);
}
}
return $result;
}
$result = array( 'items' => $parseApi($quest);
You need a recursive function, and it needs to be able to tell the difference between associative and numerically-indexed arrays.
// from: http://stackoverflow.com/questions/173400/how-to-check-if-php-array-is-associative-or-sequential
function isAssoc($arr) { return array_keys($arr) !== range(0, count($arr) - 1); }
function itemize($foo) {
$output = [];
if( ! isAssoc($foo) ) {
foreach( $foo as $value ) {
if( is_array($value) ) {
$output[] = itemize($value);
} else {
$output[] = $value;
}
}
} else {
foreach( $foo as $key => $value ) {
if( is_array($value) ) {
$output[] = [
'title' => $key,
'items' => itemize($value)
];
} else {
$output[$key] = $value;
}
}
}
return $output;
}
echo json_encode(itemize($quest), JSON_PRETTY_PRINT);
Output:
[
{
"title": "gastronomy",
"items": [
{
"title": "restaurants",
"items": [
{
"title": "italians",
"items": [
{
"title": "Al Castello",
"leaf": true
},
{
"title": "Italia",
"leaf": true
}
]
},
{
"title": "asians",
"items": [
{
"title": "Gautam",
"leaf": true
},
{
"title": "Wok",
"leaf": true
}
]
}
]
}
]
}
]

Categories