I have an array generated by PHP
array(
array(
'name'=>'node1',
'id' => '4'
),
array(
'name'=>'node2'
'id'=>'7'
)
)
And I am trying to add an array to a specific id (so let's say id 4)
'children'=>
array(
array('name'=>'node2','id'=>'5'),
array('name'=>'node3','id'=>'6')
)
So then it would look like
array(
array(
'name'=>'node1',
'id' => '4'
'children'=>
array(
array('name'=>'node2','id'=>'5'),
array('name'=>'node3','id'=>'6')
)
),
array(
'name'=>'node2'
'id'=>'7'
)
)
but I can't seem to figure out a way to search a multidimensional array, and add a multidimensional array to that array.
Any help?
Use a foreach loop to iterate through the array (making sure to get the key too), check the value, add if needed and break when found.
foreach($array as $k=>$v) {
if( $v['id'] == 4) {
$array[$k]['children'] = array(...);
break;
}
}
foreach($a as $k => $v)
{
if(is_array($v))
{
foreach($v as $ke => $va)
{
if($ke == 'children')
{
......
}
}
}
}
It's a monstrosity but it does what you want. Using push in a recursive function fails because the reference is destroyed after the second function call, so if you don't know how deep the key is there's a choice between using an arbitrary number of loops and hoping for the best, importing variables and using eval to push new values, or pulling apart and rebuilding the array. I chose eval. The description of what you wanted is a little different from pushing a value because you're looking for a key-value pair within an array, not an array. This function finds the key value pair and adds whatever you want as a sibling, if no value is specified the value is added as sibling to the first key matched. If no pushval is specified it will return the chain of keys that points to the matched key/key-value.
$ref_array is the multi-array to be modified,
$key is the key you're looking for,
$val is the value of the key you're looking for,
$newkey is the new key that will reference the new value,
$pushval is the new value to be indexed by $newkey.
DON'T pass an argument for the $val_array parameter. It's for use with recusive calls only. It's how the function distinguishes new calls from recursive calls, and it's how the function finds the key-value without disrupting the pass-by-reference.
function deepPush(&$ref_array, $key, $val=null, $newkey=null, $pushval=null, $val_array=null)
{
static $r, $keys;
#reset static vars on first call
if(!$val_array){ $r = 0; $keys = array();}
#cap recursion
if($r > 100){ trigger_error('Stack exceeded 100'); return;}
#init val_array
$val_array = ($r) ? $val_array : $ref_array;
#specified search value???
$search_val = ($val!==null && !in_array($val, $val_array)) ? true : false;
if(!array_key_exists($key, $val_array) || $search_val) {
$i=0;foreach($val_array as $k=>$v){
if(gettype($v) == 'array') {
if($i>0){/*dead-end*/array_pop($keys); /*keep recusion accurate*/$r-=$i;}
$keys[] = $k;/*build keychain*/
$r++; $i++; /*increment recursion, iteration*/
if(deepPush($ref_array, $key, $val, $newkey, $pushval, $v)){ /*close stack on 1st success*/return $keys;}
}//if
}//foreach
}//if
else{
if($pushval === null){return $keys;}
#add $newkey to the keychain
$keys[] = $newkey;
#process $pushval based on type
$pushval = (gettype($pushval) == 'string') ? sprintf("'%s'", $pushval) : var_export($pushval, true);
#link keys together to form pointer
$p = '$ref_array';
for($j=0,$c=count($keys); $j<$c; $j++) {
$k = $keys[$j];
$p .= "['$k']";
}//for
#concat the value to be pushed
$p .= sprintf("= %s;",$pushval);
#push it
eval($p);
$keys = array();
return true;
}//else
}
deepPush($array, 'id', 4, 'children', $addThis);
Related
I have this array:
$test = [
['teams' => ['home' => 'Lazio']],
['teams' => ['away' => 'Inter']]
];
I need to search Inter, so I did:
$key = array_search('Inter', array_column($test, 'teams'));
var_dump($test[$key]);
but it returns false as $key.
array_search() doesn't search nested arrays. There's no built-in functionthat does this, so just use a loop.
$key == null;
foreach (array_column($test, 'teams') as $i => $teams) {
if (in_array('Inter', $teams)) {
$key = $i;
break;
}
}
if ($key !== null) {
var_dump($test[$key]);
}
$needle = array_flip(['Inter']);
$result = array_filter(array_column($test, 'teams'), function(array $item) use ($needle) {
return array_intersect_key(array_flip($item), $needle);
});
using $needle as an array (and as result array_intersect_key) just in case you need to find more than one value.
You can change array_intersect_key to isset and single $needle value (instead of array).
also, is better to avoid using the array_search, (also in_array) functions for big arrays because of their complexity of algorithm (eq low performance)
I'm bored. You can filter out non-matching items and then get the key.
You can search for it anywhere in the array without specifying home, away or other:
$key = key(array_filter($test, function($v) { return in_array('Inter', $v['teams']); }));
Not the preferred way, but if there is only home and away you can search using an array:
($key = array_search(['away' => 'Inter'], array_column($test, 'teams'))) ||
($key = array_search(['home' => 'Inter'], array_column($test, 'teams')));
Or with one array_column call:
($t = array_column($test, 'teams')) && ($key = array_search(['away' => 'Inter'], $t)) ||
($key = array_search(['home' => 'Inter'], $t));
All of the code above returns the key of the first match.
I want to get sub array keys from array by less than values.
This is an example:
$arr_less_than = array(55,60,10,70);
$BlackList = array(10,8,15,20);
$MasterArray = array(
10 => array(1 => array(50,20,5,40), 2 => array(70,77,58,10), 3 => array(155,95,110,105), 4 => array(250,215,248,188)),
11 => array(1 => array(5,65,49,100), 2 => array(80,85,60,30), 3 => array(175,85,95,120), 4 => array(235,205,218,284)),
12 => array(1 => array(82,80,55,80), 2 => array(90,90,74,110), 3 => array(180,122,156,222), 4 => array(255,225,233,263)),
13 => array(1 => array(350,360,400,375), 2 => array(95,99,111,75), 3 => array(188,112,66,111), 4 => array(66,69,33,110)),
);
Now I need to get sub array keys from $MasterArray by less than $arr_less_than if the sub array key is not in the array $BlackList.
For the above example, the result must return array(12,13).
Note: I don't want to use a foreach loop
There are 2 solutions here - one for all sub-arrays meet the criteria, and one for any sub-array meets the criteria (which it seems is what the OP had in mind). At the end, there are foreach based solutions for the latter case where ANY sub-array meets the criteria.
If I understand the problem correctly, the goal is to identify rows in MasterArray where all of the sub-arrays have values that are greater than the corresponding value in $arr_less_than.
The OP does not want to use foreach (See the end of the answer for foreach based answers which are much simpler) - which may actually produce a more efficient version because it can avoid unnecessary compares and save some cycles, so here is a heavily annotated version using array functions.
I've excluded the data which can be copied from OP's post:
function getMatchingRows($arr_less_than, $MasterArray, $BlackList)
{
return array_values( // When we're done we just want a straight array of the keys from $MasterArray
array_filter(
array_keys($MasterArray), // Iterate over $MasterArray's keys (Because we need the keys for the BlackList)
function ($v) use ($BlackList, $MasterArray, $arr_less_than) {
// Filter out MasterArray Entries that dont meet the criteria
echo "Evaluate $MasterArray[$v]" . PHP_EOL;
// Remove array entries whose key is in the BlackList
if (in_array($v, $BlackList)) {
echo "\tBlacklisted" . PHP_EOL;
return false;
}
// For each entry in the MasterArray value, add up the number of non-matching entries
$y = array_reduce(
$MasterArray[$v],
function ($c1, $sub) use ($arr_less_than) {
// For each subarray entry in a MasterArray value, reduce the array to a count
// of elements whose value is less than the corresponding value in the $arr_less_than
$s1 = array_reduce(
array_keys($sub),
function ($carry, $key) use ($sub, $arr_less_than) {
if ($sub[$key] <= $arr_less_than[$key]) {
return ++$carry;
}
},
0 // Initial value for the array_reduce method
);
// $s1 will be a count of non-matching values
return $c1 + $s1;
},
0 //Initial value for the array_reduce method
);
echo "\t$y" . PHP_EOL;
// Include the array value in the filter only if there are no non-matching values ($y == 0)
return !$y;
}
)
);
}
print_r(getMatchingRows($arr_less_than, $MasterArray, $BlackList));
The basic idea is to generate a list of keys from the outermost array - so we iterate over them with array_filter. Then we exclude those with a key in the blacklist. Rows that arent in the blacklist, are reduced into an integer by iterating over each sub=arrays values and comparing them positon-wise against $arr_less_than and adding 1 for each value that fails to be greater than the corresponding member in $arr_less_than. Then those values are summed for all of the members in the MasterArray row. If the result is zero, then the row passes. Finally, the ultimate result is passed to array_values to normalize the resulting array.
Note that this requires that all values be compared, even if the first sub-value in the first sub-array fails. For that reason a foreach approach that can escape may be more efficient.
This is essentially the same method without comments and couple of shortcuts:
function getMatchingRows($arr_less_than, $MasterArray, $BlackList)
{
return array_values( // When we're done we just want a straight array of the keys from $MasterArray
array_filter(
array_keys($MasterArray),
function ($v) use ($BlackList, $MasterArray, $arr_less_than) {
return !in_array($v, $BlackList) && !array_reduce(
$MasterArray[$v],
function ($c1, $sub) use ($arr_less_than) {
return $c1 ?: array_reduce(
array_keys($sub),
function ($carry, $key) use ($sub, $arr_less_than) {
return $carry ?: ($sub[$key] <= $arr_less_than[$key] ? 1 : 0);
},
0
);
},
0
);
}
)
);
}
Some of the methods in array_reduce are short-circuited using ?: operator since the actual count is irrelevant. Once the count exceeds zero, the row fails, regardless.
Here is similar code if the criterion is that AT LEAST ONE sub-array has all members greater than the reference array.
function getMatchingRowsAny($arr_less_than, $MasterArray, $BlackList)
{
return array_values( // When we're done we just want a straight array of the keys from $MasterArray
array_filter(
array_keys($MasterArray), // Iterate over $MastrArray's keys (Because we need the keys for theBlackList)
function ($v) use ($BlackList, $MasterArray, $arr_less_than) {
// Filter out MasterArray Entries that dont meet the criteria
echo "Evaluate \MasterArray[$v]" . PHP_EOL;
// Remove array entries whose key is in the BlackList
if (in_array($v, $BlackList)) {
echo "\tBlacklisted" . PHP_EOL;
return false;
}
// For each entry in the MasterArray value, add up the number of non-matching entries
$y = array_reduce(
$MasterArray[$v],
function ($c1, $sub) use ($arr_less_than) {
// For each subarray entry in a MasterArray value, reduce the array to a flag
// indicating if it has whose value is <= the corresponding value in the $arr_less_than
$s1 = array_reduce(
array_keys($sub),
function ($fail, $key) use ($sub, $arr_less_than) {
return $fail || $sub[$key] <= $arr_less_than[$key];
},
false
);
// This could be short-circuited above to avoid an unnecessary array_reduce call
return $c1 || !$s1;
},
false
);
echo "\t$y" . PHP_EOL;
// Include the array value in the filter if there are any matching values
return $y;
}
)
);
}
print_r(getMatchingRowsAny($arr_less_than, $MasterArray, $BlackList));
As an exercise (and because I'm a glutton for punishment) I rendered the same methods using foreach as both a generator and a function returning an array - mostly to illustrate that foreach may be the better choice, and is definitely simpler:
// Implemented as a generator - The associated foreach that uses it follows
function generateMatchingRows($arr_less_than, $MasterArray, $BlackList)
{
foreach ($MasterArray as $k => $v) {
if (in_array($k, $BlackList)) {
continue;
}
foreach ($v as $sub_array) {
$match = true;
foreach ($sub_array as $k1 => $v1) {
if ($v1 <= $arr_less_than[$k1]) {
$match = false;
break;
}
}
if ($match) {
yield $k;
break;
}
}
}
}
foreach (generateMatchingRows($arr_less_than, $MasterArray, $BlackList) as $k) {
echo $k . PHP_EOL; // Or push them onto an array
}
// Implemented as a function returning an array - classical approach - just return an array
function getMatchingRowsForEach($arr_less_than, $MasterArray, $BlackList)
{
$rv = [];
foreach ($MasterArray as $k => $v) {
if (in_array($k, $BlackList)) {
continue;
}
foreach ($v as $sub_array) {
$match = true;
foreach ($sub_array as $k1 => $v1) {
if ($v1 <= $arr_less_than[$k1]) {
$match = false;
break;
}
}
if ($match) {
$rv[] = $k;
break;
}
}
}
return $rv;
}
print_r(getMatchingRowsForEach($arr_less_than, $MasterArray, $BlackList));
I will have a lot unknown levels of array; I would like to delete the selected ones.
Example:
$data = array(
'level1' => array(
'level2' => array(
'level3' => array(
'level4' => 'the end level at currently example, but not always',
'level4_remain' => 'this data will remain'
),
'remain' => 'this data would not delete in this example too'
)
)
'root' => 'this data would not delete in this example'
);
I have a class called vars; it stores all variables globally that I can fetch at anytime; add at anytime, but now that I'm going to make the delete method, I worry about the multi-level issue:
class vars {
public static $data = array();
public static function get($key){
$key = explode('/', $key);
$v = self::$data;
foreach($key as $k)
{
if(!isset($v[$k]))
{
return null;
}
$v = &$v[$k];
}
return $v;
}
public static function has($key){
.........
}
public static function del($key){
$key = explode('/', $key);
//....Any idea how to do this....//
}
}
and I will use the get method like this:
$blabla = vars::get('level1/level2/level3/level4');
and return correct data, but in the del method, I have no idea what to do:
$deleted = vars::del('level1/level2/level3/level4');
It needs to be finished after deleting the array:
unset($data['level1']['level2']['level3']['level4']);
After doing some research I found this, but this is just for "set" level of array, not automatically set as many level as it can be:
foreach ($data as $k1 => $arr) {
foreach ($arr as $k2 => $arr2) {
foreach ($arr2 as $k3 => $arr3) {
if ($k3 == $key) {
unset($rules[$k1][$k2][$k3]);
}
}
}
}
and I think it can be done like this but is quite ugly:
foreach($data as $k1 => $arr1){
if(is_array($arr1)){
foreach($arr1 as $k2 => $arr2){
//...keep writing the if(is_array()){}else{} and foreach(){} for more level deeper...//
}
}else{
//...the unset should be here...//
}
}
After some research, eval might be harmful, so anyone have any ideas how to do this using any method except eval?
If you don't mind using eval...
public static function del($levels)
{
$lvls = explode('/', $levels);
$count = count($lvls);
$eval = '';
for ($i=0; $i<$count; ++$i) {
// Current Array Key
$key = $lvls[$i];
$eval .= "['$key']";
}
$eval = 'self::data' . $eval;
eval("unset($eval);");
}
However, running untrusted (like User input) through eval can be dangerous.
To avoid eval, you can go the recursive route. The trick is to loop over your indices and pass a subarray reference repeatedly, similar to your get function, just recursively.
public static function del($key){
$key = array_reverse( explode('/', $key) ); // _reverse for _pop later
del_r(self::$data,$key); // start the recursive function
}
private static function del_r(&$array,$keys) { // shouldn't be used from outside
$key = array_pop($keys); // extract next key, this removes it from $keys
if (count($keys)) { // still keys left -> recurse further
del_r($array[$key],$keys); // pass subarray by reference
}
else { // no more keys -> delete the element at the current $key
unset($array[$key]);
}
}
Depending on the number of keys, you can use array_shift() instead of array_pop() and remove the array_reverse().
$records = array(
'123PP' => 3.63,
'123DDD' => 9.63,
'123D' => 6.63,
'123PPPP' => 9.63,
'123DD' => 9.63,
'123P' => 2.63,
'123PPP' => 1.53
);
After looping through the records, I have to get only one value
whose key should be 123D because the order of preference is:
123D, 123P, 123DD, 123PP, 123DDD, 123PPP, 123PPPP...
For e.g.:
If 123D is not found in the array, then 123P is the answer.
If 123P is not found in the array, then 123DD is the answer.
And I have found a solution :
foreach ($records as $key => $value) {
if (empty($this->minLength)) {
$this->invoiceTax = $value;
$this->minLength = strlen($key);
}
elseif (strpos($key, 'P') !== false && (strlen($key) < $this->minLength)) {
$this->invoiceTax = $value;
$this->minLength = strlen($key);
}
elseif (strpos($key, 'D') !== false && (strlen($key) <= $this->minLength)) {
$this->invoiceTax = $value;
$this->minLength = strlen($key);
}
But I want to know if this code can be optimised by not storing the string length of every key.
This function could easily be tidied but this is something that could be solved with recursion. This means that if 123D is in the array the code will be highly optimised and only run once, twice for 123P, three times for 123DD, etc.
function GetMostPref($records, $key = "123", $next = "D", $depth = 0)
{
if($depth == count($records))
{
// Hit end of array with nothing found
return false;
}
if(strpos($next, 'D') !== false)
{
// Currently looking at a 'D...' key.
// Next key is the same length as this key just with Ps.
$nextKey = str_repeat('P', strlen($next));
}
else if(strpos($next, 'P') !== false)
{
// Currently looking at a 'P...' key.
// Next key has one extra char and is all Ds.
$nextKey = str_repeat('D', strlen($next)+1);
}
else
{
// Key not valid
return false;
}
if(array_key_exists($key.$next, $records))
{
// Found the key in the array so return it.
return $records[$key.$next];
}
else
{
// Recursive call with the next key and increased depth.
return GetMostPref($records, $key, $nextKey, $depth + 1);
}
}
// Testing
$records = array(
'123PP' => 3.63,
'123DDD' => 9.63,
'123D' => 6.63,
'123PPPP' => 9.63,
'123DD' => 9.63,
'123P' => 2.63,
'123PPP' => 1.53
);
// Finds 123D and returns 6.63
echo GetMostPref($records);
function prepareFunctionCall(){
$records = array('123PP' => 3.63,'123DDD' => 9.63,'123PPPP' => 9.63
,'123DD' => 9.63,'123P' => 2.63,'123PPP' => 1.53);
// '123D' => 6.63,
foreach($records as $key=>$value){
$tmp = strlen($key).$key;
$arTmp[$tmp] = $value;
}
getFirstValue($arTmp);
}
function getFirstValue($pArray){
ksort($pArray);
reset($pArray);
print key($pArray).' = '.current($pArray);
}
This is an alternative for the good solution provided by MatthewMcGovern.
I provide the alternative, because this function makes use of the php functions ksort, reset and current. Those functions are created for this type of situation and if possible then would I advise you to rewrite the keys of the array before finding out which key is the first key to select. That is what I did with the addition of the strlen. But that is suboptimal compared to rewriting the keys at the moment of collecting the data. The core of the function are the calls to the functions ksort, reset and current.
Example:
$arr = array(array("name"=>"Bob","species"=>"human","children"=>array(array("name"=>"Alice","age"=>10),array("name"=>"Jane","age"=>13)),array("name"=>"Sparky","species"=>"dog")));
print_r($arr);
array_walk_recursive($arr, function($v,$k) {
echo "key: $k\n";
});
The thing here is that I get only the last key, but I have no way to refer where I been, that is to store a particular key and change value after I left the function or change identical placed value in another identical array.
What I would have to get instead of string is an array that would have all keys leading to given value, for example [0,"children",1,"age"].
Edit:
This array is only example. I've asked if there is universal way to iterate nested array in PHP and get full location path not only last key. And I know that there is a way of doing this by creating nested loops reflecting structure of the array. But I repeat: I don't know the array structure in advance.
To solve your problem you will need recursion. The following code will do what you want, it will also find multiple paths if they exists:
$arr = array(
array(
"name"=>"Bob",
"species"=>"human",
"children"=>array(
array(
"name"=>"Alice",
"age"=>10
),
array(
"name"=>"Jane",
"age"=>13
)
),
array(
"name"=>"Sparky",
"species"=>"dog"
)
)
);
function getPaths($array, $search, &$paths, $currentPath = array()) {
foreach ($array as $key => $value) {
if (is_array($value)) {
$currentPath[] = $key;
if (true !== getPaths($value, $search, $paths, $currentPath)) {
array_pop($currentPath);
}
} else {
if ($search == $value) {
$currentPath[] = $key;
$paths[] = $currentPath;
return true;
}
}
}
}
$paths = array();
getPaths($arr, 13, $paths);
print_r($paths);