php foreach multidimensional array recursion not working? - php

Some kind of followup to my last question: for loop - move deeper on numeric key in multidimensional array
I have this array as input:
Array
(
[0] => apl_struct Object
(
[funcname] => say
[args] => Array
(
[0] => Array
(
[0] => apl_struct Object
(
[funcname] => text
[args] => Array
(
[value] => hello
)
)
)
)
)
)
I now have 2 functions working for me.
One is a func only for getting the next key/value in an associative array.
None of the next(), prev(), etc. were working for me like on indexed arrays:
function getnext($array, $key) {
$keys = array_keys($array);
if ((false !== ($p = array_search($key, $keys))) && ($p < count($keys) - 1)) {
return array('key' => $keys[++$p], 'value' => $array[$keys[$p]]);
} else {return false;}
}
The next function is my executer or constructer. he creates a semi-xmlstruct for me.
I tried to add recursion for skipping the numeric key. They're obviously nonsense and can be skipped.
I then want to check if all the values of the non-numeric keys are arrays or not.
If it is an array it indicates arguments to be followed and output should look like: INPUT.
If not, it's either the functionname (funcname) or indeed a real value for us like "hello".
function arr2xml($array, $level = 1, $pos = 1) {
$xml = '';
foreach ($array as $key => $value) {
if (is_object($value)) {$value = get_object_vars($value);}// convert object to array
if (is_numeric($key)) {
$xml .= arr2xml($value);
} else {
if (!is_array($value)) {
switch ($key) {
case 'funcname':
$nextkey = getnext($array, $key);
$xml .= str_repeat("\t", $level) . "<apl:$value>\n";
$xml .= arr2xml($nextkey['value'], $level++);
$xml .= str_repeat("\t", $level) . "</apl:$value>\n";
break;
case 'value':
$xml .= str_repeat("\t", $level) . "\t$value\n";
break;
}
} else {
$xml .= str_repeat("\t", $level) . "<$key pos='$pos'>\n\t";
$xml .= arr2xml($value, $level++, $pos++);
$xml .= str_repeat("\t", $level) . "</$key>\n";
}
}
}
return $xml;
}
but what I am getting out of this so far is this:
the function name was inserted right.
it is say and text.
also, in some wild circumstances, the -tag and the value are executed properly.
<apl:say>
<apl:text>
hello
</apl:text>
<args pos='1'>
hello
</args>
</apl:say>
<args pos='1'>
<apl:text>
hello
</apl:text>
<args pos='1'>
hello
</args>
</args>
</xml>
for me it looks like the recursion isn't really working. Am i missing something here?
I've tried to rebuild it from previous mentioned post.
Also I'm wondering about the multiple output I am getting here.
The tags seem to get filled right, but the actual arrangement is quite confusing for me.
I was expecting the output to look like this:
<apl:say>
<args pos='1'>
<apl:text>
<args pos='1'>
hello
</args>
</apl:text>
</args>
</apl:say>
Thanks in advance

TL;DR
It's a combination of the (completely superfluous function) getnext(), and how your arr2xml() recurses. I've provided here an arr2xml() replacement function which will do what you want, without any need for getnext().
A detailed description of what went wrong in your code, and how I suggest fixing it, follows.
function arr2xml($array, $level = 0, $pos = 1) {
$xml = '';
foreach ($array as $key => $value) {
if (is_object($value)) {
$value = get_object_vars($value);
}
if (is_numeric($key)) {
$xml .= arr2xml($value, $level+1);
continue;
} else {
if (!is_array($value)) {
switch ($key) {
case 'funcname':
array_shift($array);
$xml .= str_repeat(" ", $level) . "<apl:$value>\n";
$xml .= arr2xml($array, $level+1);
$xml .= str_repeat(" ", $level) . "</apl:$value>\n";
return $xml;
case 'value':
$xml .= str_repeat(" ", $level) . " $value\n";
return $xml;
}
} else {
$xml .= str_repeat(" ", $level) . "<$key pos='$pos'>\n ";
$xml .= arr2xml($value, $level+1, $pos+1);
$xml .= str_repeat(" ", $level) . "</$key>\n";
return $xml;
}
}
}
return $xml;
}
Here is an eval.in showing this new function in use on the same data structure you provided, giving you more or less the desired output (the whitespace may not be exactly what you wanted, I'll leave that as an exercise for you.)
What went wrong with your code
When funcname is 'say', the condition case 'funcname': calls getnext() with $key set to 'funcname' and $array set to:
array(2) {
["funcname"]=>
string(3) "say"
["args"]=>
array(1) {
[0]=>
array(1) {
[0]=>
object(apl_struct)#1 (2) {
["funcname"]=>
string(4) "text"
["args"]=>
array(1) {
["value"]=>
string(5) "hello"
}
}
}
}
}
You then find 'funcname' in that array ($p = array_search($key, $keys)) and create a new array containing only the value of the next item in the array:
return array('key' => $keys[++$p], 'value' => $array[$keys[$p]]);
The result is an array that no longer contains the 'args' key:
array(2) {
["key"]=>
string(4) "args"
["value"]=>
array(1) {
[0]=>
array(1) {
[0]=>
object(apl_struct)#1 (2) {
["funcname"]=>
string(4) "text"
["args"]=>
array(1) {
["value"]=>
string(5) "hello"
}
}
}
}
}
Thus, you will never get the tag you were hoping for, because the data structure has been corrupted by getnext() to remove the key you expected to find in order to construct it.
The duplicate values can be resolved by returning from the inner recursion earlier. Right now, you are recursing, processing the "inner" nodes, then returning back to the top and processing them again.
Instead, we can drop getnext entirely (since it doesn't even do what you wanted), and we can just use array_shift instead to throw away the left-most value of the array. We then continue processing $array like normal.

after some time spent i came up with this solution for my problem:
function apl2xml($array, $tlevel = 0) {
$ret = '';
$ret .= str_repeat("\t", $tlevel) . "<apl>\n";
foreach ($array as $key => $value) {
$ret .= $this->aplstruct2xml($value, $tlevel + 1);
}
$ret .= str_repeat("\t", $tlevel) . "</apl>\n";
return $ret;
}
function aplstruct2xml($apl_struct, $tlevel = 0) {
$ret = '';
if ($apl_struct->funcname == 'text') {
$ret .= str_repeat("\t", $tlevel) . "<text>\n";
$ret .= str_repeat("\t", $tlevel);
$ret .= $apl_struct->args[0] . "\n";
$ret .= str_repeat("\t", $tlevel) . "</text>\n";
} else {
$ret .= str_repeat("\t", $tlevel) . "<aplfunc:{$apl_struct->funcname}>\n";
foreach ($apl_struct->args as $key => $value) {
$ret .= str_repeat("\t", $tlevel + 1) . "<arg pos='$key'>\n";
$ret .= $this->apl2xml($value, $tlevel + 2);
$ret .= str_repeat("\t", $tlevel + 1) . "</arg>\n";
}
$ret .= str_repeat("\t", $tlevel) . "</aplfunc:{$apl_struct->funcname}>\n";
}
return $ret;
}
turns out that i did not need any recursion at all..
the so called apl just consisted of an apl_struct which could contain more apl's/apl_struct's

Related

Tree map by categories

I'm trying to build a tree-map from categories.
I have the categories (I have a lot of categories and I want to remove duplicates and show them in a tree-map view)
$cat = array(
"Sneakers/Men",
"Sneakers/Women",
"Accessories/Jewellery/Men",
"Accessories/Jewellery/Women",
"Accessories/Jewellery/Men
");
...and I want them like this
$categories = array(
"Sneakers" => array(
"Men" => array(),
"Women" => array()
),
"Accessories" => array(
"Jewellery" => array(
"Men" => array(),
"Women" => array()
)
)
);
to print them like this
- Sneakers
-- Men
-- Women
- Accessories
-- Jewellery
--- Men
--- Women
Try this:
<?php
$cat = array(
"Sneakers/Men",
"Sneakers/Women",
"Accessories/Jewellery/Men",
"Accessories/Jewellery/Women",
"Accessories/Jewellery/Men
");
function buildTree($categories, $result = []){
$temp = [];
foreach($categories as $categoryString){
$catParts = explode('/',$categoryString);
if(count($catParts) > 1){
$temp[$catParts[0]][] = str_replace($catParts[0].'/','',$categoryString);
} else {
$temp[$catParts[0]] = [];
}
}
foreach($temp as $elemName => $elemVal){
$result[$elemName] = buildTree($elemVal);
}
return $result;
}
var_dump(buildTree($cat));
The most simple way is to use references, like this:
$out = [];
foreach ($cat as $str) {
$lookup =& $out;
foreach (explode("/", $str) as $part) {
$lookup =& $lookup[$part];
if (!isset($lookup)) {
$lookup = [];
}
}
}
$lookup initially refers to the whole expected result, then the reference is extended at each step to follow the path of nested members.
Note that each new member added looks like member-name => [], so that actually even final leaves are arrays: it may seem a bit weird, but is a pretty way to have a reduced code (each member is always ready to receive children).
And it's not a difficulty, though, to use the resulting array to then print it like the OP asked:
function nest_print($src, $level = 0) {
$prefix = '<br />' . str_repeat('- ', ++$level);
foreach ($src as $key => $val) {
echo $prefix . $key;
if ($val) {
nest_print($val, $level);
}
}
}
nest_print($out);
EDIT
Here is an alternate solution, including the count of final leaves, as asked by the OP in his comment:
$out = [];
foreach ($cat as $str) {
$lookup =& $out;
$parts = explode("/", $str);
foreach ($parts as $part) {
$lookup =& $lookup[$part];
if (!isset($lookup)) {
$lookup = [];
}
// when $part is a final leaf, count its occurrences
if ($part == end($parts)) {
$lookup = is_array($lookup) ? 1 : ++$lookup;
}
}
}
(might likely be improved in a more elegant way, though)
And here is how to modify the print-result snippet accordingly:
function nest_print($src, $level = 0) {
$prefix = '<br />' . str_repeat('- ', ++$level);
foreach ($src as $key => $val) {
echo $prefix . $key;
if (is_array($val)) {
nest_print($val, $level);
} else {
echo ': ' . $val;
}
}
}
nest_print($out);

Nested array into menu, but with urls generation

Pleaser help to create menu with nested urls.
I have a mulidimentual array, like this:
["Увлажнение"]=>
array(0) {
}
["Туалетная вода"]=>
array(0) {
}
["Духи и парфюмерная вода"]=>
array(0) {
}
["Мужские аксессуары"]=>
array(13) {
["Часы"]=>
array(1) {
["Часы"]=>
array(0) {
}
}
["Сумки и чехлы"]=>
array(8) {
["Спортивные сумки"]=>
array(0) {
}
["Сумки"]=>
array(0) {
}
["Рюкзаки"]=>
array(0) {
}
I have a function that create HTML Menu from this array:
function makeList($array) {
//Base case: an empty array produces no list
if (empty($array)) return '';
//Recursive Step: make a list with child lists
$output = '<ul>';
foreach ($array as $key => $subArray) {
$url = URLify::filter ($key);
$output .= '<li>' . $key .''. makeList($subArray) . '</li>';
}
$output .= '</ul>';
return $output;
};
That generame manu like:
УвлажнениеТуалетная водаДухи и парфюмерная водаМужские аксессуарыЧасыСумки и чехлыСпортивные сумкиСумки
Every menu urls like:
uvlazhnenie
tualetnaya-voda
duhi-i-parfyumernaya-voda
muzhskie-aksessuary
chasy
But i need urls like:
muzhskie-aksessuary/chasy
With nested (with delemiter) urls.
Please help me. Thanks.
You can use a second argument for your function for this:
function makeList($array, $url = '') {
//Base case: an empty array produces no list
if (empty($array)) return '';
//Recursive Step: make a list with child lists
$output = '<ul>';
foreach ($array as $key => $subArray) {
$url2 = $url . ($url == '' ? '' : '/') . URLify::filter ($key);
$output .= '<li>' . $key .''. makeList($subArray, $url2) . '</li>';
}
$output .= '</ul>';
return $output;
}

PHP - variable scope in recursively function

First assumption: Assume, we defined a variable (its name is $tmp) in a function(functioin name is 'ExpMenu') for temporary calculating and in end of function we return this variable.
Second assumption: Assume, we call that function recursively for create a navigation menu base on a multidimensional array.
My question is about scope of that variable ($tmp). In every call funtion, will its value overwritten? In other words, by every function call we lose previous value?
For more detail, please review below code:
/// --- { Declaration Block
$content = array(
array(
'level'=>'1',
'order'=>'1',
'text'=>'New Solution WorkFlow',
'is_parent'=>'yes',
'child'=> array(
array(
'level'=>'2',
'order'=>'1',
'text'=>'Define New Solution',
'is_parent'=>'no',
'url'=>'#'
),
array(
'level'=>'2',
'order'=>'2',
'text'=>'View Solutions',
'is_parent'=>'no',
'url'=>'#'
),
array(
'level'=>'2',
'order'=>'3',
'text'=>'View Solutions',
'is_parent'=>'no',
'url'=>'#'
)
)
),
array(
'level'=>'1',
'order'=>'2',
'text'=>'Solution Modify WorkFlow',
'is_parent'=>'yes',
'child'=> array(
array(
'level'=>'2',
'order'=>'1',
'text'=>'Request For Solution Modify',
'is_parent'=>'no',
'url'=>'#'
)
)
),
array(
'level'=>'1',
'order'=>'3',
'text'=>'Solution Close WorkFlow',
'is_parent'=>'yes',
'child'=> array(
array(
'level'=>'2',
'order'=>'1',
'text'=>'Declare For Solution Close',
'is_parent'=>'no',
'url'=>'#'
)
)
)
);
function ExpMenu($item_array ) {
$tmp='';
foreach ($item_array as $item) {
if ($item['is_parent']=='yes') {
$tmp = '<li class="hasChild">' . $item["text"] . '<ul>';
$tmp .= ExpMenu($item['child']);
$tmp .= '</ul></li>';
} else {
$tmp = '<li>';
$tmp .= ''. $item['text'] . '' ;
$tmp .= '</li>';
}
}
return $tmp;
}
/// --- }
$menu='<div><ul>';
$menu .= ExpMenu($content);
$menu.='</ul></div>';
echo $m . '<br />';
It seams by every call function we lose pervious value.
I thank #l0rkaY for her/him solution, But I found another solution that doesn't need add new parameter in my function.
Because $tmp scope is in 'ExpMenu' function and we call recursively function, therefore variable still alive and wasn't terminated.
So, I modify my function a bit:
function ExpMenu($item_array ) {
$tmp='';
foreach ($item_array as $item) {
if ($item['is_parent']=='yes') {
$tmp .= '<li class="hasChild">' . $item["text"] . '<ul>';
$tmp .= ExpMenu($item['child']);
$tmp .= '</ul></li>';
} else {
$tmp .= '<li>';
$tmp .= ''. $item['text'] . '' ;
$tmp .= '</li>';
}
}
return $tmp;
}
I assume your actual problem is, that your function generates only single item with a single child item.
It's because you are overwriting your previous item in your if/else blocks. That's why you only get the last item.
You just need to concatenate them to the existing items.
function ExpMenu($item_array ) {
$tmp='';
foreach ($item_array as $item) {
if ($item['is_parent']=='yes') {
$tmp .= '<li class="hasChild">' . $item["text"] . '<ul>';
$tmp .= ExpMenu($item['child']);
$tmp .= '</ul></li>';
} else {
$tmp .= '<li>';
$tmp .= ''. $item['text'] . '' ;
$tmp .= '</li>';
}
}
return $tmp;
}

replace any specific character in array key

$array['a:b']['c:d'] = 'test';
$array['a:b']['e:f']= 'abc';
I need output like below. array can have multiple level . Its comes with api so we do not know where colon come.
$array['ab']['cd'] = 'test';
$array['ab']['ef']= 'abc';
(untested code) but the idea should be correct if want to remove ':' from keys:
function clean_keys(&$array)
{
// it's bad to modify the array being iterated on, so we do this in 2 steps:
// find the affected keys first
// then move then in a second loop
$to_move = array();
forach($array as $key => $value) {
if (strpos($key, ':') >= 0) {
$target_key = str_replace(':','', $key);
if (array_key_exists($target_key, $array)) {
throw new Exception('Key conflict detected: ' . $key . ' -> ' . $target_key);
}
array_push($to_move, array(
"old_key" => $key,
"new_key" => $target_key
));
}
// recursive descent
if (is_array($value)) {
clean_keys($array[$key]);
}
}
foreach($to_move as $map) {
$array[$map["new_key"]] = $array[$map["old_key"]];
unset($array[$map["old_key"]]);
}
}
try this:
$array=array();
$array[str_replace(':','','a:b')][str_replace(':','','c:d')]="test";
print_r($array);
This seems like the simplest and most performant approach:
foreach ($array as $key => $val) {
$newArray[str_replace($search, $replace, $key)] = $val;
}

PHP — How to determine number of parents of a child array?

I'm not so strong with arrays but I need to determine how to count the number of parents a child array has in order to determine the indenting to display it as an option in a SELECT.
So, if I have this array:
array(
'World'=>array(
'North America'=>array(
'Canada'=>array(
'City'=>'Toronto'
)
)
)
);
How would I go about determining how many parents 'City' has in order to translate that into the number of spaces I want to use as an indent?
Thanks for any help.
EDIT: Let's see if I can explain myself better:
I have this code I'm using to build the OPTIONS list for a SELECT:
function toOptions($array) {
foreach ($array as $key=>$value) {
$html .= "<option value=\"" . $key . "\" >";
$html .= $value['title'];
$html .= "</option>";
if (array_key_exists('children', $value)) {
$html .= toOptions($value['children']);
}
}
return $html;
}
print toOptions($list);
So, I'm trying to determine how to get the number of parents in order to add spaces before the title in this line:
$html .= $value['title'];
Like:
$html .= " " . $value['title'];
But, I'm not sure how to figure out how many spaces to add.
Hopefully this is more clear.
Thanks for any help so far.
$x = array(
'World'=>array(
'North America'=>array(
'Canada'=>array(
'City'=>'Toronto'
)
)
)
);
// This function do something with the key you've found in the array
function visit($name, $depth)
{
echo $name . ' has ' . $depth . ' parents.';
}
// This function visits all the contents aff $array
function find_recursive($array, $depth = 0)
{
if (is_array($array)) {
foreach ($array as $k => $value) {
visit($k, $depth + 1);
find_recursive($array, $depth + 1);
}
}
}
For visiting:
find_recursive($x);
Well. Off the top what you are dealing with is a multi dimensional array.
You could run a count w/ foreach on each level of the array, and use the count number returned +1 for each level the foreach loops through.
I'm not sure if this answers your question, but I am trying to see exactly what it is you are trying to achieve.
As you are already using a recursive function to display that data, you can just extend your function. There is no need to traverse the array more often than one time:
function getWhitespaces($count) {
$result = '';
while($count--) {
$result .= '$nbsp;';
}
return $result;
}
function toOptions($array, $level=0) {
foreach ($array as $key=>$value) {
$html .= "<option value=\"" . $key . "\" >";
$html .= getWhitespaces($level) + $value['title'];
$html .= "</option>";
if (array_key_exists('children', $value)) {
$html .= toOptions($value['children'], $level + 1);
}
}
return $html;
}
print toOptions($list);
Try the following.. Your solution screams for recursion in my mind. Its a bit ugly but it seems to work
$totraverse = array(
'Moon' => array(
'Dark Side' => "Death Valley"
),
'Halley Commet' => "Solar System",
'World' => array(
'North America' => array(
'Canada' => array(
'City' => 'Toronto'
)
), 'South America' => array(
'Argentina' => array(
'City' => 'Toronto'
)
)
)
);
function traverse($totraverse_, $path="", $count=0) {
global $array;
// echo count($totraverse_) . " count\n";
if (!is_array($totraverse_)) {
echo "returning $path and $key\n";
return array($path, $count);
} else {
foreach ($totraverse_ as $key => $val) {
echo "assting $path and $key\n";
$result = traverse($val, $path . "/" . $key, $count + 1);
if($result){
$array[]=$result;
}
}
}
echo false;
}
$array = array();
traverse($totraverse);
foreach($array as $item){
echo "{$item[0]}--->{$item[1]}\n";
}

Categories