Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Finding matches between multiple JavaScript Arrays

I have multiple arrays with string values and I want to compare them and only keep the matching results that are identical between ALL of them.

Given this example code:

var arr1 = ['apple', 'orange', 'banana', 'pear', 'fish', 'pancake', 'taco', 'pizza'];
var arr2 = ['taco', 'fish', 'apple', 'pizza'];
var arr3 = ['banana', 'pizza', 'fish', 'apple'];

I would like to to produce the following array that contains matches from all given arrays:

['apple', 'fish', 'pizza']

I know I can combine all the arrays with var newArr = arr1.concat(arr2, arr3); but that just give me an array with everything, plus the duplicates. Can this be done easily without needing the overhead of libraries such as underscore.js?

(Great, and now i'm hungry too!)

EDIT I suppose I should mention that there could be an unknown amount of arrays, I was just using 3 as an example.

like image 248
FiniteLooper Avatar asked Jun 18 '12 01:06

FiniteLooper


3 Answers

var result = arrays.shift().filter(function(v) {
    return arrays.every(function(a) {
        return a.indexOf(v) !== -1;
    });
});

DEMO: http://jsfiddle.net/nWjcp/2/

You could first sort the outer Array to get the shortest Array at the beginning...

arrays.sort(function(a, b) {
    return a.length - b.length;
});

For completeness, here's a solution that deals with duplicates in the Arrays. It uses .reduce() instead of .filter()...

var result = arrays.shift().reduce(function(res, v) {
    if (res.indexOf(v) === -1 && arrays.every(function(a) {
        return a.indexOf(v) !== -1;
    })) res.push(v);
    return res;
}, []);

DEMO: http://jsfiddle.net/nWjcp/4/

like image 162
6 revsuser1106925 Avatar answered Nov 05 '22 05:11

6 revsuser1106925


Assuming there is an array of arrays those we want to find the intersection of, a simplest single liner approach could be

var arr = [[0,1,2,3,4,5,6,7,8,9],[0,2,4,6,8],[4,5,6,7]],
    int = arr.reduce((p,c) => p.filter(e => c.includes(e)));

document.write("<pre>" + JSON.stringify(int) + "</pre>");
like image 21
Redu Avatar answered Nov 05 '22 07:11

Redu


Now, that you've added an indeterminate number of arrays to the question, here's another approach that collects the count for each item into an object and then collates the items that have the max count.

Advantages of this approach:

  1. ~15x faster that brute force search options (used by other answers) if arrays are larger
  2. Does not require ES5 or ES5 shim (works with all browsers)
  3. Completely non-destructive (doesn't change source data at all)
  4. Handles duplicates items in source arrays
  5. Handles an arbitrary number of input arrays

And here's the code:

function containsAll(/* pass all arrays here */) {
    var output = [];
    var cntObj = {};
    var array, item, cnt;
    // for each array passed as an argument to the function
    for (var i = 0; i < arguments.length; i++) {
        array = arguments[i];
        // for each element in the array
        for (var j = 0; j < array.length; j++) {
            item = "-" + array[j];
            cnt = cntObj[item] || 0;
            // if cnt is exactly the number of previous arrays, 
            // then increment by one so we count only one per array
            if (cnt == i) {
                cntObj[item] = cnt + 1;
            }
        }
    }
    // now collect all results that are in all arrays
    for (item in cntObj) {
        if (cntObj.hasOwnProperty(item) && cntObj[item] === arguments.length) {
            output.push(item.substring(1));
        }
    }
    return(output);
}    

Working demo: http://jsfiddle.net/jfriend00/52mAP/

FYI, this does not require ES5 so will work in all browsers without a shim.

In a performance test on 15 arrays each 1000 long, this was more than 10x faster than the search method used in am not i am's answer in this jsperf: http://jsperf.com/in-all-arrays.


Here's a version that uses an ES6 Map and Set to de-dup and keep track of counts. This has the advantage that the type of data is preserved and can be anything (it doesn't even have to have a natural string conversion, the data can even be objects though objects are compared for being the exact same object, not having the same properties/values).

var arrays = [
    ['valueOf', 'toString','apple', 'orange', 'banana', 'banana', 'pear', 'fish', 'pancake', 'taco', 'pizza', 1, 2, 999, 888],
    ['valueOf', 'toString','taco', 'fish', 'fish', 'apple', 'pizza', 1, 999, 777, 999, 1],
    ['valueOf', 'toString','banana', 'pizza', 'fish', 'apple', 'apple', 1, 2, 999, 666, 555]
    ];
    
// subclass for updating cnts    
class MapCnt extends Map {
    constructor(iterable) {
        super(iterable);
    }
    
    cnt(iterable) {
        // make sure items from the array are unique
        let set = new Set(iterable);
        // now update the cnt for each item in the set
        for (let item of set) {
            let cnt = this.get(item) || 0;
            ++cnt;
            this.set(item, cnt);
        }
    }
}


function containsAll(...allArrays) {
    let cntObj = new MapCnt();
    for (array of allArrays) {
        cntObj.cnt(array);
    }
    // now see how many items have the full cnt
    let output = [];
    for (var [item, cnt] of cntObj.entries()) {
        if (cnt === allArrays.length) {
            output.push(item);
        }
    }
    return(output);
}    

var result = containsAll.apply(this, arrays);

document.body.innerHTML = "<pre>[<br>    " + result.join(',<br>    ') + "<br>]</pre>";
like image 16
jfriend00 Avatar answered Nov 05 '22 05:11

jfriend00