I'm looking for some tool to give me a recursive diff of two arrays. What I envision is a web page with two color-coded tree-structures. On each tree, green are parts of the array which match in both arrays, and red is for parts of each that don't match the other. Something like the output of dBug
I have some code that gives me a nested array to populate a report. I'm developing a new method that should be faster, but I need to test the values and also the structure, to make sure it gives output identical to the old method.
Is there something out there that I can use? Or do I need to write this? Or is there another way to accomplish my goals?
There is one such function implemented in the comments of array_diff.
function arrayRecursiveDiff($aArray1, $aArray2) {
$aReturn = array();
foreach ($aArray1 as $mKey => $mValue) {
if (array_key_exists($mKey, $aArray2)) {
if (is_array($mValue)) {
$aRecursiveDiff = arrayRecursiveDiff($mValue, $aArray2[$mKey]);
if (count($aRecursiveDiff)) { $aReturn[$mKey] = $aRecursiveDiff; }
} else {
if ($mValue != $aArray2[$mKey]) {
$aReturn[$mKey] = $mValue;
}
}
} else {
$aReturn[$mKey] = $mValue;
}
}
return $aReturn;
}
The implementation only handles two arrays at a time, but I do not think that really posses a problem. You could run the diff sequentially if you need the diff of 3 or more arrays at a time. Also this method uses key checks and does a loose verification.
The accepted answer is close to correct, but it doesn't really emulate array_diff correctly.
There are two problems that largely revolve around key matching:
array_diff has a specific behavior where it does not produce a result for an array key that is completely missing from the second array if its value is still in the second array. If you have two arrays $first = ['foo' => 2, 'moo' => 2] and $second = ['foo' => 2], using the accepted answer's function the output will be ['moo' => 2]. If you run the same arrays through array_diff, it will produce an empty array. This is because the above function's final else statement adds it to the diff if the array key is missing, but that's not the expected behavior from array_diff. The same is true with these two arrays: $first = ['foo' => 1] and $second = [1]. array_diff will produce an empty array.
If two arrays have the same values but different keys, it returns more values than expected. If you have two arrays $foo = [1, 2] and $moo = [2, 1], the function from the accepted answer will output all values from $foo. This is because it's doing a strict key matching on each iteration where it finds the same key (numerical or otherwise) in both arrays instead of checking all of the other values in the second array.
The following function is similar, but acts more closely to how you'd expect array_diff to work (also with less silly variable names):
function array_diff_recursive($arr1, $arr2)
{
$outputDiff = [];
foreach ($arr1 as $key => $value)
{
//if the key exists in the second array, recursively call this function
//if it is an array, otherwise check if the value is in arr2
if (array_key_exists($key, $arr2))
{
if (is_array($value))
{
$recursiveDiff = array_diff_recursive($value, $arr2[$key]);
if (count($recursiveDiff))
{
$outputDiff[$key] = $recursiveDiff;
}
}
else if (!in_array($value, $arr2))
{
$outputDiff[$key] = $value;
}
}
//if the key is not in the second array, check if the value is in
//the second array (this is a quirk of how array_diff works)
else if (!in_array($value, $arr2))
{
$outputDiff[$key] = $value;
}
}
return $outputDiff;
}
function array_diff_assoc_recursive($array1, $array2)
{
foreach($array1 as $key => $value){
if(is_array($value)){
if(!isset($array2[$key]))
{
$difference[$key] = $value;
}
elseif(!is_array($array2[$key]))
{
$difference[$key] = $value;
}
else
{
$new_diff = array_diff_assoc_recursive($value, $array2[$key]);
if($new_diff != FALSE)
{
$difference[$key] = $new_diff;
}
}
}
elseif((!isset($array2[$key]) || $array2[$key] != $value) && !($array2[$key]===null && $value===null))
{
$difference[$key] = $value;
}
}
return !isset($difference) ? 0 : $difference;
}
Example:
$a = array(
"product_a" => array(
'description'=>'Product A',
'color'=>'Red',
'quantity'=>'5',
'serial'=>array(1,2,3)
),
"product_b" => array(
'description'=>'Product B'
)
);
$b = array(
"product_a" => array(
'description'=>'Product A',
'color'=>'Blue',
'quantity'=>'5',
'serial'=>array(1,2,5)
),
"product_b" => array(
'description'=>'Product B'
)
);
Output:
array_diff_assoc_recursive($a,$b);
Array
(
[product_a] => Array
(
[color] => Red
[serial] => Array
(
[2] => 3
)
)
)
Try this code:
function arrayDiffRecursive($firstArray, $secondArray, $reverseKey = false)
{
$oldKey = 'old';
$newKey = 'new';
if ($reverseKey) {
$oldKey = 'new';
$newKey = 'old';
}
$difference = [];
foreach ($firstArray as $firstKey => $firstValue) {
if (is_array($firstValue)) {
if (!array_key_exists($firstKey, $secondArray) || !is_array($secondArray[$firstKey])) {
$difference[$oldKey][$firstKey] = $firstValue;
$difference[$newKey][$firstKey] = '';
} else {
$newDiff = arrayDiffRecursive($firstValue, $secondArray[$firstKey], $reverseKey);
if (!empty($newDiff)) {
$difference[$oldKey][$firstKey] = $newDiff[$oldKey];
$difference[$newKey][$firstKey] = $newDiff[$newKey];
}
}
} else {
if (!array_key_exists($firstKey, $secondArray) || $secondArray[$firstKey] != $firstValue) {
$difference[$oldKey][$firstKey] = $firstValue;
$difference[$newKey][$firstKey] = $secondArray[$firstKey];
}
}
}
return $difference;
}
$differences = array_replace_recursive(
arrayDiffRecursive($firstArray, $secondArray),
arrayDiffRecursive($secondArray, $firstArray, true)
);
var_dump($differences);
The answer by Mohamad is working good, except that it needs change on the line:
$difference[$newKey][$firstKey] = $secondArray[$firstKey];
with:
$difference[$newKey][$firstKey] = array_key_exists($firstKey, $secondArray) ? $secondArray[$firstKey] : null;
or, if you are using Laravel, with:
$difference[$newKey][$firstKey] = array_get($secondArray, $firstKey);
Otherwise, you will get errors like
PHP error: Undefined index: some_key
when a some_key exists in $secondArray but not in $firstArray
Related
How can I do a deep extension of a multi dimensional associative array (for use with decoded JSON objects).
I need the php equivalent of jQuery's $.extend(true, array1, array2) with arrays instead of JSON and in PHP.
Here's an example of what I need (array_merge_recursive didn't seem to do the same thing)
$array1 = ('1'=> ('a'=>'array1a', 'b'=>'array1b'));
$array2 = ('1'=> ('a'=>'array2a', 'c'=>'array2b'));
$array3 = array_extend($array1, $array2);
//$array3 = ('1'=> ('a'=>'array2a', 'b'=>'array1b', 'c'=>'array2b'))
Notice how array2 overrides array1 if it has same value (like how extension of classes works)
If you have PHP 5.3.0+, you can use array_replace_recursive which does exactly what you need:
array_replace_recursive() replaces the values of array1 with the same
values from all the following arrays. If a key from the first array
exists in the second array, its value will be replaced by the value
from the second array. If the key exists in the second array, and not
the first, it will be created in the first array. If a key only exists
in the first array, it will be left as is. If several arrays are
passed for replacement, they will be processed in order, the later
array overwriting the previous values.
This might be what you're looking for:
function array_extend(&$result) {
if (!is_array($result)) {
$result = array();
}
$args = func_get_args();
for ($i = 1; $i < count($args); $i++) {
// we only work on array parameters:
if (!is_array($args[$i])) continue;
// extend current result:
foreach ($args[$i] as $k => $v) {
if (!isset($result[$k])) {
$result[$k] = $v;
}
else {
if (is_array($result[$k]) && is_array($v)) {
array_extend($result[$k], $v);
}
else {
$result[$k] = $v;
}
}
}
}
return $result;
}
Usage:
$arr1 = array('a' => 1, 'b' => 2, 'c' => 3);
$arr2 = array('b' => 'b', 'd' => 'd');
array_extend($arr1, $arr2);
print_r($arr1); // array('a' => 1, 'b' => 'b', 'c' => 3, 'd' => 'd')
// or, to create a new array and leave $arr1 unchanged use:
array_extend($arr3, $arr1, $arr2);
print_r($arr3); // array('a' => 1, 'b' => 'b', 'c' => 3, 'd' => 'd')
// or, use the return value:
print_r(array_extend($arr1, $arr2)); // but this will also modify $arr1
I use this in the same way I use angular.extend(dst, src) and jQuery.extend().
function extend($base = array(), $replacements = array()) {
$base = ! is_array($base) ? array() : $base;
$replacements = ! is_array($replacements) ? array() : $replacements;
return array_replace_recursive($base, $replacements);
}
Example:
si() is a utility sanitize function that grabs $_POST or $_GET and returns an array.
$s = extend(array(
'page' => 1,
'take' => 100,
'completed' => 1,
'incomplete' => 1,
), si());
Taken from array_merge docs:
function array_extend($a, $b) {
foreach($b as $k=>$v) {
if( is_array($v) ) {
if( !isset($a[$k]) ) {
$a[$k] = $v;
} else {
$a[$k] = array_extend($a[$k], $v);
}
} else {
$a[$k] = $v;
}
}
return $a;
}
You should use: https://github.com/appcia/webwork/blob/master/lib/Appcia/Webwork/Storage/Config.php#L64
/**
* Merge two arrays recursive
*
* Overwrite values with associative keys
* Append values with integer keys
*
* #param array $arr1 First array
* #param array $arr2 Second array
*
* #return array
*/
public static function merge(array $arr1, array $arr2)
{
if (empty($arr1)) {
return $arr2;
} else if (empty($arr2)) {
return $arr1;
}
foreach ($arr2 as $key => $value) {
if (is_int($key)) {
$arr1[] = $value;
} elseif (is_array($arr2[$key])) {
if (!isset($arr1[$key])) {
$arr1[$key] = array();
}
if (is_int($key)) {
$arr1[] = static::merge($arr1[$key], $value);
} else {
$arr1[$key] = static::merge($arr1[$key], $value);
}
} else {
$arr1[$key] = $value;
}
}
return $arr1;
}
With a little googling I found this:
/**
* jquery style extend, merges arrays (without errors if the passed values are not arrays)
*
* #return array $extended
**/
function extend() {
$args = func_get_args();
$extended = array();
if(is_array($args) && count($args)) {
foreach($args as $array) {
if(is_array($array)) {
$extended = array_merge($extended, $array);
}
}
}
return $extended;
}
extend($defaults, $new_options);
I guess here is the correct answer, because:
your answer have a bug with warning:
Warning: Cannot use a scalar value as an array in...
Because $a is not always an array and you use $a[$k].
array_merge_recursive does indeed merge arrays, but it converts values with duplicate keys to arrays rather than overwriting the value in the first array with the duplicate value in the second array, as array_merge does.
other aswers are not recursives or not simple.
So, here is my answer: your answer without bugs:
function array_extend(array $a, array $b) {
foreach($b as $k=>$v) {
if( is_array($v) ) {
if( !isset($a[$k]) ) {
$a[$k] = $v;
} else {
if( !is_array($a[$k]){
$a[$k]=array();
}
$a[$k] = array_extend($a[$k], $v);
}
} else {
$a[$k] = $v;
}
}
return $a;
}
And my answer with ternary operator:
function array_extend(array $a, array $b){
foreach($b as $k=>$v)
$a[$k] = is_array($v)&&isset($a[$k])?
array_extend(is_array($a[$k])?
$a[$k]:array(),$v):
$v;
return $a;
}
Edit: And a bonus one with as many arrays you want:
function array_extend(){
$args = func_get_args();
while($extended = array_shift($args))
if(is_array($extended))
break;
if(!is_array($extended))
return FALSE;
while($array = array_shift($args)){
if(is_array($array))
foreach($array as $k=>$v)
$extended[$k] = is_array($v)&&isset($extended[$k])?
array_extend(is_array($extended[$k])?
$extended[$k]:array(),$v):
$v;
}
return $extended;
}
$arr1 = array('apple' => 1, 'mango'=>5, 'banana'=>3 );
$arr2 = array('apple' => 3, 'banana'=>2 );
my result array should be
array('apple'=>4, 'mango'=>5,'banana'=>5);
How can i do that?
You can do:
foreach($arr2 as $key=>$val) {
if(isset($arr1[$key])) {
$arr1[$key] += $val;
} else {
$arr1[$key] = $val;
}
}
http://www.ideone.com/rDFFW
If you really want a pretty one-liner then this would do for the present problem
array_walk($arr1, create_function('&$item,$key,$arr2', '$item += $arr2[$key] ;'),$arr2);
This uses the PHP trick of $array['non-existing-key'] evaluating to zero. However in real life I would have written this as
function walk(&$item,$key,$arr2) {
$item = array_key_exists($key,$arr2) ? $item + $arr2[$key] : $item;
}
array_walk($arr1,'walk',$arr2);
I'm looking for some tool to give me a recursive diff of two arrays. What I envision is a web page with two color-coded tree-structures. On each tree, green are parts of the array which match in both arrays, and red is for parts of each that don't match the other. Something like the output of dBug
I have some code that gives me a nested array to populate a report. I'm developing a new method that should be faster, but I need to test the values and also the structure, to make sure it gives output identical to the old method.
Is there something out there that I can use? Or do I need to write this? Or is there another way to accomplish my goals?
There is one such function implemented in the comments of array_diff.
function arrayRecursiveDiff($aArray1, $aArray2) {
$aReturn = array();
foreach ($aArray1 as $mKey => $mValue) {
if (array_key_exists($mKey, $aArray2)) {
if (is_array($mValue)) {
$aRecursiveDiff = arrayRecursiveDiff($mValue, $aArray2[$mKey]);
if (count($aRecursiveDiff)) { $aReturn[$mKey] = $aRecursiveDiff; }
} else {
if ($mValue != $aArray2[$mKey]) {
$aReturn[$mKey] = $mValue;
}
}
} else {
$aReturn[$mKey] = $mValue;
}
}
return $aReturn;
}
The implementation only handles two arrays at a time, but I do not think that really posses a problem. You could run the diff sequentially if you need the diff of 3 or more arrays at a time. Also this method uses key checks and does a loose verification.
The accepted answer is close to correct, but it doesn't really emulate array_diff correctly.
There are two problems that largely revolve around key matching:
array_diff has a specific behavior where it does not produce a result for an array key that is completely missing from the second array if its value is still in the second array. If you have two arrays $first = ['foo' => 2, 'moo' => 2] and $second = ['foo' => 2], using the accepted answer's function the output will be ['moo' => 2]. If you run the same arrays through array_diff, it will produce an empty array. This is because the above function's final else statement adds it to the diff if the array key is missing, but that's not the expected behavior from array_diff. The same is true with these two arrays: $first = ['foo' => 1] and $second = [1]. array_diff will produce an empty array.
If two arrays have the same values but different keys, it returns more values than expected. If you have two arrays $foo = [1, 2] and $moo = [2, 1], the function from the accepted answer will output all values from $foo. This is because it's doing a strict key matching on each iteration where it finds the same key (numerical or otherwise) in both arrays instead of checking all of the other values in the second array.
The following function is similar, but acts more closely to how you'd expect array_diff to work (also with less silly variable names):
function array_diff_recursive($arr1, $arr2)
{
$outputDiff = [];
foreach ($arr1 as $key => $value)
{
//if the key exists in the second array, recursively call this function
//if it is an array, otherwise check if the value is in arr2
if (array_key_exists($key, $arr2))
{
if (is_array($value))
{
$recursiveDiff = array_diff_recursive($value, $arr2[$key]);
if (count($recursiveDiff))
{
$outputDiff[$key] = $recursiveDiff;
}
}
else if (!in_array($value, $arr2))
{
$outputDiff[$key] = $value;
}
}
//if the key is not in the second array, check if the value is in
//the second array (this is a quirk of how array_diff works)
else if (!in_array($value, $arr2))
{
$outputDiff[$key] = $value;
}
}
return $outputDiff;
}
function array_diff_assoc_recursive($array1, $array2)
{
foreach($array1 as $key => $value){
if(is_array($value)){
if(!isset($array2[$key]))
{
$difference[$key] = $value;
}
elseif(!is_array($array2[$key]))
{
$difference[$key] = $value;
}
else
{
$new_diff = array_diff_assoc_recursive($value, $array2[$key]);
if($new_diff != FALSE)
{
$difference[$key] = $new_diff;
}
}
}
elseif((!isset($array2[$key]) || $array2[$key] != $value) && !($array2[$key]===null && $value===null))
{
$difference[$key] = $value;
}
}
return !isset($difference) ? 0 : $difference;
}
Example:
$a = array(
"product_a" => array(
'description'=>'Product A',
'color'=>'Red',
'quantity'=>'5',
'serial'=>array(1,2,3)
),
"product_b" => array(
'description'=>'Product B'
)
);
$b = array(
"product_a" => array(
'description'=>'Product A',
'color'=>'Blue',
'quantity'=>'5',
'serial'=>array(1,2,5)
),
"product_b" => array(
'description'=>'Product B'
)
);
Output:
array_diff_assoc_recursive($a,$b);
Array
(
[product_a] => Array
(
[color] => Red
[serial] => Array
(
[2] => 3
)
)
)
Try this code:
function arrayDiffRecursive($firstArray, $secondArray, $reverseKey = false)
{
$oldKey = 'old';
$newKey = 'new';
if ($reverseKey) {
$oldKey = 'new';
$newKey = 'old';
}
$difference = [];
foreach ($firstArray as $firstKey => $firstValue) {
if (is_array($firstValue)) {
if (!array_key_exists($firstKey, $secondArray) || !is_array($secondArray[$firstKey])) {
$difference[$oldKey][$firstKey] = $firstValue;
$difference[$newKey][$firstKey] = '';
} else {
$newDiff = arrayDiffRecursive($firstValue, $secondArray[$firstKey], $reverseKey);
if (!empty($newDiff)) {
$difference[$oldKey][$firstKey] = $newDiff[$oldKey];
$difference[$newKey][$firstKey] = $newDiff[$newKey];
}
}
} else {
if (!array_key_exists($firstKey, $secondArray) || $secondArray[$firstKey] != $firstValue) {
$difference[$oldKey][$firstKey] = $firstValue;
$difference[$newKey][$firstKey] = $secondArray[$firstKey];
}
}
}
return $difference;
}
$differences = array_replace_recursive(
arrayDiffRecursive($firstArray, $secondArray),
arrayDiffRecursive($secondArray, $firstArray, true)
);
var_dump($differences);
The answer by Mohamad is working good, except that it needs change on the line:
$difference[$newKey][$firstKey] = $secondArray[$firstKey];
with:
$difference[$newKey][$firstKey] = array_key_exists($firstKey, $secondArray) ? $secondArray[$firstKey] : null;
or, if you are using Laravel, with:
$difference[$newKey][$firstKey] = array_get($secondArray, $firstKey);
Otherwise, you will get errors like
PHP error: Undefined index: some_key
when a some_key exists in $secondArray but not in $firstArray
Is it possible in PHP to extract values from an array with a particular key path and return an array of those values? I'll explain with an example:
$user =
array (
array(
'id' => 1,
'email' =>'asd#example.com',
'project' => array ('project_id' => 222, 'project_name' => 'design')
),
array(
'id' => 2,
'email' =>'asd2#example.com',
'project' => array ('project_id' => 333, 'project_name' => 'design')
)
);
/** I have to write a function something like: */
$projectIds = extractValuesWithKey($user, array('project', 'project_id'));
print_r($projectIds);
Output:
Array(
[0] => 222,
[1] => 333
)
I would have gone for a different approach (not that there's anything wrong with the array-function-based answers) by using a recursive iterator to flatten the array which makes the key-path comparison fairly simple.
function extractValuesWithKey($array, $keys) {
$iterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($array));
$keys_count = count($keys);
// No point going deeper than we have to!
$iterator->setMaxDepth($keys_count);
$result = array();
foreach ($iterator as $value) {
// Skip any level that can never match our keys
if ($iterator->getDepth() !== $keys_count) {
continue;
}
// Build key path to current item for comparison
$key_path = array();
for ($depth = 1; $depth <= $keys_count; $depth++) {
$key_path[] = $iterator->getSubIterator($depth)->key();
}
// If key paths match, add to results
if ($key_path === $keys) {
$result[] = $value;
}
}
return $result;
}
To make the whole thing more useful, you could even wrap the code into a custom FilterIterator rather than a basic function, but I guess that's probably a different question entirely.
Well, that's easier than you think.
function extractValuesWithKey($array, $parts) {
$return = array();
$rawParts = $parts;
foreach ($array as $value) {
$tmp = $value;
$found = true;
foreach ($parts as $key) {
if (!is_array($tmp) || !isset($tmp[$key])) {
$found = false;
continue;
} else {
$tmp = $tmp[$key];
}
}
if ($found) {
$return[] = $tmp;
}
}
return $return;
}
If the 'key path' isn't dynamic, you can do a one-liner with array_map:
$projectIds = array_map(function($arr) { return $arr['project']['project_id']; }, $user);
Alternatively, for dynamic paths:
function extractValuesWithKey($users, $path) {
return array_map(function($array) use ($path) {
array_walk($path, function($key) use (&$array) { $array = $array[$key]; });
return $array;
}, $users);
}
The closures/anonymous functions only work with PHP 5.3+, and I've no idea how this would compare performance-wise to a double foreach loop. Note also that there's no error checking to ensure that the path exists.
I also used a similiar function in one of my projects, maybe you find this useful:
function extractValuesWithKey($data, $path) {
if(!count($path)) return false;
$currentPathKey = $path[0];
if(isset($data[$currentPathKey])) {
$value = $data[$currentPathKey];
return is_array($value) ? extractValuesWithKey($value, array_slice($path, 1)) : $value;
}
else {
$tmp = array();
foreach($data as $key => $value) {
if(is_array($value)) $tmp[] = extractValuesWithKey($value, $path);
}
return $tmp;
}
}
Example:
$arr = array(1 => 'Foo', 5 => 'Bar', 6 => 'Foobar');
/*... do some function so $arr now equals:
array(0 => 'Foo', 1 => 'Bar', 2 => 'Foobar');
*/
Use array_values($arr). That will return a regular array of all the values (indexed numerically).
PHP docs for array_values
array_values($arr);
To add to the other answers, array_values() will not preserve string keys. If your array has a mix of string keys and numeric keys (which is probably an indication of bad design, but may happen nonetheless), you can use a function like:
function reset_numeric_keys($array = array(), $recurse = false) {
$returnArray = array();
foreach($array as $key => $value) {
if($recurse && is_array($value)) {
$value = reset_numeric_keys($value, true);
}
if(gettype($key) == 'integer') {
$returnArray[] = $value;
} else {
$returnArray[$key] = $value;
}
}
return $returnArray;
}
Not that I know of, you might have already checked functions here
but I can imagine writing a simple function myself
resetarray($oldarray)
{
for(int $i=0;$i<$oldarray.count;$i++)
$newarray.push(i,$oldarray[i])
return $newarray;
}
I am little edgy on syntax but I guess u got the idea.