Prune/reduce a multi-dimensional array in PHP to a certain depth - php

I've searched for this on and off for a couple of years to no avail. Occasionally I'd like to prune a multi-dimension to a certain depth. This would be useful when storing large recursive data sets when you only need data to a certain depth and the rest should be discarded.
For example, given the following array:
$arr = array(
'1' => array(
'1.1' => '1.1.1'),
'2',
'3' => array(
'3.1' => array(
'3.1.1' => '3.1.1.1')
)
);
I would want to prune it back to x number of dimensions calling something like this:
some_prune_method($arr array, $dimensions integer)
...and expect data to be returned something like this:
print_r(some_prune_method($arr, 1));
Array(
[
'1',
'2',
'3'
]
)
print_r(some_prune_method($arr, 2));
Array(
[
'1' => ['1.1'],
'2',
'3' => ['3.1']
]
)
print_r(some_prune_method($arr, 3));
Array(
[
'1' => ['1.1'],
'2',
'3' => ['3.1' => '3.1.1']
]
)
I can think how to do this manually using a callback to iterate through the array but that's slow. Ideally this would need to be done WITHOUT iterating through the entire array.
Maybe I'm missing a simple way to do this?

Think this is what your after. The main concept is just a recursive method to copy the input array which keeps track of the depth. Each time it goes back round it takes 1 off the depth and just before checking if it needs to call the recursive loop it checks if the depth will allow it. If not then it just adds the key to the output loop instead.
$arr = array(
'1' => array(
'1.1' => '1.1.1'),
'2',
'3' => array(
'3.1' => array(
'3.1.1' => '3.1.1.1')
)
);
function some_prune_method ( $array, $depth ) {
$output = [];
foreach ( $array as $key => $element ) {
if ( is_array($element)) {
if ( $depth > 1 ) {
$output [$key] = some_prune_method ( $element, $depth-1 );
}
else {
$output[] = $key;
}
}
else {
$output[] = $element;
}
}
return $output;
}
print_r(some_prune_method($arr, 2));
gives...
Array
(
[1] => Array
(
[0] => 1.1.1
)
[2] => 2
[3] => Array
(
[0] => 3.1
)
)

Related

Insert associative array into nested associative array at a given position

I've searched stackoverflow and found many similar questions which where solved with array_slice, array_splice and the occasional array_merge, but none of these worked for me and I'm stumped. I must be doing something wrong, but can't figure out what exactly.
My problem:
I have a nested associative array representing a menu structure. It looks like this
$this->navigation_array=[
"CATEGORY_MAIN"=>[
"Page1"=>"page1.php",
"Page2"=>"page2.php",
"Page3"=>"page3.php",
"Page4"=>"page4.php"
],
"CATEGORY_NEW"=>[
"Page1"=>"page1_edit.php",
"Page2"=>"page2_edit.php",
"Page3"=>"page3_edit.php"
],
"CATEGORY_EMPTY1"=>[],
"CATEGORY_EMPTY2"=>[],
"SEARCH"=>[
"Page1"=>"page1_search.php",
"Page2"=>"page2_search.php",
"Page3"=>"page3_search.php"
],
"BACK"=>["Home"=>"index.php"]
];
Now I need a function to add a new category at a given position from either the beginning or the end of the navigation_array. So, either $MENU->category_add("test",2) or $MENU->category_add("test",-1)
What I tried so far fails, even though I tried several different approaches from here, here or here. The current, non-functional, iteration looks like this
public function category_add(string $category,int $position=0): bool{
if(array_key_exists($category,$this->navigation_array))return false;
if($position!=0){
$category_array=array($category=>[]);
array_splice($this->navigation_array,$position,0,$category_array);
}else $this->navigation_array[$category]=array(); //simply append if pos is 0
return true;
}
It does insert at the correct position when I call it with $MENU->category_add("test",2) or $MENU->category_add("test",-1) but what is inserted is [0] => Array ( ) instead of ["test"] => Array ( ).
The resulting navigation array thus looks like this:
Array ( [CATEGORY_MAIN] => Array ( [Page1] => page1.php [Page2] => page2.php [Page3] => page3.php [Page4] => page4.php ) [CATEGORY_NEW] => Array ( [Page1] => page1_edit.php [Page2] => page2_edit.php [Page3] => page3_edit.php ) [CATEGORY_EMPTY1] => Array ( ) [CATEGORY_EMPTY2] => Array ( ) [SEARCH] => Array ( [Page1] => page1_search.php [Page2] => page2_search.php [Page3] => page3_search.php ) [0] => Array ( ) [BACK] => Array ( [Home] => index.php ) )
But should look like
Array ( [CATEGORY_MAIN] => Array ( [Page1] => page1.php [Page2] => page2.php [Page3] => page3.php [Page4] => page4.php ) [CATEGORY_NEW] => Array ( [Page1] => page1_edit.php [Page2] => page2_edit.php [Page3] => page3_edit.php ) [CATEGORY_EMPTY1] => Array ( ) [CATEGORY_EMPTY2] => Array ( ) [SEARCH] => Array ( [Page1] => page1_search.php [Page2] => page2_search.php [Page3] => page3_search.php ) [test] => Array ( ) [BACK] => Array ( [Home] => index.php ) )
I know it's probably something pretty minor or silly that I'm overlooking, but I've stared at this so long, I'm obviously blind to it.
Could somebody be so kind and give me a hint?
Edit
So according to the php doc, ""Note: Keys in the replacement array are not preserved." array_splice does not work. But there must be another way to somehow achieve this, right?
Try this code, it has two functions category_add which is calling arrayInsert custom function, we can add a whole new category as well as insert new page inside of existing category.
<?php
$navigation_array = [
"CATEGORY_MAIN" => [
"Page1" => "page1.php",
"Page2" => "page2.php",
"Page3" => "page3.php",
"Page4" => "page4.php"
],
"CATEGORY_NEW" => [
"Page1" => "page1_edit.php",
"Page2" => "page2_edit.php",
"Page3" => "page3_edit.php"
],
"CATEGORY_EMPTY1" => [],
"CATEGORY_EMPTY2" => [],
"SEARCH" => [
"Page1" => "page1_search.php",
"Page2" => "page2_search.php",
"Page3" => "page3_search.php"
],
"BACK" => [
"Home" => "index.php"
]
];
function category_add(array $array, string $category, array $newElement, int $position = 0)
{
if (array_key_exists($category, $array)) {
$categoryArray = $array[$category];
$categoryArray = arrayInsert($categoryArray, $position, $newElement);
$array[$category] = $categoryArray;
} else {
$array[$category] = $newElement;
}
return $array;
}
function arrayInsert($array, $position, $insertArray)
{
$ret = [];
if ($position == count($array)) {
$ret = $array + $insertArray;
} else {
$i = 0;
foreach ($array as $key => $value) {
if ($position == $i++) {
$ret += $insertArray;
}
$ret[$key] = $value;
}
}
return $ret;
}
$newNavigation = category_add($navigation_array, 'CATEGORY_MAIN', ['Page2aaa' => "page2aaa.php"], 1);
echo '<pre>'; print_r($newNavigation);
// create whole new category
$newNavigation2 = category_add($navigation_array, 'CATEGORY_NEW2', ['new' => "new.php"]);
echo '<pre>'; print_r($newNavigation2);
https://www.php.net/manual/en/function.array-splice.php#111204
When trying to splice an associative array into another, array_splice is missing two key ingredients:
a string key for identifying the offset
the ability to preserve keys in the replacement array
This is primarily useful when you want to replace an item in an array with another item, but want to maintain the ordering of the array without rebuilding the array one entry at a time.
<?php
function array_splice_assoc(&$input, $offset, $length, $replacement) {
$replacement = (array) $replacement;
$key_indices = array_flip(array_keys($input));
if (isset($input[$offset]) && is_string($offset)) {
$offset = $key_indices[$offset];
}
if (isset($input[$length]) && is_string($length)) {
$length = $key_indices[$length] - $offset;
}
$input = array_slice($input, 0, $offset, TRUE)
+ $replacement
+ array_slice($input, $offset + $length, NULL, TRUE);
}
$fruit = array(
'orange' => 'orange',
'lemon' => 'yellow',
'lime' => 'green',
'grape' => 'purple',
'cherry' => 'red',
);
// Replace lemon and lime with apple
array_splice_assoc($fruit, 'lemon', 'grape', array('apple' => 'red'));
// Replace cherry with strawberry
array_splice_assoc($fruit, 'cherry', 1, array('strawberry' => 'red'));
?>

Flatten a multi-dimensional array, whilst removing an element from each

Is it possible to flatten a multi-dimensional array, whilst also removing an element from each sub-array?
Currently, I am storing two elements per sub-array, like so:
Array (
[billing_first_name] => Array (
[0] => Test
[1] => 1
)
[billing_last_name] => Array (
[0] => Test
[1] => 1
)
)
But I need to remove the second sub-element, flattening the array to:
Array (
[billing_first_name] => Test
[billing_last_name] => Test
)
I had thought that this could be possible through a foreach, but after removing the 2nd element from the sub-array, I'm unsure what route would be most efficient to flatten the array.
foreach( $customer_data_new as $key => $value ) {
unset($customer_data_new[$key][1]);
}
If anyone could explain the best option, I would be graatful.
Can you try the below code
$customer_data_new = array(
'billing_first_name' => array(
'0' =>'Test',
'1' => 1
),
'billing_last_name' => array(
'0' =>'Test',
'1' => 1
)
);
$newData = array();
foreach( $customer_data_new as $key => $value ) {
$newData[$key] = $value[0];
}
print_r($newData);
Demo Link
Call current() on every subarray. Dead simple.
Code: (Demo)
var_export(array_map('current', $customer_data_new));
Output:
array (
'billing_first_name' => 'Test',
'billing_last_name' => 'Test',
)

Simplify a multidimensional array in PHP

I've received and XML, converted it into an array for usage.
The XML comes in unpredictable multi dimension when I convert it into array.
Had been looking around but could not find a suitable solution.
An alternative is to simplify the converted array.
I've converted an XML to array in PHP, and the result looked like this:
Array
(
[GetMLCBRes] => Array
(
[0] => Array
(
[Ord] => Array
(
[0] => Array
(
[OrdId] => Array
(
[0] => DP Order ID
)
)
)
[ReqInf] => Array
(
[0] => Array
(
[ReqDat] => Array
(
[0] => Date of Request
)
)
)
[Res] => Array
(
[0] => PDF Report
)
)
)
)
May I know how to drop the index like [0] but remain the assoc keys like [Ord], [OrdId], [ReqInf] and [Res], etc.
How to convert it to become like this?
Array
(
[GetMLCBRes] => Array
(
[Ord] => Array
(
[OrdId] => DP Order ID
)
[ReqInf] => Array
(
[ReqDat] => Date of Request
)
[Res] => PDF Report
)
)
it works but if you change your structure maybe it won't. It's not optimized too :)
$input = Array(
'GetMLCBRes' => Array(Array(
'Ord' => Array(Array(
'OrdId' => Array('DP Order ID')
)),
'ReqInf' => Array(Array(
'ReqDat' => Array('Date of Request')
)),
'Res' => Array('PDF Report')
))
);
foreach($input as &$in){
$sub = $in[0];
foreach($sub as $key => &$value){
$sub2 = $value[0];
if(!is_array($sub2)){
$sub[$key] = $sub2;
continue;
}
$final2 = array();
foreach($sub2 as $key2 => $final)
$final2[$key2] = $final[0];
$sub[$key] = $final2;
}
$in = $sub;
}
var_dump($input);
You can test it here : http://sandbox.onlinephpfunctions.com/code/a6770c7649d7d277aa1dc3544093cc87bed0951d
This should work as expected :
function recursive_skip(array $ary) {
foreach($ary as $k => &$v) {
if (is_array($v)) {
// Skip it
$v = $v[0];
}
if (is_array($v)) {
// If current array item is an array itself, recursively call the function on it
$v = recursive_skip($v);
}
}
// Return updated array to calling context
return $ary;
}
$source = Array(
'GetMLCBRes' => Array(Array(
'Ord' => Array(Array(
'OrdId' => Array('DP Order ID')
)),
'ReqInf' => Array(Array(
'ReqDat' => Array('Date of Request')
)),
'Res' => Array('PDF Report')
))
);
$dest = recursive_skip($source);
var_dump($dest);
A few caveats : the function will only skip one array level each time (but could be adapted to handle more if needed) and might come with a significant performance cost if your source array is huge since it's recursive (O(n)), it just walks through the whole array tree.

Count number of different strings?

I have an array that looks like
Array
(
[1] => Array
(
[0] => Date
[1] => Action
)
[2] => Array
(
[0] => 2011-01-22 11:23:19
[1] => SHARE_TWEET
)
[3] => Array
(
[0] => 2011-01-22 11:23:19
[1] => SHARE_FACEBOOK
)
and many other different values (about 10), what I want to do is I want to count the number of times a string is in the array. I was going to use array_count_values but it doesn't count multidimensional arrays.
Any other options?
This could be done by first flattening the array, and then using array_count_values() on it:
For flattening, here is the trick:
$array = call_user_func_array('array_merge', $arrays);
And then:
$counts = array_count_values($array);
Output:
array (
'Date' => 1,
'Action' => 1,
'2011-01-22 11:23:19' => 2,
'SHARE_TWEET' => 1,
'SHARE_FACEBOOK' => 1,
)
Full code:
$array = call_user_func_array('array_merge', $arrays);
var_export(array_count_values($array));
Any time you're dealing with arrays, especially with loops in PHP I can't string enough suggest you look at the array documentation, You'd be suprised how quickly you realise most of the loops in your code is unnecessary. PHP has a built in function to achieve what you're after called array_walk_recursive. And since you're using PHP5 you can use closures rather that create_function (which can be very troublesome, especially to debug, and can't be optimised by the PHP interpreter afik)
$strings = array();
array_walk_recursive($arr, function($value, $key) use (&$strings) {
$strings[$value] = isset($strings[$value]) ? $strings[$value]+1 : 1;
});
I know, unary statements aren't always clear, but this one is simple enough, but feel free to expand out the if statement.
The result of the above is:
print_r($strings);
Array
(
[Date] => 1,
[Action] => 1,
[2011-01-22 11:23:19] => 2,
[SHARE_TWEET] => 1,
[SHARE_FACEBOOK] => 1,
)
Pseudo Code
$inputArray = // your array as in the example above
foreach ($inputArray as $key => $value) {
$result[$value[1]] = $result[$value[1]] + 1;
}
var_dump($result);
Here is a way to do the job:
$arr = Array (
1 => Array (
0 => 'Date',
1 => 'Action'
),
2 => Array (
0 => '2011-01-22 11:23:19',
1 => 'SHARE_TWEET'
),
3 => Array (
0 => '2011-01-22 11:23:19',
1 => 'SHARE_FACEBOOK'
)
);
$result = array();
function count_array($arr) {
global $result;
foreach($arr as $k => $v) {
if (is_array($v)) {
count_array($v);
} else {
if (isset($result[$v])) {
$result[$v]++;
} else {
$result[$v] = 1;
}
}
}
}
count_array($arr);
print_r($result);
output:
Array
(
[Date] => 1
[Action] => 1
[2011-01-22 11:23:19] => 2
[SHARE_TWEET] => 1
[SHARE_FACEBOOK] => 1
)

How do you reindex an array in PHP but with indexes starting from 1?

I have the following array, which I would like to reindex so the keys are reversed (ideally starting at 1):
Current array (edit: the array actually looks like this):
Array (
[2] => Object
(
[title] => Section
[linked] => 1
)
[1] => Object
(
[title] => Sub-Section
[linked] => 1
)
[0] => Object
(
[title] => Sub-Sub-Section
[linked] =>
)
)
How it should be:
Array (
[1] => Object
(
[title] => Section
[linked] => 1
)
[2] => Object
(
[title] => Sub-Section
[linked] => 1
)
[3] => Object
(
[title] => Sub-Sub-Section
[linked] =>
)
)
If you want to re-index starting to zero, simply do the following:
$iZero = array_values($arr);
If you need it to start at one, then use the following:
$iOne = array_combine(range(1, count($arr)), array_values($arr));
Here are the manual pages for the functions used:
array_values()
array_combine()
range()
Why reindexing? Just add 1 to the index:
foreach ($array as $key => $val) {
echo $key + 1, '<br>';
}
Edit   After the question has been clarified: You could use the array_values to reset the index starting at 0. Then you could use the algorithm above if you just want printed elements to start at 1.
This will do what you want:
<?php
$array = array(2 => 'a', 1 => 'b', 0 => 'c');
array_unshift($array, false); // Add to the start of the array
$array = array_values($array); // Re-number
// Remove the first index so we start at 1
$array = array_slice($array, 1, count($array), true);
print_r($array); // Array ( [1] => a [2] => b [3] => c )
?>
You may want to consider why you want to use a 1-based array at all. Zero-based arrays (when using non-associative arrays) are pretty standard, and if you're wanting to output to a UI, most would handle the solution by just increasing the integer upon output to the UI.
Think about consistency—both in your application and in the code you work with—when thinking about 1-based indexers for arrays.
You can reindex an array so the new array starts with an index of 1 like this;
$arr = array(
'2' => 'red',
'1' => 'green',
'0' => 'blue',
);
$arr1 = array_values($arr); // Reindex the array starting from 0.
array_unshift($arr1, ''); // Prepend a dummy element to the start of the array.
unset($arr1[0]); // Kill the dummy element.
print_r($arr);
print_r($arr1);
The output from the above is;
Array
(
[2] => red
[1] => green
[0] => blue
)
Array
(
[1] => red
[2] => green
[3] => blue
)
Well, I would like to think that for whatever your end goal is, you wouldn't actually need to modify the array to be 1-based as opposed to 0-based, but could instead handle it at iteration time like Gumbo posted.
However, to answer your question, this function should convert any array into a 1-based version
function convertToOneBased( $arr )
{
return array_combine( range( 1, count( $arr ) ), array_values( $arr ) );
}
EDIT
Here's a more reusable/flexible function, should you desire it
$arr = array( 'a', 'b', 'c' );
echo '<pre>';
print_r( reIndexArray( $arr ) );
print_r( reIndexArray( $arr, 1 ) );
print_r( reIndexArray( $arr, 2 ) );
print_r( reIndexArray( $arr, 10 ) );
print_r( reIndexArray( $arr, -10 ) );
echo '</pre>';
function reIndexArray( $arr, $startAt=0 )
{
return ( 0 == $startAt )
? array_values( $arr )
: array_combine( range( $startAt, count( $arr ) + ( $startAt - 1 ) ), array_values( $arr ) );
}
A more elegant solution:
$list = array_combine(range(1, count($list)), array_values($list));
The fastest way I can think of
array_unshift($arr, null);
unset($arr[0]);
print_r($arr);
And if you just want to reindex the array(start at zero) and you have PHP +7.3 you can do it this way
array_unshift($arr);
I believe array_unshift is better than array_values as the former does not create a copy of the array.
Changelog
Version
Description
7.3.0
This function can now be called with only one parameter. Formerly, at least two parameters have been required.
-- https://www.php.net/manual/en/function.array-unshift.php#refsect1-function.array-unshift-changelog
$tmp = array();
foreach (array_values($array) as $key => $value) {
$tmp[$key+1] = $value;
}
$array = $tmp;
It feels like all of the array_combine() answers are all copying the same "mistake" (the unnecessary call of array_values()).
array_combine() ignores the keys of both parameters that it receives.
Code: (Demo)
$array = [
2 => (object)['title' => 'Section', 'linked' => 1],
1 => (object)['title' => 'Sub-Section', 'linked' => 1],
0 => (object)['title' => 'Sub-Sub-Section', 'linked' => null]
];
var_export(array_combine(range(1, count($array)), $array));
Output:
array (
1 =>
(object) array(
'title' => 'Section',
'linked' => 1,
),
2 =>
(object) array(
'title' => 'Sub-Section',
'linked' => 1,
),
3 =>
(object) array(
'title' => 'Sub-Sub-Section',
'linked' => NULL,
),
)
If you are not trying to reorder the array you can just do:
$array = array_reverse( $array );
and then call it once more to get it back to the same order:
$array = array_reverse( $array );
array_reverse() reindexes as it reverses. Someone else showed me this a long time ago. So I can't take credit for coming up with it. But it is very simple and fast.
The result is an array with indexed keys starting from 0. https://3v4l.org/iQgVh
array (
0 =>
(object) array(
'title' => 'Section',
'linked' => 1,
),
1 =>
(object) array(
'title' => 'Sub-Section',
'linked' => 1,
),
2 =>
(object) array(
'title' => 'Sub-Sub-Section',
'linked' => NULL,
),
)
Here's my own implementation. Keys in the input array will be renumbered with incrementing keys starting from $start_index.
function array_reindex($array, $start_index)
{
$array = array_values($array);
$zeros_array = array_fill(0, $start_index, null);
return array_slice(array_merge($zeros_array, $array), $start_index, null, true);
}

Categories