75

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.

Chris Barr
  • 29,851
  • 23
  • 95
  • 135
  • Take a look at this http://stackoverflow.com/questions/1885557/simplest-code-for-array-intersection-in-javascript – nbrooks Jun 18 '12 at 01:34

13 Answers13

102
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/

  • +1 because I didn't know you can do `JSON.stringify(result,null,4)`... Jaw dropped. – Derek 朕會功夫 Jun 18 '12 at 01:35
  • 1
    @Derek:: That was a relatively recent discovery for me as well. Check out the MDN docs. The second argument is really slick. Also, you don't need to pass a number. It can be a string that will be used as the indentation character. –  Jun 18 '12 at 01:36
  • Very cool, this is the simplest so far! I made an edit because I forgot to mention that this could be an unknown number of arrays, not necessarily limited to the 3 I used in this example. – Chris Barr Jun 18 '12 at 01:39
  • @amnotiam - Now *that's* a hidden function in JavaScript! – Derek 朕會功夫 Jun 18 '12 at 01:40
  • @ChrisBarr: Are the Arrays passed as function arguments? Or are they nested in another Array? –  Jun 18 '12 at 01:40
  • 1
    @amnotiam yes, they are nested in another array. I guess I need to get better and giving sample code... ha! – Chris Barr Jun 18 '12 at 01:42
  • ...there are a few ES5 methods being used. To support legacy implementations, you'd need to include compatibility patches. –  Jun 18 '12 at 01:45
  • @amnotiam This works great, but I noticed it doesn't handle duplicates within the same array - also legacy support as you mentioned. – Chris Barr Jun 18 '12 at 05:04
  • It is compact, but I noticed the duplicates issue too. Also, it's not very fast if the arrays are long. I've proposed an answer that is 10-15x faster (for larger arrays) and handles dups in the same array and does not require ES5. – jfriend00 Jun 18 '12 at 06:54
  • 1
    @ChrisBarr: Just to cover all the bases, I added a solution in the same style that deals with duplicates. It's at the bottom. –  Jun 18 '12 at 18:42
  • 1
    @amnotiam That rules, thank you so much! I really need to learn more about these built in methods, these are powerful. – Chris Barr Jun 19 '12 at 21:42
  • How can we achieve the opposite? Getting all the elements that are not common? Base on this answer? – Tom B. Mar 07 '17 at 08:28
  • 1
    @TomB.: Do you mean to **1)** keep items from the arrays that do not appear in *any* of the other arrays, or to **2)** keep items that do not appear in *at least one* of the other arrays? So if "pizza" was in the first and second arrays but not the third, it would *not* be included according to interpretation **1** but would be according to **2**. –  Mar 07 '17 at 15:03
  • @squint: keep all items that do not apear in at least one of the other arrays is what i'm looking for. – Tom B. Mar 08 '17 at 12:32
  • 1
    @TomB.: So basically remove all items that appear in all the lists. Here's one way: http://jsfiddle.net/nWjcp/296 Not the most efficient since it potentially compares all the items of each array against all the other arrays, but it gets it done. –  Mar 08 '17 at 16:05
33

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>");
Redu
  • 25,060
  • 6
  • 56
  • 76
  • 4
    I have no idea why this is not the correct answer - I know; was given 4 years later... But it should! Tx @Redu! – Pedro Ferreira Oct 06 '17 at 08:56
  • Perfect - there's always a great 1 liner for things like this. My only suggestion would be to give more meaningful names to the params because I can't tell what `p`, `c` and `e` represent. – ESR Aug 23 '18 at 09:26
  • 1
    @Edmund Reed Thanks... The variables `p` for previous and `c` for current are conventional for `reduce` operations with no initial value. `e` for element is a very common variable name for all array method callbacks. – Redu Aug 23 '18 at 10:25
  • Is there a way to fix the exception that occurs when `arr = []` or does it have to be checked beforehand? – Otto Abnormalverbraucher May 05 '21 at 13:13
  • @Otto Abnormalverbraucher In this case a `[]` is just like a `false` in an `&&` operation. The result is normally `[]`. I wouldn't call it an exception. Yet of course you can check in advance the arrays and prevent this if need be. – Redu May 05 '21 at 13:33
  • @Redu No. There literally appears an exception when you try to `.reduce(...)` on an empty array. You describe the case where `arr = [[]]`, in which case you have an empty entry in your array, but if there is no entry at all, the operation breaks. – Otto Abnormalverbraucher May 07 '21 at 14:40
  • 1
    @Otto Abnormalverbraucher As i mention in my answer we assume that there is an array of arrays which has a base case of `[[]]`. Yet... Fair enough comment. When fed with an empty array the exception mentions, `.reduce()` here doesn't use an initial value to start with. So without further ado perhaps doing like `arr.length ? arr.reduce((p,c) => p.filter(e => c.includes(e))) : [];` is sufficient. – Redu May 07 '21 at 15:13
  • @Redu Okay, that is what I already assumed. Coming from C# and Linq, which offers many XyzOrDefault functions, which offer an easy way to deal with null values, I had hoped there may be an easy solution I could add to the construct. Unfortunately, getting the `arr` in my case is already a huge array method construct so I cannot call it twice in one line but had to split it up. Thank you, though. – Otto Abnormalverbraucher May 09 '21 at 16:57
16

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>";
jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • Tweaked the algorithm to handle dups and added performance test to show how much faster it is than some of the other methods (14x faster). – jfriend00 Jun 18 '12 at 06:02
  • +1 I like the `containsAll` approach, I was thinking of an object-based approach but not using a counter. Nice handling of dups without removing them from the original array too. I think a lot of the speed comes from avoiding array methods like splice and slice, object property lookup is probably very highly optimised since it's so fundamental to any non-trivial script. – RobG Jun 18 '12 at 15:39
  • Oh, one problem with this approach is that in IE 8 and lower at least, properties "toString" and "valueOf" are always not enumerable, so if the arrays in question have those names as member values, the above will never put them in the results array. One solution is to test for those values on `item` explicitly. – RobG Jun 19 '12 at 01:51
  • @RobG - I modified the code to work with `"toString"`, `"valueOf"` in IE8 or any other built-in method. To do that, I add a prefix to each key to distinguish it from any built in methods. – jfriend00 Jun 19 '12 at 02:36
  • —another approach is to add a test of Object.prototype properties on a plain object to see which are never enumerable, then test them after the for..in at the end. – RobG Jun 19 '12 at 04:24
  • @RobG - I thought about doing it that way to build the list of troublesome property names, but I wasn't sure how one allows those enumerable properties to then be in the data structure without changing them to a new value and it seemed like a lot more code. – jfriend00 Jun 19 '12 at 04:29
  • —added a function to get the list to my answer. Will be moot when IE 8 and lower are no longer in use (assuming it's fixed in IE 9+ and sycophants). – RobG Jun 19 '12 at 05:03
  • It turns arrays of numbers into arrays of strings. – dtech May 05 '17 at 19:41
  • @dtech - Yes, this assumes strings as in the OP's question. It could be modified fairly easily to preserve the type of the data, but it assumes an auto-conversion to string for uniqueness comparisons. That assumption could be removed by using an ES6 `Map` or a similar type object. – jfriend00 May 05 '17 at 20:34
  • I ended up writing it in C++, but anyway, glad to see ES6 is finally getting some sane data structures :) It only took like 20 years. Too bad it won't likely be supported by the framework I am using any time soon: https://bugreports.qt.io/browse/QTBUG-47735 – dtech May 05 '17 at 21:21
  • @dtech - I added an ES6 version of the code that preserves the source data type and it can be anything. – jfriend00 May 05 '17 at 21:21
3

A couple thoughts- you can compare just the items in the shortest array, and prevent duplicates in the returned array.

function arraysInCommon(arrays){
    var i, common,
    L= arrays.length, min= Infinity;
    while(L){
        if(arrays[--L].length<min){
            min= arrays[L].length;
            i= L;
        }
    }
    common= arrays.splice(i, 1)[0];
    return common.filter(function(itm, indx){
        if(common.indexOf(itm)== indx){
            return arrays.every(function(arr){
                return arr.indexOf(itm)!= -1;
            });
        }
    });
}

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

var allArrays = [arr1,arr2,arr3];

arraysInCommon(allArrays).sort();

returned value: apple,fish,pizza

DEMO - http://jsfiddle.net/kMcud/

Chris Barr
  • 29,851
  • 23
  • 95
  • 135
kennebec
  • 102,654
  • 32
  • 106
  • 127
3
    // The easiest way!! 
    
    var arr1 = ['apple', 'orange', 'banana', 'pear', 'fish', 'pancake', 'taco', 'pizza'];
    var arr2 = ['taco', 'fish', 'apple', 'pizza'];
    var arr3 = ['banana', 'pizza', 'fish', 'apple'];
    var arr4 = [];


    for(let i of arr1){
      if(arr2.includes(i) && arr3.includes(i)){
        arr4.push(i)
      }
    }

    console.log(arr4)


------------- OR -----------------


arr4 = arr1.filter(value => arr2.includes(value) && arr3.includes(value))
md_salm
  • 459
  • 5
  • 9
2

Assuming array of arrays and checking through all arrays:

DEMO: http://jsfiddle.net/qUQHW/

var tmp = {};
for (i = 0; i < data.length; i++) {
    for (j = 0; j < data[i].length; j++) {
        if (!tmp[data[i][j]]) {
            tmp[data[i][j]] = 0;
        }
        tmp[data[i][j]]++;
    }
}

var results = $.map(tmp, function(val,key) {
    return val == data.length ? key :null;
})
charlietfl
  • 170,828
  • 13
  • 121
  • 150
2

Here goes a single-line solution. You can split it into two thinking steps:

  1. Calculate join/intersection between two arrays

var arrA = [1,2,3,4,5];
var arrB = [4,5,10];
var innerJoin = arrA.filter(el=>arrB.includes(el));
console.log(`Intersection is: ${innerJoin}`);
  1. Reduce the content: calculate the intersection between the acumulated intersection and the next array.

var arrays = [
 ['apple', 'orange', 'banana', 'pear', 'fish', 'pancake', 'taco', 'pizza'],
 ['taco', 'fish', 'apple', 'pizza'],
 ['banana', 'pizza', 'fish', 'apple']
];
var join = arrays.reduce((join, current) => join.filter(el => current.includes(el)));
console.log(`Intersection is: ${join}`);
dinigo
  • 6,872
  • 4
  • 37
  • 48
  • This doesn't work when using these two arrays as a test case. [1,2,2,1], [2]. It should return [2] but returns [2, 2]. – iPzard Jun 23 '19 at 17:54
1

This should work for any number of arrays:

function intersection(arr1, arr2) {
  var temp = [];

  for (var i in arr1) {
    var element = arr1[i];

    if (arr2.indexOf(element) > -1) {
      temp.push(element);
    }
  }

  return temp;
}

function multi_intersect() {
  var arrays = Array.prototype.slice.apply(arguments).slice(1);
  var temp = arguments[0];

  for (var i in arrays) {
    temp = intersection(arrays[i], temp);

    if (temp == []) {
      break;
    }
  }

  return temp;
}

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

multi_intersect(arr1, arr2, arr3);
Blender
  • 289,723
  • 53
  • 439
  • 496
0

Just for the heck of it, another long hand approach:

function getCommon(a) {

  // default result is copy of first array
  var result = a[0].slice();
  var mem, arr, found = false;

  // For each member of result, see if it's in all other arrays
  // Go backwards so can splice missing entries
  var i = result.length;

  while (i--) {
    mem = result[i];

    // Check in each array
    for (var j=1, jLen=a.length; j<jLen; j++) {
      arr = a[j];
      found = false;

      // For each member of arr and until found
      var k = arr.length;
      while (k-- && !found) {

        // If found in this array, set found to true
        if (mem == arr[k]) {
          found = true;
        }
      }
      // if word wasn't found in this array, remove it from result and 
      // start on next member of result, skip remaining arrays.
      if (!found) {
        result.splice(i,1);
        break;
      }
    }
  }
  return result;
}

var data = [
  ['taco', 'fish', 'apple', 'pizza', 'mango', 'pear'],
  ['apple', 'orange', 'banana', 'pear', 'fish', 'pancake', 'taco', 'pizza'],
  ['banana', 'pizza', 'fish', 'apple'],
  ['banana', 'pizza', 'fish', 'apple', 'mango', 'pear']
];

Edit

Function to find never enumerable properties based on thise on Object.prototype:

// Return an array of Object.prototype property names that are not enumerable
// even when added directly to an object.
// Can be helpful with IE as properties like toString are not enumerable even
// when added to an object.
function getNeverEnumerables() {

    // List of Object.prototype property names plus a random name for testing
    var spNames = 'constructor toString toLocaleString valueOf ' +
                  'hasOwnProperty isPrototypeOf propertyIsEnumerable foo';

    var spObj = {foo:'', 'constructor':'', 'toString':'', 'toLocaleString':'', 'valueOf':'',
                 'hasOwnProperty':'', 'isPrototypeOf':'', 'propertyIsEnumerable':''};

    var re = [];

    // BUild list of enumerable names in spObj
    for (var p in spObj) {
      re.push(p); 
    }

    // Remove enumerable names from spNames and turn into an array
    re = new RegExp('(^|\\s)' + re.join('|') + '(\\s|$)','g');
    return spNames.replace(re, ' ').replace(/(^\s+)|\s\s+|(\s+$)/g,'').split(' ');
}

document.write(getNeverEnumerables().join('<br>'));
RobG
  • 142,382
  • 31
  • 172
  • 209
0

This is essentially a compilation of all the answers boiled down:

  // Intersect any number of arrays:

 function intersect() {

   // - Arguments -> traditional array,
   // - First item ( arrays[0] ) = shortest to reduce iterations
   var arrays = Array.prototype.slice.call(arguments).sort(function(a, b) {
     return a.length - b.length;
   });

   // Use first array[0] as the base.
   var a = arrays.shift();

   var result = [];
   for (var i = a.length; i--;) {

     var val = a[i];

     // Prevent duplicates
     if (result.indexOf(val) < 0) {

       // Seek
       var found = true;
       for (var ii = arrays.length; ii--;) {
         if (arrays[ii].indexOf(val) < 0) {
           found = false;
           break;
         }
       }

       if (found) {
         result.push(val);
       }

     }

   }

   return result;

 }

 /*
 // Slower, but smaller code-base:
 function intersect (){
  
  // - Arguments -> traditional array,
  // - First item ( arrays[0] ) = shortest to reduce iterations
  var arrays = Array.prototype.slice.call(arguments).sort(function(a, b) {
         return a.length - b.length;
     });
  
  // Use first array[0] as the base.
  var a = arrays.shift();

  return a.filter(function (val, idx, aa) {
   
      // Seek
                  for(var i=arrays.length; i--;){
                      if (arrays[i].indexOf(val) < 0) {
           return false;
          }
                  }
      
      // Prevent duplicates
                  return aa.indexOf(val) === idx;
  
     });

 }
 */

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

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


 var result = intersect(arr1, arr2, arr3);

  // For fiddle output:
 var elem = document.getElementById("result");
 elem.innerHTML = JSON.stringify(result);
 console.log(result);
<div id="result">Results</div>
bob
  • 7,539
  • 2
  • 46
  • 42
0

You can use array#reduce and array#filter. For each array, get all unique values and in a Map lookup and keep their count. Once done, array#filter this lookup based on the length of array.

const commonElements = (...arr) => {
  const lookup = arr.reduce((map, a) => {
    const unique = [...new Set(a)];
    unique.forEach(v => {
      map.set(v, (map.get(v) || 0) + 1)
    });
    return map;
  },new Map());
  return [...lookup.keys()].filter(k => lookup.get(k) === arr.length);
}

const arr1 = ['apple', 'orange', 'banana', 'pear', 'fish', 'pancake', 'taco', 'pizza'],
      arr2 = ['taco', 'fish', 'apple', 'pizza'],
      arr3 = ['banana', 'pizza', 'fish', 'apple'];
console.log(commonElements(arr1,arr2,arr3));
Hassan Imam
  • 21,956
  • 5
  • 41
  • 51
0

Another one solution:

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

const result  = combinedArr
    .flatMap(([...values]) => values)
    .filter((value, index, coll) => (coll.indexOf(value) === index) && combinedArr.every(
        (values) => values.includes(value)
    ));
    
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
A1exandr Belan
  • 4,442
  • 3
  • 26
  • 48
0

This approach counts the number of each item. After all arrays have been iterated, the counts that equal the number of arrays are selected.

To be generic, the function accepts an array of arrays, and comparison function to generate the map key in cases where you are using objects instead of primitive values.

It returns an array of common values, using the instance from the first array as the reference.

function commonValues<T>(arrays: T[][], keyFn: (item: T) => string): T[] {
  const counts: Record<any, { count: number, item: T }> = {}
  for (const array of arrays) {
    for (const item of array) {
      const key = keyFn(item)
      let entry = counts[key]
      if (!entry) {
        entry = {count: 0, item}
        counts[key] = entry
      }
      entry.count++
    }
  }
  return Object.values(counts)
      .filter(it => it.count === arrays.length)
      .map(it => it.item)
}

// For this basic example, the key function simply returns the string.

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

const common = commonItems([arr1,arr2,arr3], it=>it)

function commonValues(arrays, keyFn) {
  const counts = {}
  for (const array of arrays) {
    for (const item of array) {
      const key = keyFn(item)
      let entry = counts[key]
      if (!entry) {
        entry = {count: 0, item}
        counts[key] = entry
      }
      entry.count++
    }
  }
  return Object.values(counts)
      .filter(it => it.count === arrays.length)
      .map(it => it.item)
}

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

const found = commonValues([arr1,arr2,arr3], it=>it)

console.log('common', found)
Steven Spungin
  • 27,002
  • 5
  • 88
  • 78