Generate multi-dimensional array from an array in php? - php

I've a list of associative arrays as below:
[
"country" => "AU",
"state" => "VIC",
"suburb" => "Carlton",
"precedence" => ["country", "state", "suburb"]
]
And I want a new multidimensional array like below where the elements are nested based on the order defined by precedence key on first array:
[
"country" => [
"AU" => [
"state" => [
"VIC" => [
"suburb" => "Carlton
]
]
]
]
]
The above is just an example and I want a generic solution that will work for any kinds of array. Only 1 condition that'll be satisfied by all input arrays is that they'll have a precedence element denoting the order in which the output array needs to be generated.
I've tried some recursive solution but it's not working as expected and I've got PHP Fatal error: Allowed memory size of 1073741824 bytes exhausted (looks like it's running infinitely):
function generateArray(&$array)
{
foreach ($array['precedence'] as $key => $property) {
if ($key == sizeof($array['precedence']) - 1) {
return [$property => $array[$property]];
} else {
return generateAssetConfig($array);
}
}
}

You could loop the reversed items of the precedence part.
If there are no items in the result array yet, add the first key => value pair.
Else wrap the current result in a multidimensional array, setting the current value if the iteration as the outer key, and wrap the value (for that key in the source array) together with the current result in a second array.
$source = [
"country" => "AU",
"state" => "VIC",
"suburb" => "Carlton",
"precedence" => ["country", "state", "suburb"]
];
function generateArray($array)
{
$result = [];
foreach(array_reverse($array["precedence"]) as $v) {
$result =! $result ? [$v => $array[$v]] : [$v => [$array[$v] => $result]];
}
return $result;
}
var_export(generateArray($source));
Output
array (
'country' =>
array (
'AU' =>
array (
'state' =>
array (
'VIC' =>
array (
'suburb' => 'Carlton',
),
),
),
),
)

Try this:
function generateNestedArray($arr) {
$precedence = $arr['precedence'];
$nestedArray = [];
for ($i = count($precedence)-1; $i >= 0; $i--) {
$key = $precedence[$i];
if (!$nestedArray) {
$nestedArray[$key] = $arr[$key];
} else {
$nestedArray = [$key => [ $arr[$key]=> $nestedArray]];
}
}
return $nestedArray;
}

Here's a recursive algorithm to do this:
<?php
$raw = [
[
"country" => "AU",
"state" => "VIC",
"suburb" => "Carlton",
"precedence" => ["country", "state", "suburb"]
],
[
"country" => "AU",
"state" => "NSW",
"suburb" => "Sydney",
"precedence" => ["country", "state", "suburb"]
]
];
function generateFromPrecedence($array)
{
if (!isset($array['precedence']))
throw new Exception('Precedence array does not exist');
if (!empty(array_diff($array['precedence'], array_diff(array_keys($array), ['precedence']))))
throw new Exception('Keys and precendence keys different');
return generateStructure($array);
}
function generateStructure($array, $precedence = 0)
{
if ($precedence == count($array['precedence'])-1)
return [$array['precedence'][$precedence] => $array[$array['precedence'][$precedence]]];
return [$array['precedence'][$precedence] => [$array[$array['precedence'][$precedence]] => generateStructure($array, ++$precedence)]];
}
$output = generateFromPrecedence($raw[0]);
var_dump($output);
Outputs:
array(1) {
["country"]=>
array(1) {
["AU"]=>
array(1) {
["state"]=>
array(1) {
["NSW"]=>
array(1) {
["suburb"]=>
string(6) "Sydney"
}
}
}
}
}

Simplest solution (recursive function):
function generateArrayRecursion($array, $precedenceIndex = 0) {
$precedence = $array['precedence'];
return [
$precedence[$precedenceIndex] => $precedenceIndex === \count($precedence) - 1
? $array[$precedence[$precedenceIndex]]
: [$array[$precedence[$precedenceIndex]] => generateArrayRecursion($array, $precedenceIndex + 1)]
];
}
Alternative solution (loop and array references):
function generateArray($array) {
$precedence = $array['precedence'];
$result = [];
$lastKey = $precedence[count($precedence) - 1];
$currentElement = &$result;
foreach ($precedence as $key) {
if ($key === $lastKey) {
$currentElement[$key] = $array[$key];
} else {
$currentElement[$key] = [$array[$key] => []];
$currentElement = &$currentElement[$key][$array[$key]];
}
}
return $result;
}
Usage example:
$array = [
"country" => "AU",
"state" => "VIC",
"suburb" => "Carlton",
"precedence" => ["country", "state", "suburb"]
];
var_dump(generateArrayRecursion($array));
var_dump(generateArray($array));

Related

Remove A|B duplicates from associative array in Laravel

I have an associative array containing entries that are virtually the same, except the key-value pairs are swapped:
[
[
"partnerA" => "Alice",
"partnerB" => "Alfred"
],
[
"partnerA" => "Alfred",
"partnerB" => "Alice"
],
[
"partnerA" => "Alfred",
"partnerB" => "Brandon"
]
]
I stored the array in a Laravel collection and tried using ->unique()
$partners = collect($array)->unique();
but the output matches the array feeding in.
How can I remove duplicates like this from an array so each pair is unique no matter if the keys are swapped?
The desired output is:
[
[
"partnerA" => "Alice",
"partnerB" => "Alfred"
],
[
"partnerA" => "Alfred",
"partnerB" => "Brandon"
]
]
Update: What I've tried so far that seems to be working...
$size = sizeof($partners);
for($i = 0; $i <= $size; $i++){
$match = $partners[$i];
$needle = ["partnerA" => $match["partnerB"], "partnerB" => $match["partnerA"]];
if(in_array($needle, $partners)){
unset($partners[$i]);
}
}
Sort the pairs and concatenate the values for a unique key, then filter based on the result.
$unique_keys = array_keys(array_unique(array_map(
function($a){
sort($a);
return implode("", $a);
},
$partners
)));
$res = array_filter(
$partners,
function($a)use($unique_keys) {
return in_array($a, $unique_keys);
},
ARRAY_FILTER_USE_KEY
);
var_dump($res);
Output:
array(2) {
[0]=>
array(2) {
["partnerA"]=>
string(6) "Alfred"
["partnerB"]=>
string(5) "Alice"
}
[2]=>
array(2) {
["partnerA"]=>
string(6) "Alfred"
["partnerB"]=>
string(7) "Brandon"
}
}
Try this
$results = [
[
"partnerA" => "Alfred",
"partnerB" => "Alice"
],
[
"partnerA" => "Alfred",
"partnerB" => "Alice"
]
];
$newArray = [];
foreach($results as $result) {
if(is_array($result)) {
foreach($result as $key => $output) {
$newArray[$key] = $output;
}
}
}
print_r(array_unique($newArray));

How to recursively sort associative array

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,
),
),

Create multidimensional array from four arrays based on parent id

From these four arrays:
$root = (FORD, FIAT, GM, KIA, FERRARI);
$parentIsFiat = (Panda, Punto, Tipo);
$parentIsPanda = (1, 13, 16, 20);
$parentIs13 = (Red, Blue, White);
How can I create a multidimensional array to give me this:
FORD
FIAT
--Panda
----1
----13
------Red
------Blue
------White
----16
----20
--Punto
--Tipo
GM
KIA
FERRARI
Background: The current menu on my site has every link on the site in the HTML. I want to only have the HTML for menu items that are actually visible. At the moment I get each item in the path (FIAT > Panda > 13) and it's siblings with code similar to this:
$categoryPath = \XLite\Core\Database::getRepo('\XLite\Model\Category')->getCategoryPath($this->getCategoryId());
foreach ($categoryPath as $category) {
$currentCatID = $category->getID();
$currentCatSiblings = $category->getSiblings(true);
foreach ($currentCatSiblings as $sibling) {
$menuItems[$currentCatID][] = $sibling->getName(); // the four arrays above
}
}
Each of the arrays has the parent ID as the key so I can 'attach' it in the correct place but I don't know how to build the array from my four existing arrays.
You could introduce a temporary $parent variable that will reference the place in the tree where the siblings must be inserted, and move that $parent reference further down the tree for the next "generation" of siblings:
$tree = [];
$parent = &$tree; // Use `= &` to reference the same location
foreach ($categoryPath as $category) {
$currentCatID = $category->getID();
$currentCatSiblings = $category->getSiblings(true);
foreach ($currentCatSiblings as $sibling) {
$parent[] = [ "name" => $sibling->getName() ];
if ($sibling->getID() === $currentCatID) $index = count($parent) - 1;
}
$parent[$index]["children"] = [];
$parent = &$parent[$index]["children"]; // Use `= &` to reference the deeper location
}
At the end $tree will contain the nested array. It will have "children" keys to store the nested structure, like this:
[
[ "name" => "Ford" ],
[
"name" => "Fiat",
"children" => [
[
"name" => "Panda",
"children" => [
[ "name" => "1" ],
[
"name" => "13",
"children" => [
[ "name" => "Red" ],
[
"name" => "Blue",
"children" => [],
],
[ "name" => "White" ],
],
],
[ "name" => "16" ],
[ "name" => "20" ],
],
],
[ "name" => "Punto" ],
[ "name" => "Tipo" ],
],
],
[ "name" => "GM" ],
[ "name" => "Kia" ],
[ "name" => "Ferrari" ],
]
Or, if you prefer to have the names used as keys in an associative array, then:
$tree = [];
$parent = &$tree; // Use `= &` to reference the same location
foreach ($categoryPath as $category) {
$currentCatID = $category->getID();
$currentCatSiblings = $category->getSiblings(true);
foreach ($currentCatSiblings as $sibling) {
$parent[$sibling->getName()] = [];
}
$parent = &$parent[$category->getName()]; // Use `= &` to reference the deeper location
}
Result:
[
"Ford" => [],
"Fiat" => [
"Panda" => [
"1" => [],
"13" => [
"Red" => [],
"Blue" => [],
"White" => []
],
"16" => [],
"20" => []
],
"Punto" => [],
"Tipo" => []
],
"GM" => [],
"Kia" => [],
"Ferrari" => []
]
$root = ["FORD", "FIAT", "GM", "KIA", "FERRARI"];
$parentIsFiat = ["Panda", "Punto", "Tipo"];
$parentIsPanda = [1, 13, 16, 20];
$parentIs13 = ["Red", "Blue", "White"];
$desiredOutput = [];
foreach($root as $make){
$desiredOutput[$make] = [];
}
foreach($parentIsFiat as $model){
$desiredOutput["FIAT"][$model] = [];
}
foreach($parentIsPanda as $year){
$desiredOutput["FIAT"]["Panda"][$year] = [];
}
foreach($parentIs13 as $color){
$desiredOutput["FIAT"]["Panda"][13][$color] = [];
}
var_dump($desiredOutput);
Yields:
array(5) {
["FORD"]=>
array(0) {
}
["FIAT"]=>
array(3) {
["Panda"]=>
array(4) {
[1]=>
array(0) {
}
[13]=>
array(3) {
["Red"]=>
array(0) {
}
["Blue"]=>
array(0) {
}
["White"]=>
array(0) {
}
}
[16]=>
array(0) {
}
[20]=>
array(0) {
}
}
["Punto"]=>
array(0) {
}
["Tipo"]=>
array(0) {
}
}
["GM"]=>
array(0) {
}
["KIA"]=>
array(0) {
}
["FERRARI"]=>
array(0) {
}
}
The key to remember is that all arrays in PHP are more like a "dictionary" or "hash set" type in other languages. PHP does not have an "array" as other languages (Java, C, Javascript, ...) have them. If you want an "array" so you can use syntax like echo $myStuff[3]; then you simply create a PHP array (a/k/a "dictionary") where every key is a successive integer.
Example to look under the hood in any array:
$anyArray = getAnyArray();
foreach($anyArray as $key=>$value){
echo($key . ":" . $value . "<br>");
}

Search for matching subarray in parent or child of a multidimensional array

I have the following multidimensional array to build a dynamic menu:
[
"3gnitjUdm6" => [
"name" => "Overview",
"slug" => "overview",
"priority" => 1,
"pages" => [
"i3OQlLqgqO" => [
"name" => "Dashboard",
"url" => "",
"priority" => 2,
"subpages" => [],
],
"izma1tvjGd" => [
"name" => "Settings",
"url" => "/settings",
"priority" => 4,
"subpages" => [],
]
]
],
"IcSujiIx9A" => [
"name" => "Content",
"slug" => "content",
"priority" => 5,
"pages" => [
"3KJdhtCRuI" => [
"name" => "Users",
"url" => "/users",
"priority" => 2,
"subpages" => [],
],
"M3zw9hq6rW" => [
"name" => "Pets",
"url" => "/pets",
"priority" => 4,
"subpages" => [],
],
],
],
]
Each section contains an array of pages, and each page can contain an array of subpages. I need to be able to search through this array to find the key of the section using a key and value pair.
private function _find_section($key, $value) {
foreach($this->menu as $section_key => $section) {
if(is_array($section[$key])) {
foreach($section[$key] as $sub_key => $sub) {
if($sub_key === $value) {
return $section_key;
}
}
} elseif(is_string($section[$key])) {
if($section[$key] === $value) {
return $section_key;
}
} else {
return false;
}
}
}
Running the following code:
_find_section('name', 'Content')
Always returns false.
function flatten(array $collection, array $nested_keys = []) {
$output = [];
foreach ($collection as $key => $value) {
foreach ($nested_keys as $nested_key) {
if (isset($value[$nested_key]) && is_array($value[$nested_key])) {
$output = array_merge($output, flatten($value[$nested_key], [$nested_key]));
}
}
$output[$key] = $value;
}
return $output;
}
function column(array $collection, string $key) {
return array_combine(
array_keys($collection),
array_map(function ($row) use ($key) { return $row[$key]; }, $collection)
);
}
function find_section(array $menu, string $key, string $value) {
$set = column(flatten($menu, ['pages', 'subpages']), $key);
return array_search($value, $set);
}
var_dump(find_section($menu, 'name', 'Dashboard')); // string(10) "i3OQlLqgqO"
You might want to try doing a recursive function instead and forego hardcoded foreach() loops, then you can easily search through many levels of your array using this method:
function recurseFind($array,$findK,$findV,$last=false)
{
if(is_object($array))
$array = (array) $array;
foreach($array as $key => $value) {
if($key == $findK && $value == $findV)
return $last;
if(is_array($value))
$doFind = recurseFind($value,$findK,$findV,$key);
if(!empty($doFind))
return $doFind;
}
}
print_r(recurseFind($arr,'name','Dashboard'));
Gives you:
i3OQlLqgqO

Preg match array keys

I have it like this:
$data = array(
"City_0" => "London",
"City_1" => "Paris",
"City_2" => "Lisbon",
"City_3" => "Berlin"
);
plus some other data in that same array.
User will select only one of these and what I need is:
Check with preg_match to get all keys that starts with "city_"
find key which has value (it is not empty), take that value
assign it to new key
remove all "city_" keys
add new key to array with the name "chosen_city" which will contain that value
What I tried:
foreach ($data as $key => $value) {
$matches = preg_match('/city_/i', $key);
if ($value != "") {
$newValue = $value;
break;
}
}
$data['chosen_city'] = $newValue;
print_r($data);
This works partially, how can I remove all previous "city_" keys from array in that if statement?
NOTE:
I have other keys in array, and I don't want to remove them as well.
Input array:
$data = array(
"City_0" => "London",
"City_1" => "Paris",
"City_2" => "Lisbon",
"City_3" => "Berlin",
"distance" => "5 km",
"days" => "7",
"tickets" => "2",
"discount" => "10%",
);
Expected output:
$data = array(
"chosen_city" => "Berlin",
"distance" => "5 km",
"days" => "7",
"tickets" => "2",
"discount" => "10%",
);
Thanks.
Please put unset for example code :
$data = array( "City_0" => "London", "City_1" => "Paris", "City_2" => "Lisbon", "City_3" => "Berlin");
foreach($data as $key => $value){
$matches = preg_match('/city_/i', $key);
if($matches && $value != ""){
$newValue = $value;
unset($data[$key]);
}elseif($matches){
unset($data[$key]);
}
}
$data['chosen_city'] = $newValue;
preg_* is somewhat overkill in this instance - you could use strpos and it'd work just as well
However, the question is 'how to I remove the city_* keys', so to do that, just use unset():
foreach($data as $key => $value){
$matches = preg_match('/city_/i', $key);
if($value != ""){
$newValue = $value;
unset($data[$key]); //Remove this item from the array
break;
}
}
$data['chosen_city'] = $newValue;
print_r($data);
$data = array(
"City_0" => "London",
"City_1" => "Paris",
"City_2" => "Lisbon",
"City_3" => "Berlin",
"distance" => "5 km",
"days" => "7",
"tickets" => "2",
"discount" => "10%",
);
$value = 'Berlin';
if (array_search($value, $data)) {
$data['chosen_city'] = $value;
foreach ($data as $key=>$val) {
if (stripos($key, 'city_')===0) {
unset($data[$key]);
}
}
}
result:
array(5) {
'distance' =>
string(4) "5 km"
'days' =>
string(1) "7"
'tickets' =>
string(1) "2"
'discount' =>
string(3) "10%"
'chosen_city' =>
string(6) "Berlin"
}
If you use PHP >=5.6, probably you can use (I didn't tested this code)
$city = array_search($value, $data);
if ($city) {
$data['chosen_city'] = $value;
$data = array_filter($data,function($key,$val){return stripos($key,'city_')===false;},ARRAY_FILTER_USE_BOTH);
}

Categories