To start off with, I have checked all resources I could for examples but was not able to find one that brought me close enough so i can resolve this query (simple as it may seem).
I've also seen there is a question that is the same but never resolved here: Get allDirectories() in Laravel and create a tree
I'll also just use the same sample data cause it's the exact same scenario.
I basically get an output from laravel's AllDirectories() function which output's something like this:
array:20 [▼
0 => "test"
1 => "files"
2 => "files/2"
3 => "files/2/Blocks"
4 => "files/2/Blocks/thumbs"
5 => "files/shares"
]
And I want to convert that into a multidimensional array that looks something like this:
[
["label" => "test", "path" => "test", "children" => []],
["label" => "files", "path" => "files", "children" =>
[
["label" => "2", "path" => "files/2", "children" =>
[
["label" => "Blocks", "path" => "files/2/Blocks", "children" =>
[
[
"label" => "thumbs", "path" => "files/2/Blocks/thumbs", "children" => []
]
]
]
]
],
["label" => "shares", "path" => "files/shares", "children" => []]
]
],
];
How can one go about converting the output from AllDirectories() to a multidimensional array?
Thanks in advance for any tips or tricks :)
You could exploit laravel collections and a bit of recursion to achieve what you need.
I wrote a function which works on a preprocessed output (array instead of plain string) and does the following steps:
Take all given paths and create groups based on the first segment of each path.
For each created group, take its children paths and remove the first segment from each children (filter out empty paths).
Execute convertPathsToTree function on the children paths, and assign its output result to the children key of the resulting tree structure.
Here is the code:
function convertPathsToTree($paths, $separator = '/', $parent = null)
{
return $paths
->groupBy(function ($parts) {
return $parts[0];
})->map(function ($parts, $key) use ($separator, $parent) {
$childrenPaths = $parts->map(function ($parts) {
return array_slice($parts, 1);
})->filter();
return [
'label' => (string) $key,
'path' => $parent . $key,
'children' => $this->convertPathsToTree(
$childrenPaths,
$separator,
$parent . $key . $separator
),
];
})->values();
}
Usage
First of all, let's assume the paths are assigned as a collection to a $data variable:
$data = collect([
'test',
'files',
'files/2',
'files/2/Blocks',
'files/2/Blocks/thumbs',
'files/shares',
]);
You first need to pre-process the array by splitting each path with the directory separator (/ in this example). This can be done with a simple map call:
$processedData = $data->map(function ($item) {
return explode('/', $item);
});
Then, you can use the above function and provide the transformed input to obtain the requested structure:
convertPathsToTree($processedData);
If you would rather obtain an output array instead of an collection, add ->toArray(); after the ->values() call at the end of the function.
STEPS
Convert the paths into arrays.
Find the maximum path depth.
Group paths based on their level of depth.
Merge groupings in a hierarchical format.
Reset the result's array indices/keys.
Print output.
$rawPaths = [
0 => "test",
1 => "files",
2 => "files/2",
3 => "files/2/Blocks",
4 => "files/2/Blocks/thumbs",
5 => "files/karma",
6 => "files/karma/foo",
7 => "files/karma/foo/bar",
8 => "files/shares",
];
// 1. Convert the paths into arrays.
$paths = array_map(function ($path) {
return explode("/", $path);
}, $rawPaths);
// 2. Find the maximum path depth.
$maxDepth = 0;
for ($i = 0; $i < count($rawPaths); $i++) {
if (($count = substr_count($rawPaths[$i], "/")) > $maxDepth) {
$maxDepth = $count;
}
}
// 3. Group paths based on their level of depth.
$groupings = [];
for ($j = 0; $j <= $maxDepth; $j++) {
$groupings[] = array_filter($paths, function ($p) use ($j) {
return count($p) === ($j + 1);
});
}
// 4. Merge groupings in a hierarchical format.
$result = [];
for ($depth = 0; $depth <= $maxDepth; $depth++) {
array_map(function ($grouping) use (&$result, $depth) {
setNode($result, $grouping, $depth);
}, $groupings[$depth]);
}
function setTree(&$grouping, &$depth): array
{
$pathBuilder = $grouping[$depth];
for ($i = 0; $i < $depth; $i++) {
$pathBuilder = $grouping[$depth - ($i + 1)] . "/" . $pathBuilder;
}
return [
"label" => $grouping[$depth],
"path" => $pathBuilder,
"children" => []
];
}
function setNode(&$result, $grouping, $depth)
{
$node = &$result[$grouping[0]];
if ($depth) {
for ($i = ($depth - 1); $i >= 0; $i--) {
$node = &$node["children"][$grouping[$depth - $i]];
}
}
$node = setTree($grouping, $depth);
}
// 5. Reset the result's array indices/keys.
$arrayIterator = new \RecursiveArrayIterator(array_values($result));
$recursiveIterator = new \RecursiveIteratorIterator($arrayIterator, \RecursiveIteratorIterator::SELF_FIRST);
foreach ($recursiveIterator as $key => $value) {
if (is_array($value) && ($key === "children")) {
$value = array_values($value);
// Get the current depth and traverse back up the tree, saving the modifications.
$currentDepth = $recursiveIterator->getDepth();
for ($subDepth = $currentDepth; $subDepth >= 0; $subDepth--) {
// Get the current level iterator.
$subIterator = $recursiveIterator->getSubIterator($subDepth);
// If we are on the level we want to change, use the replacements ($value), otherwise set the key to the parent iterators value.
$subIterator->offsetSet($subIterator->key(), ($subDepth === $currentDepth ? $value : $recursiveIterator->getSubIterator(($subDepth + 1))->getArrayCopy()));
}
}
}
// 6. Print output.
var_export($recursiveIterator->getArrayCopy());
// Output
array (
0 =>
array (
'label' => 'test',
'path' => 'test',
'children' =>
array (
),
),
1 =>
array (
'label' => 'files',
'path' => 'files',
'children' =>
array (
0 =>
array (
'label' => '2',
'path' => 'files/2',
'children' =>
array (
0 =>
array (
'label' => 'Blocks',
'path' => 'files/2/Blocks',
'children' =>
array (
0 =>
array (
'label' => 'thumbs',
'path' => 'files/2/Blocks/thumbs',
'children' =>
array (
),
),
),
),
),
),
1 =>
array (
'label' => 'karma',
'path' => 'files/karma',
'children' =>
array (
0 =>
array (
'label' => 'foo',
'path' => 'files/karma/foo',
'children' =>
array (
0 =>
array (
'label' => 'bar',
'path' => 'files/karma/foo/bar',
'children' =>
array (
),
),
),
),
),
),
2 =>
array (
'label' => 'shares',
'path' => 'files/shares',
'children' =>
array (
),
),
),
),
)
Related
[
0 => [
'qty' => 10
'section' => 'VK7B'
'time_window' => 1
]
1 => [
'qty' => 1
'section' => 'STIC'
'time_window' => 1
]
2 => [
'qty' => 1
'section' => 'STIC'
'time_window' => 1
]
]
I have this multidimensional array where I want to merge the array's if both the section and the time_window are the same, summing the qty for that array. What is an elegant way to do this?
I have tried this ($sections being the multidimensional arry), but this changes the keys and only checks for one value:
$new = []
foreach ($sections as $s) {
if (isset($new[$s['section']])) {
$new[$s['section']]['qty'] += $s['qty'];
$new[$s['section']]['time_window'] = $s['time_window'];
} else {
$new[$s['section']] = $s;
}
}
[
'VK7B' => [
'qty' => 10
'section' => 'VK7B'
'time_window' => 1
]
'STIC' => [
'qty' => 2
'section' => 'STIC'
'time_window' => 1
]
]
As pointed by #RiggsFolly in the comment you are not checking time_window are the same.
You can do it with this code:
$new = []
foreach ($sections as $s) {
// you need to save both values (section and time_window) to check both of them
if (isset($new[$s['section'].$s['time_window']])) {
$new[$s['section'].$s['time_window']]['qty'] += $s['qty'];
$new[$s['section'].$s['time_window']]['time_window'] = $s['time_window'];
} else {
$new[$s['section'].$s['time_window']] = $s;
}
}
// Now $new is an associative array => let's transform it in a normal array
$new = array_values($new);
From a nested array, I want to generate the 1D associative array which contains, for each leaf, its ascending keys concatenation.
Summary
Expected results example
1.1. Input
1.2. Output
Actual results example
1.1. Input
1.2. Output
Question
Minimal, Testable Executable Sources
4.1. Explanations
4.2. Sources & Execution
Expected results example
Input
The following nested array:
[
'key1' => 'foo',
'key2' => [
'key3' => [
0 => ['key4' => 'bar' ],
1 => ['key4' => 'azerty']
]
]
]
Output
The following 1D associative array (glue character for the concatenation of the keys: _):
[
'key1' => 'foo',
'key2_key3_0_key4' => 'bar',
'key2_key3_1_key4' => 'azerty'
]
Actual results example
Input
[
'etat' => 'bar',
'proposition_en_cours' => [
'fichiers' => [
0 => ['url_fichier' => 'foo' ],
1 => ['url_fichier' => 'bar']
]
]
]
Output
Array
(
[] => bar
[proposition_en_cours] => Array
(
[fichiers] => Array
(
[0] => Array
(
[url_fichier] => foo
)
[1] => Array
(
[url_fichier] => bar
)
)
)
[proposition_en_cours_fichiers] => Array
(
[0] => Array
(
[url_fichier] => foo
)
[1] => Array
(
[url_fichier] => bar
)
)
[proposition_en_cours_fichiers_0] => foo
[proposition_en_cours_fichiers_0_1] => bar
)
Question
As you can see, the array I get differs in all points from the expected one. I can't figure out why.
Minimal, Testable Executable Sources
Explanations
I initialize an array that must contain all the ascending keys for each leaf: $key_in_db_format = [];.
I iterate on the input array. For each element (leaf or subarray), I pop $key_in_db_format if, and only if, the current depth equals the last depth. If it's an array (i.e.: not a leaf): I add the key to $key_in_db_format. I set a value (the leaf) at the key that is the concatenation of the ascending keys.
Sources & Execution
First, define this array in an empty PHP script of your choice:
$values = [
'etat' => 'bar',
'proposition_en_cours' => [
'fichiers' => [
0 => [ 'url_fichier' => 'foo' ],
1 => [ 'url_fichier' => 'bar' ]
]
]
];
Then, copy/paste the following code and you will be able to execute it:
$values_to_insert_in_meta_table = [];
$iterator = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($values), \RecursiveIteratorIterator::SELF_FIRST);
$last_depth = 0;
$key_in_db_format = [];
foreach ($iterator as $value_key_field => $value_value_field) {
if($iterator->getDepth() == $last_depth) {
array_pop($key_in_db_format);
}
if(is_array($value_value_field)) {
array_push($key_in_db_format, $value_key_field);
} else {
$values_to_insert_in_meta_table[implode('_', $key_in_db_format)] = $value_value_field;
}
$last_depth = $iterator->getDepth();
}
echo '<pre>';
print_r($values_to_insert_in_meta_table);
Maybe I missed something, but as far as I understand, I would do something like that:
<?php
function flatten(array $array, ?string $prefix = null): array {
$prefix = $prefix === null ? '' : "{$prefix}_";
$output = [];
foreach ($array as $key => $value) {
$key = $prefix . $key;
if (is_array($value)) {
$output = array_merge($output, flatten($value, $key));
} else {
$output[$key] = $value;
}
}
return $output;
}
var_export(flatten([
'key1' => 'foo',
'key2' => [
'key3' => [
0 => ['key4' => 'bar' ],
1 => ['key4' => 'azerty']
]
]
]));
Output:
array (
'key1' => 'foo',
'key2_key3_0_key4' => 'bar',
'key2_key3_1_key4' => 'azerty',
)
I think I've found a solution!!! :-)
$iterator = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($values), \RecursiveIteratorIterator::SELF_FIRST);
$key_in_db_format = [];
$current_counter = 0;
foreach($iterator as $value_key_field => $value_value_field) {
if(is_array($value_value_field)) {
$current_counter = 0;
array_push($key_in_db_format, $value_key_field);
}
if(!is_array($value_value_field)) {
$key_to_insert_in_db = !empty($key_in_db_format) ? implode('_', $key_in_db_format) . '_' . $value_key_field : $value_key_field ;
$values_to_insert_in_meta_table[$key_to_insert_in_db] = $value_value_field;
if($current_counter == count($iterator->getSubIterator())) {
array_pop($key_in_db_format);
}
$current_counter++;
}
}
echo '<br /> <pre>';
print_r($values_to_insert_in_meta_table);
exit;
The idea is:
We add to the array of ascendent keys the key if, and only if, the current element is not a leaf.
If the current element is a leaf, then we define the key equalled to the imploded ascendent keys PLUS (concatenation) the current element's key. Moreover we pop the array of ascendent keys if there are not following siblings elements.
I have a array of stdObjects, which are multilevel. I need to flatten them out.
Here is the array of objects:
[
(int) 0 => object(stdClass) {
link => ''
icon => ''
title => 'Main'
id => (int) 2
children => [
(int) 0 => object(stdClass) {
link => 'YTo0OntzOjEwOiJjb250cm9sbGVyIjtzOjU6InBhZ2VzIjtzOjY6ImFjdGlvbiI7czo3OiJkaXNwbGF5IjtzOjU6InBhc3MwIjtzOjQ6ImhvbWUiO3M6NToicGFzczEiO3M6MDoiIjt9'
icon => 'fa-tachometer-alt'
title => 'Dashboard'
id => (int) 3
}
]
}
]
Now I created following function to reduce the multilevel (can go to 4 levels deep) to single-level and converting the objects to a classic array:
function reduceArrayOfObjects(array $array, array $flatArray, $children = 'children') {
foreach($array as $lvl1) {
$lvl1 = (array) $lvl1;
if(isset($lvl1[$children])) {
reduceArrayOfObjects($lvl1[$children], $flatArray, $children);
unset($lvl1[$children]);
}
$flatArray[] = $lvl1;
}
return $flatArray;
}
$multiLevel = []; // this is the array of objects
$newArray = [];
reduceArrayOfObjects($multiLevel, $newArray);
It won't work properly, I only get the first level back into my new array. I tried lots of variations on how to get to my result, but all of those are failing. When I hardcode all foreach() loops, it works...
This is my current output:
[
(int) 0 => [
'link' => '',
'icon' => '',
'title' => 'Main',
'id' => (int) 2
]
]
Anyone has an idea how to solve my problem?
Thanks to #NigelRen I found the solution.
I just had to add & to the function:
reduceArrayOfObjects(array $array, array &$flatArray, $children = 'children) {}
I need to perform iterated explosions on values in one column of my two dimensional array, then re-group the data to flip the relational presentation from "tag name -> video id" to "video id -> tag name".
Here is my input array:
$allTags = [
[
"name" => "TAG-ONE",
"video" => "64070,64076,64110,64111",
],
[
"name" => "TAG-TWO",
"video" => "64070,64076,64110,64111",
],
[
"name" => "TAG-THREE",
"video" => "64111",
]
];
I want to isolate unique video ids and consolidate all tag names (as comma-separayed values) that relate to each video id.
Expected output:
$allTagsResult = [
[
"name" => "TAG-ONE,TAG-TWO",
"video" => "64070",
],
[
"name" => "TAG-ONE,TAG-TWO",
"video" => "64076",
],
[
"name" => "TAG-ONE,TAG-TWO",
"video" => "64110",
],
[
"name" => "TAG-ONE,TAG-TWO,TAG-THREE",
"video" => "64111",
],
];
Somehow I did it by checking the value using nested loops but I wish to know if you guys can suggest any shortest method to get the expected output.
If you want to completely remove foreach() loops, then using array_map(), array_walk_recursive(), array_fill_keys() etc. can do the job. Although I think that a more straightforward answer using foreach() would probably be faster, but anyway...
$out1 = array_map(function ($data) {
return array_fill_keys(explode(",", $data['video']), $data['name']); },
$allTags);
$out2 = [];
array_walk_recursive( $out1, function ( $data, $key ) use (&$out2) {
if ( isset($out2[$key])) {
$out2[$key]['name'] .= ",".$data;
}
else {
$out2[$key] = [ 'name' => $data, 'video' => $key ];
}
} );
print_r($out2);
will give...
Array
(
[64070] => Array
(
[name] => TAG-ONE,TAG-TWO
[video] => 64070
)
[64076] => Array
(
[name] => TAG-ONE,TAG-TWO
[video] => 64076
)
[64110] => Array
(
[name] => TAG-ONE,TAG-TWO
[video] => 64110
)
[64111] => Array
(
[name] => TAG-ONE,TAG-TWO,TAG-THREE
[video] => 64111
)
)
if you want to remove the keys, then
print_r(array_values($out2));
The code could be compressed by piling all of the code onto single lines, but readability is more useful sometimes.
Another method if you don't like looping:
$video_ids = array_flip(array_unique(explode(",",implode(",",array_column($allTags,'video')))));
$result = array_map(function($id){
return ['name' => '','video' => $id];
},array_flip($video_ids));
array_walk($allTags,function($tag_data) use (&$result,&$video_ids){
$ids = explode(",",$tag_data['video']);
foreach($ids as $id) $result[$video_ids[$id]]['name'] = empty($result[$video_ids[$id]]['name']) ? $tag_data['name'] : $result[$video_ids[$id]]['name'] . "," . $tag_data['name'];
});
Demo: https://3v4l.org/vlIks
Below is one way of doing it.
$allTags = [
'0' => [
"name" => "TAG-ONE",
"video" => "64070,64076,64110,64111",
],
'1' => [
"name" => "TAG-TWO",
"video" => "64070,64076,64110,64111",
],
'2' => [
"name" => "TAG-THREE",
"video" => "64111",
]
];
$allTagsResult = array();
$format = array();
foreach( $allTags as $a ) {
$name = $a['name'];
$videos = explode(',', $a['video']);
foreach( $videos as $v ) {
if( !isset( $format[$v]) ) {
$format[$v] = array();
}
$format[$v][] = $name;
}
}
foreach( $format as $video => $names) {
$allTagsResult[] = array('name' => implode(',', $names), 'video' => $video);
}
echo '<pre>';
print_r($allTagsResult);
die;
You can check Demo
I am typically in favor of functional style coding, but for this task I feel it only serves to make the script harder to read and maintain.
Use nested loops and explode the video strings, then group by those video ids and concatenate name strings within each group. When finished iterating, re-index the array.
Code: (Demo)
$result = [];
foreach ($allTags as $tags) {
foreach (explode(',', $tags['video']) as $id) {
if (!isset($result[$id])) {
$result[$id] = ['video' => $id, 'name' => $tags['name']];
} else {
$result[$id]['name'] .= ",{$tags['name']}";
}
}
}
var_export(array_values($result));
Output:
array (
0 =>
array (
'video' => '64070',
'name' => 'TAG-ONE,TAG-TWO',
),
1 =>
array (
'video' => '64076',
'name' => 'TAG-ONE,TAG-TWO',
),
2 =>
array (
'video' => '64110',
'name' => 'TAG-ONE,TAG-TWO',
),
3 =>
array (
'video' => '64111',
'name' => 'TAG-ONE,TAG-TWO,TAG-THREE',
),
)
I'm working with an array that I'd like to filter so it only contains the lowest prices per key. So 50 would only have one unit, same with 100, and that unit would be the lowest price.
Here's an example of what I'm working with:
$units = [
50 => [
41788 => ['StdRate' => 231.0000, 'UnitName' => "NN23"],
46238 => ['StdRate' => 303.0000, 'UnitName' => "1038"],
46207 => ['StdRate' => 303.0000, 'UnitName' => "1007"]
],
100 => [
41570 => ['StdRate' => 299.0000, 'UnitName' => "HH18"],
46214 => ['StdRate' => 388.0000, 'UnitName' => "1014"]
]
];
I wanted to avoid doing this with a complicated foreach loop, so I thought an array_filter would be nice, but having a hard time wrapping my head around it. Or would a foreach be best?
$filtered = array_filter($units, function($a) {
});
Expected Output:
[
50 => [
41788 => ['StdRate' => 231.0000, 'UnitName' => "NN23"]
],
100 => [
41570 => ['StdRate' => 299.0000, 'UnitName' => "HH18"]
]
];
Here is a function that will go through your multi-dimensional array and grab the lowest units. It will retain the key of the big array (50, 100) and the key of the unit it grabbed (41788, 41570). It will not retain multiple values of the same low rate. In those cases it will return the first of the lowest value it found. Might not be exactly what you want, but it should be a good start and you can always modify it later. It uses a nested foreach to get its work done.
Hope this help you out!
function findLows($big) {
$lowUnits = array();
foreach($big as $id => $array) {
$low = false;
$prev = false;
foreach($array as $k => $a) {
if(!$low) {
$low = $k;
$prev = $a['StdRate'];
} else {
if($a['StdRate'] < $prev) {
$prev = $a['StdRate'];
$low = $k;
}
}
}
$lowUnits[$id] = array( $low => $array[$low]);
}
return $lowUnits;
}
$bigArray = array();
$bigArray[50][41788] = array('StdRate' => 231.0000, 'UnitName' => "NN23");
$bigArray[50][46238] = array('StdRate' => 303.0000, 'UnitName' => "1038");
$bigArray[50][46207] = array('StdRate' => 303.0000, 'UnitName' => "1007");
$bigArray[100][41570] = array('StdRate' => 299.0000, 'UnitName' => "HH18");
$bigArray[100][46214] = array('StdRate' => 388.0000, 'UnitName' => "1014");
$filtered = findLows($bigArray);
var_dump($filtered);
Will produce:
array(2) {
[50]=>
array(1) {
[41788]=>
array(2) {
["StdRate"]=>
float(231)
["UnitName"]=>
string(4) "NN23"
}
}
[100]=>
array(1) {
[41570]=>
array(2) {
["StdRate"]=>
float(299)
["UnitName"]=>
string(4) "HH18"
}
}
}
For an array with 1 fewer levels of depth, you could have directly iterated the data with array_filter(). Because you want to filter each set of data relating to first level entries, you just need to nest the array_filter() inside of array_map() (for a functional-style script).
I am going to isolate the StdRate values in each set and determine the lowest value by calling min() on the temporary array generated by array_column().
To pass the $min value into the filter's scope, use use().
This snippet will potentially return multiple deep subarrays for a respective set of data IF there is a tie among the lowest values in the set.
Code: (Demo)
$units = [
50 => [
41788 => ['StdRate' => 231.0000, 'UnitName' => "NN23"],
46238 => ['StdRate' => 303.0000, 'UnitName' => "1038"],
46207 => ['StdRate' => 303.0000, 'UnitName' => "1007"]
],
100 => [
41570 => ['StdRate' => 299.0000, 'UnitName' => "HH18"],
46214 => ['StdRate' => 388.0000, 'UnitName' => "1014"]
]
];
var_export(
array_map(
function($unit) {
$min = min(array_column($unit, 'StdRate'));
return array_filter(
$unit,
function($row) use ($min) {
return $row['StdRate'] == $min;
}
);
},
$units
)
);
Output:
array (
50 =>
array (
41788 =>
array (
'StdRate' => 231.0,
'UnitName' => 'NN23',
),
),
100 =>
array (
41570 =>
array (
'StdRate' => 299.0,
'UnitName' => 'HH18',
),
),
)