12

I am using array_diff() to take values out of array1 which are found in array2. The issue is it removes all occurrences from array1, as the PHP documentations makes note of. I want it to only take out one at a time.

$array1 = array();
$array1[] = 'a';
$array1[] = 'b';
$array1[] = 'a';

$array2 = array();
$array2[] = 'a';

It should return an array with one 'a' and one 'b', instead of an array with just 'b';

1
  • array_unique() returns an array without duplicate values. What I'm trying to do is subtract two arrays. array_diff() does this but it is not exact. Commented Jun 6, 2013 at 22:47

2 Answers 2

13

Just for the fun of it, something that just came to mind. Will work as long as your arrays contain strings:

$a = array('a','b','a','c');
$b = array('a');

$counts = array_count_values($b);
$a = array_filter($a, function($o) use (&$counts) {
    return empty($counts[$o]) || !$counts[$o]--;
});

It has the advantage that it only walks over each of your arrays just once.

See it in action.

How it works:

First the frequencies of each element in the second array are counted. This gives us an arrays where keys are the elements that should be removed from $a and values are the number of times that each element should be removed.

Then array_filter is used to examine the elements of $a one by one and remove those that should be removed. The filter function uses empty to return true if there is no key equal to the item being examined or if the remaining removal count for that item has reached zero; empty's behavior fits the bill perfectly.

If neither of the above holds then we want to return false and decrement the removal count by one. Using false || !$counts[$o]-- is a trick in order to be terse: it decrements the count and always evaluates to false because we know that the count was greater than zero to begin with (if it were not, || would short-circuit after evaluating empty).

Sign up to request clarification or add additional context in comments.

3 Comments

Twice. Once for array_count_values, once for array_filter. Still more efficient than above, which is O(N^2), though.
@SébastienRenauld: Once each, plus of course it does some hash lookups when iterating $a. But even so it's still O(N+M).
@SébastienRenauld: No worries. :-)
8

Write some function that removes elements from first array one by one, something like:

function array_diff_once($array1, $array2) {
    foreach($array2 as $a) {
        $pos = array_search($a, $array1);
        if($pos !== false) {
            unset($array1[$pos]);
        }
    }

    return $array1;
}

$a = array('a', 'b', 'a', 'c', 'a', 'b');
$b = array('a', 'b', 'c');

print_r( array_diff_once($a, $b) );

2 Comments

This code has a bug in it. $pos will, upon first non-match of an item in $array2 in $array1, (which this case isn't included in example), will then return false, which, in the unset() call, will be casted to integer zero, resulting in unset($array1[0]). To put this another way, this has a very serious flaw, that if there is an element in $array2 that is not present in $array1, then the first element of $array1 will not be considered for the array difference result, which resulted in a very serious bug in my production code because I blindly copied and pasted this, being fairly non-obvious.
yeah, indeed, I edited my post to include your fix :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.