181

I know I can do it using loops, but I'm trying to find an elegant way of doing this:

I have two jagged arrays (array of arrays):

var array1 = [['a', 'b'], ['b', 'c']];
var array2 = [['b', 'c'], ['a', 'b']];

I want to use lodash to confirm that the above two jagged arrays are the same. By 'the same' I mean that there is no item in array1 that is not contained in array2. Notice that the items in jagged array are actually arrays. So I want to compare between inner arrays.

In terms of checking equality between these items:

['a', 'b'] == ['b', 'a'] 

or

['a', 'b'] == ['a', 'b'] 

Both work since the letters will always be in order.


UPDATE: Original question was talking about to "arrays" (instead of jagged arrays) and for years many people discussed (and added answers) about comparing simple one-dimensional arrays (without noticing that the examples provided in the question were not actually similar to the simple one-dimensional arrays they were expecting).

Mariano Desanze
  • 7,847
  • 7
  • 46
  • 67
pQuestions123
  • 4,471
  • 6
  • 28
  • 59

9 Answers9

337

If you sort the outer array, you can use _.isEqual() since the inner array is already sorted.

var array1 = [['a', 'b'], ['b', 'c']];
var array2 = [['b', 'c'], ['a', 'b']];
_.isEqual(array1.sort(), array2.sort()); //true

Note that .sort() will mutate the arrays. If that's a problem for you, make a copy first using (for example) .slice() or the spread operator (...).

Or, do as Daniel Budick recommends in a comment below:

_.isEqual(_.sortBy(array1), _.sortBy(array2))

Lodash's sortBy() will not mutate the array.

Trott
  • 66,479
  • 23
  • 173
  • 212
  • 10
    Take into account that array.sort() is mutating the original array. Maybe this one might be better: var array1 = [['a', 'b'], ['b', 'c']]; var array2 = [['b', 'c'], ['a', 'b']]; _.isEqual([...array1].sort(), [...array2].sort()); //true – Yaniv Efraim Nov 08 '17 at 14:04
  • 3
    Added two sentences noting that `.sort()` mutates and suggesting options for copying first if that's a problem for the user. – Trott Nov 22 '17 at 03:16
  • 23
    If you are already using lodash, you could just do `_.isEqual(_.sortBy(array1), _sortBy(array2))` to prevent mutation. – Daniel Budick Dec 18 '18 at 11:53
  • 2
    @DanielBudick Thanks! I've added that to the answer. Great suggestion. – Trott Dec 18 '18 at 15:46
60

You can use lodashs xor for this

doArraysContainSameElements = _.xor(arr1, arr2).length === 0

If you consider array [1, 1] to be different than array [1] then you may improve performance a bit like so:

doArraysContainSameElements = arr1.length === arr2.length && _.xor(arr1, arr2).length === 0
Felix K.
  • 14,171
  • 9
  • 58
  • 72
Stephan Hoyer
  • 4,792
  • 2
  • 29
  • 26
  • 1
    Arrays need to be sorted anyway. – Sovattha Sok Mar 12 '19 at 13:37
  • 2
    This should be the better way for newer version – Leonardo May 23 '19 at 07:05
  • 1
    @Sovattha Sok The arrays don't need to be sorted. Doing `_.xor([3,4], [4,3]).length == 0` will get you true. – Nikolay Nov 04 '19 at 21:12
  • 7
    A word of caution: This technique works well for "small" arrays but it can be a pain performance and memory-wise if your arrays are huge and are mostly different because _.xor() will keep going waaaay past the first difference. In other words it doesn't return fast on the first difference detected. – XDS Nov 27 '19 at 15:42
  • By the way, lodash now includes [xorWith](https://lodash.com/docs/4.17.15#xorWith) which could be used for deep comparison like this: `_.xorWith(objects, others, _.isEqual);`. – savbace Jun 24 '21 at 11:58
  • 1
    The xor is 60-80% slower (depends on browser & machine) than the sorted array solution. Perf comparison: https://jsbench.me/drksdwsbgk/1 – Agent47DarkSoul Aug 16 '21 at 00:44
8

There are already answers here, but here's my pure JS implementation. I'm not sure if it's optimal, but it sure is transparent, readable, and simple.

// Does array a contain elements of array b?
const union = new Set([...a, ...b]);
const contains = (a, b) => union.size === a.length && union.size === b.length;
// Since order is not important, just data validity.
const isEqualSet = (a, b) => union.contains(a, b) || union.contains(b, a)

The rationale in contains() is that if a does contain all the elements of b, then putting them into the same set would not change the size.

For example, if const a = [1,2,3,4] and const b = [1,2], then new Set([...a, ...b]) === {1,2,3,4}. As you can see, the resulting set has the same elements as a.

From there, to make it more concise, we can boil it down to the following:

const isEqualSet = (a: string[], b: sting[]): boolean => {
  const union = new Set([...a, ...b])
  return union.size === a.length && union.size === b.length;
}

Edit: This will not work with obj[{a: true}, true, 3] but does compare array contents probably as long as they are primitive elements. Method fixed and tested against strings two arrays using the same values in different orders. Does not work with object types. I recommend making a universal helper which calls a helper function depending on the type which needs to be compared. Try _.isEqual(a. b); from the very fantastic lodash library.

AlphaG33k
  • 1,588
  • 1
  • 12
  • 24
J.Ko
  • 964
  • 12
  • 26
  • I really like this approach, but does it work when `a` and `b` have each duplicate elements that are not the same between them? like a = [ 1, 1, 2, 3] and b = [1, 2, 2, 3] – germanio Dec 28 '21 at 18:49
  • ok I ran a quick test and it doesn't, be careful with that – germanio Dec 28 '21 at 18:52
7

By 'the same' I mean that there are is no item in array1 that is not contained in array2.

You could use flatten() and difference() for this, which works well if you don't care if there are items in array2 that aren't in array1. It sounds like you're asking is array1 a subset of array2?

var array1 = [['a', 'b'], ['b', 'c']];
var array2 = [['b', 'c'], ['a', 'b']];

function isSubset(source, target) {
    return !_.difference(_.flatten(source), _.flatten(target)).length;
}

isSubset(array1, array2); // → true
array1.push('d');
isSubset(array1, array2); // → false
isSubset(array2, array1); // → true
Adam Boduch
  • 11,023
  • 3
  • 30
  • 38
2

PURE JS (works also when arrays and subarrays has more than 2 elements with arbitrary order). If strings contains , use as join('-') parametr character (can be utf) which is not used in strings

array1.map(x=>x.sort()).sort().join() === array2.map(x=>x.sort()).sort().join()

var array1 = [['a', 'b'], ['b', 'c']];
var array2 = [['b', 'c'], ['b', 'a']];

var r = array1.map(x=>x.sort()).sort().join() === array2.map(x=>x.sort()).sort().join();

console.log(r);
Kamil Kiełczewski
  • 85,173
  • 29
  • 368
  • 345
0

I definitely feel very unclean for posting this solution, but:

var array1 = [['a', 'b'], ['b', 'c']];
var array2 = [['b', 'c'], ['a', 'b']];
_.isMatch([array1], [array2]) && _.isMatch([array2], [array1]) // true

array1 = [['b', 'a'], ['c', 'b']];
array2 = [['b', 'c'], ['a', 'b']];
_.isMatch([array1], [array2]) && _.isMatch([array2], [array1]) // also true

Note that you have to wrap array1 and array2 into a container (array, object) in order for this to work? Why? There's probably a perfectly stupid reason for this.

Dejay Clayton
  • 3,710
  • 2
  • 29
  • 20
0
import { differenceBy } from 'lodash'

export default function (arr1, arr2) {
    return !differenceBy(arr1, arr2).length && arr1.length === arr2.length
}

if there are no different characters and the array length is the same, it makes them the same.

-1

Edit: I missed the multi-dimensional aspect of this question, so I'm leaving this here in case it helps people compare one-dimensional arrays

It's an old question, but I was having issues with the speed of using .sort() or sortBy(), so I used this instead:

function arraysContainSameStrings(array1: string[], array2: string[]): boolean {
  return (
    array1.length === array2.length &&
    array1.every((str) => array2.includes(str)) &&
    array2.every((str) => array1.includes(str))
  )
}

It was intended to fail fast, and for my purposes works fine.

  • Is the last check really necessary? `array1.every((str) => array2.includes(str))` should be enough. Also the OP wanted to use lodash, you should at least say why you propose a vanillaJS solution (... now that we have every and includes ...). Please also provide an example how to apply your function to the problem provided (2-dimensional arrays). – line-o Apr 07 '20 at 12:30
  • That's a good point - I didn't address the multi-dimensional aspect of it, not the requirement for lodash. I thought it would useful that anyone searching (as I did) for `lodash methods to compare arrays without considering order` to see an alternative in modern Javascript. The second check is necessary as `arraysContainSameStrings(['1', '2', '2'], ['1', '2', '3'])` would return true otherwise. I'll leave it here, as it might help, but I appreciate I haven't answered the question – charliematters Apr 07 '20 at 13:45
-2

We can use _.difference function to see if there is any difference or not.

function isSame(arrayOne, arrayTwo) {
   var a = _.uniq(arrayOne),
   b = _.uniq(arrayTwo);
   return a.length === b.length && 
          _.isEmpty(_.difference(b.sort(), a.sort()));
}

// examples
console.log(isSame([1, 2, 3], [1, 2, 3])); // true
console.log(isSame([1, 2, 4], [1, 2, 3])); // false
console.log(isSame([1, 2], [2, 3, 1])); // false
console.log(isSame([2, 3, 1], [1, 2])); // false

// Test cases pointed by Mariano Desanze, Thanks.
console.log(isSame([1, 2, 3], [1, 2, 2])); // false
console.log(isSame([1, 2, 2], [1, 2, 2])); // true
console.log(isSame([1, 2, 2], [1, 2, 3])); // false

I hope this will help you.

Adding example link at StackBlitz

Amitesh
  • 2,917
  • 2
  • 19
  • 14
  • 5
    Wrong, your function will get `true` for `console.log(isSame([1,2], [2,3,1]));` – David Lin Jan 23 '17 at 03:55
  • 3
    Thank you @DavidLin to point it out. I have made the changes to consider that case. Thank you and sorry for inconvenience. – Amitesh Jan 30 '17 at 11:31
  • 2
    you don't need to switch places for a & b if lengths are not the same. If lengths differ then they can't be the same already, so if(arrayOne.lenght !== arrayTwo.lenght) return false; – Alex Oct 12 '18 at 09:30
  • 1
    -1 No point in having those `a` and `b` variables. You **only** use those variables inside the `if-then` part, and the first thing you do there is discard those values loaded at line 2. I think the following single line will work exactly the same: `return arrayOne.length <= arrayTwo.length && _.isEmpty(_.difference(arrayTwo.sort(), arrayTwo.sort());`. And the `<=` can also be improved into `===`. – Mariano Desanze Oct 08 '19 at 20:04
  • 1
    And `_.difference` will return missing items of 1st argument, but not missing items in the 2nd one. So this will incorrectly return `true` when you repeat items on 1st in 2nd: `isSame([1, 2, 3], [1, 2, 2])`. – Mariano Desanze Oct 08 '19 at 20:13
  • thanks @MarianoDesanze, +1 for your suggestion and added it in test cases. Taken care by removing duplication by unique function. – Amitesh Jan 25 '20 at 07:22
  • Making changes for latest Lodash v4.17.15 and incorporated the @mariano Desanze suggestion. – Amitesh Jan 31 '21 at 15:58
  • 1
    @Amitesh I just realized that this answer (and some others) are actually about comparing simple one-dimensional arrays. The question has examples showing jagged arrays but its text was not clear. So I modified the question to make it clearer. But even with simple arrays, I think is better to just use Lodash's isEqual (which seems to work the same for simple arrays but not to jagged arrays): https://stackblitz.com/edit/using-lodash-to-compare-arrays-items-existence-without-o-vwopnr – Mariano Desanze Jan 31 '21 at 19:02