9

I'm looking for an efficient way to find out whether two arrays contain same amounts of equal elements (in the == sense), in any order:

foo = {/*some object*/}
bar = {/*some other object*/}

a = [1,2,foo,2,bar,2]
b = [bar,2,2,2,foo,1]

sameElements(a, b) --> true

PS. Note that pretty much every solution in the thread uses === and not == for comparison. This is fine for my needs though.

georg
  • 211,518
  • 52
  • 313
  • 390
  • 1
    Well, typically `foo` and `bar` will never be equal in the `==` sense, or do you want to compare they properties (deeply?) in the case of an object element? – Matt Jun 04 '13 at 09:03
  • have a look here http://stackoverflow.com/questions/7837456/comparing-two-arrays-in-javascript – BeNdErR Jun 04 '13 at 09:04
  • 2
    @Matt: `foo` and `bar` don't have to be equal. The arrays contain both objects, `foo` and `bar`. I think that's the point (FWIW, I was confused first as well). – Felix Kling Jun 04 '13 at 09:07
  • Use a [`Set`](http://wiki.ecmascript.org/doku.php?id=harmony:simple_maps_and_sets) or a `Map` (with number of instances) instead of an array :-) – Bergi Jun 04 '13 at 12:04
  • @thg435 I updated the answer, i managed to optimize it a bit, giving it a performance boost of about 100% – Moritz Roessler Jun 13 '13 at 12:46
  • What exactly do you want to award the bounty for? Also, please describe the average multiset components: How many primitives, how many objects are in there? How often do they occur multiple times (e.g. the objects in your example don't do at all)? It will help to get more representative performance tests and better optimized solutions. – Bergi Jun 20 '13 at 22:18
  • I added the new answer, running on battery on my maching on chrome at ~100ops/s on firefox at ~80ops/s. – Moritz Roessler Jun 21 '13 at 07:25
  • @C5H8NNaO4: thank you very much once again for all your helpfulness and enthusiasm! – georg Jun 21 '13 at 07:46
  • @thg435 You are welcome :) it was a pleasure, i like stuff like this ;) – Moritz Roessler Jun 21 '13 at 07:49

8 Answers8

7

Update 5 I posted a new answer with a different approach.

Update

I extended the code to have the possibility of either checking by reference or equality

just pass true as second parameter to do a reference check.

Also I added the example to Brunos JSPerf

  • It runs at about 11 ops/s doing a reference check

I will comment the code as soon(!) as I get some spare time to explain it a bit more, but at the moment don't have the time for that, sry. Done

Update 2.

Like Bruno pointed out in the comments sameElements([NaN],[NaN]) yields false

In my opinion this is the correct behaviour as NaN is ambigious and should always lead to a false result,at least when comparing NaN.equals(NaN). But he had quite a good point.

Whether

[1,2,foo,bar,NaN,3] should be equal to [1,3,foo,NaN,bar,2] or not.

Ok.. honestly I'm a bit torn whether it should or not, so i added two flags.

  • Number.prototype.equal.NaN
    • If true
      • NaN.equals(NaN) //true
  • Array.prototype.equal.NaN
    • If true
      • [NaN].equals([NaN],true) //true
      • note this is only for reference checks. As a deep check would invoke Number.prototype.equals anyway

Update 3:

Dang i totally missed 2 lines in the sort function.

Added

 r[0] = a._srt; //DANG i totally missed this line
 r[1] = b._srt; //And this.

Line 105 in the Fiddle

Which is kind of important as it determines the consistent order of the Elements.

Update 4
I tried to optimize the sort function a bit, and managed to get it up to about 20 ops/s.

Below is the updated code, as well as the updated fiddle =)

Also i chose to mark the objects outside the sort function, it doesn't seem to make a performance difference anymore, and its more readable


Here is an approach using Object.defineProperty to add equals functions to

Array,Object,Number,String,Boolean's prototype to avoid typechecking in one function for performance reasons. As we can recursively call .equals on any element.

But of course checking Objects for equality may cause performance issues in big Objects.

So if anyone feels unpleasant manipulating native prototypes, just do a type check and put it into one function

Object.defineProperty(Boolean.prototype, "equals", {
        enumerable: false,
        configurable: true,
        value: function (c) {
            return this == c; //For booleans simply return the equality
        }
    });

Object.defineProperty(Number.prototype, "equals", {
        enumerable: false,
        configurable: true,
        value: function (c) {
            if (Number.prototype.equals.NaN == true && isNaN(this) && c != c) return true; //let NaN equals NaN if flag set
            return this == c; // else do a normal compare
        }
    });

Number.prototype.equals.NaN = false; //Set to true to return true for NaN == NaN

Object.defineProperty(String.prototype, "equals", {
        enumerable: false,
        configurable: true,
        value: Boolean.prototype.equals //the same (now we covered the primitives)
    });

Object.defineProperty(Object.prototype, "equals", {
        enumerable: false,
        configurable: true,
        value: function (c, reference) {
            if (true === reference) //If its a check by reference
                return this === c; //return the result of comparing the reference
            if (typeof this != typeof c) { 
                return false; //if the types don't match (Object equals primitive) immediately return
            }
            var d = [Object.keys(this), Object.keys(c)],//create an array with the keys of the objects, which get compared
                f = d[0].length; //store length of keys of the first obj (we need it later)
            if (f !== d[1].length) {//If the Objects differ in the length of their keys
                return false; //immediately return
            }
            for (var e = 0; e < f; e++) { //iterate over the keys of the first object
                if (d[0][e] != d[1][e] || !this[d[0][e]].equals(c[d[1][e]])) {
                    return false; //if either the key name does not match or the value does not match, return false. a call of .equal on 2 primitives simply compares them as e.g Number.prototype.equal gets called
                }
            }
            return true; //everything is equal, return true
        }
    });
Object.defineProperty(Array.prototype, "equals", {
        enumerable: false,
        configurable: true,
        value: function (c,reference) {

            var d = this.length;
            if (d != c.length) {
                return false;
            }
            var f = Array.prototype.equals.sort(this.concat());
            c = Array.prototype.equals.sort(c.concat(),f)

            if (reference){
                for (var e = 0; e < d; e++) {
                    if (f[e] != c[e] && !(Array.prototype.equals.NaN && f[e] != f[e] && c[e] != c[e])) {
                        return false;
                    }
                }                
            } else {
                for (var e = 0; e < d; e++) {
                    if (!f[e].equals(c[e])) {
                        return false;
                    }
                }
            }
            return true;

        }
    });

Array.prototype.equals.NaN = false; //Set to true to allow [NaN].equals([NaN]) //true

Object.defineProperty(Array.prototype.equals,"sort",{
  enumerable:false,
  value:function sort (curr,prev) {
         var weight = {
            "[object Undefined]":6,         
            "[object Object]":5,
            "[object Null]":4,
            "[object String]":3,
            "[object Number]":2,
            "[object Boolean]":1
        }
        if (prev) { //mark the objects
            for (var i = prev.length,j,t;i>0;i--) {
                t = typeof (j = prev[i]);
                if (j != null && t === "object") {
                     j._pos = i;   
                } else if (t !== "object" && t != "undefined" ) break;
            }
        }

        curr.sort (sorter);

        if (prev) {
            for (var k = prev.length,l,t;k>0;k--) {
                t = typeof (l = prev[k]);
                if (t === "object" && l != null) {
                    delete l._pos;
                } else if (t !== "object" && t != "undefined" ) break;
            }
        }
        return curr;

        function sorter (a,b) {

             var tStr = Object.prototype.toString
             var types = [tStr.call(a),tStr.call(b)]
             var ret = [0,0];
             if (types[0] === types[1] && types[0] === "[object Object]") {
                 if (prev) return a._pos - b._pos
                 else {
                     return a === b ? 0 : 1;
                 }
             } else if (types [0] !== types [1]){
                     return weight[types[0]] - weight[types[1]]
             }



            return a>b?1:a<b?-1:0;
        }

    }

});

With this we can reduce the sameElements function to

function sameElements(c, d,referenceCheck) {
     return c.equals(d,referenceCheck);  //call .equals of Array.prototype.
}

Note. of course you could put all equal functions into the sameElements function, for the cost of the typechecking.

Now here are 3 examples: 1 with deep checking, 2 with reference checking.

var foo = {
    a: 1,
    obj: {
        number: 2,
        bool: true,
        string: "asd"
    },
    arr: [1, 2, 3]
};

var bar = {
    a: 1,
    obj: {
        number: 2,
        bool: true,
        string: "asd"
    },
    arr: [1, 2, 3]
};

var foobar = {
    a: 1,
    obj: {
        number: 2,
        bool: true,
        string: "asd"
    },
    arr: [1, 2, 3, 4]
};

var a = [1, 2, foo, 2, bar, 2];
var b = [foo, 2, 2, 2, bar, 1];
var c = [bar, 2, 2, 2, bar, 1];

So these are the Arrays we compare. And the output is

  1. Check a and b with references only.

    console.log (sameElements ( a,b,true)) //true As they contain the same elements

  2. Check b and c with references only

    console.log (sameElements (b,c,true)) //false as c contains bar twice.

  3. Check b and c deeply

    console.log (sameElements (b,c,false)) //true as bar and foo are equal but not the same

  4. Check for 2 Arrays containing NaN

    Array.prototype.equals.NaN = true;
    console.log(sameElements([NaN],[NaN],true)); //true.
    Array.prototype.equals.NaN = false;

Demo on JSFiddle

Community
  • 1
  • 1
Moritz Roessler
  • 8,542
  • 26
  • 51
  • Which runs at `8ops/s` on an `var a = JSON.parse("["+ new Array(10000).join(JSON.stringify({a:1,b:2,c:{a:3},d:[1,2,3,4]})+",") + '{}]');` 10k Array like this – Moritz Roessler Jun 04 '13 at 10:51
  • 1
    Oh.. it seems I missed the point and only a reference check is needed, my bad – Moritz Roessler Jun 04 '13 at 11:23
  • Sorry, I don't get what your code does. Care to explain a little more? – georg Jun 05 '13 at 09:00
  • @thg435 Hey there =) I updated the answer, it does now either, reference or equality check. and added it to the jsperf. I'll explain it as soon as i find the time for it (i'll just comment it in here, not in the fiddle) – Moritz Roessler Jun 06 '13 at 11:13
  • @thg435 I tried to explain it a little better, by commenting the code. (i'm not that good in explaining, so if there any questions left, i'll try to explain them again, just place a comment) =) – Moritz Roessler Jun 06 '13 at 11:52
  • @C5H8NNaO4 `sameElements( [NaN], [NaN], true )` returns false – Bruno Jun 06 '13 at 14:29
  • I understand your point but according to you then what should the following call return `sameElements( [1,2,foo,2,bar,2,NaN], [bar,2,2,2,foo,1,NaN], true )` – Bruno Jun 06 '13 at 14:42
  • *(deleted comment:)* I stated: Of course, as `NaN !== NaN //true` and should be... As `Not a Number` is ambigous. it would require a check in `Number.prototype.equal` -> `if (this != this && c != c) return true` @Bruno Well, thats the other side. Thats a good point as well, (i deleted the comment.) And i will update the Answer with an Option in `Array.prototype.equal` to return true for `NaN` =) Thanks for pointing that out. – Moritz Roessler Jun 06 '13 at 14:49
  • @Bruno Thanks =), i updated the answer and the fiddle with flags for `[NaN].equals([NaN])` and `NaN.equals(NaN)` to yield true. – Moritz Roessler Jun 06 '13 at 15:25
  • @Bruno btw, what was the change in rev 13 of the jsperf ? – Moritz Roessler Jun 06 '13 at 15:29
  • @C5H8NNaO4 Sorry, just wanted to see what would happen if the objects being used had a deeper structure. It turned out that your algorithm was still quicker, at least on chrome :-). If you want you can revert it back. – Bruno Jun 06 '13 at 15:34
  • @Bruno Nah no need to apologize! it's your perf anyway ;) , also that's an interesting point as well, I'm totally fine with that =) I was only curious what was changed, as i didn't want to read those 500 some lines of code. Yeah, it seems, but your codes going quite ahead on firefox – Moritz Roessler Jun 06 '13 at 15:41
2

Like this perhaps?

var foo = {}; var bar=[];
var a = [3,2,1,foo]; var b = [foo,1,2,3];

function comp(a,b)
{
    // immediately discard if they are of different sizes
    if (a.length != b.length) return false;

    b = b.slice(0); // clone to keep original values after the function

    a.forEach(function(e) {
        var i;
        if ((i = b.indexOf(e)) != -1)
            b.splice(i, 1);
    });

    return !b.length;
}

comp(a,b);
BrunoLM
  • 97,872
  • 84
  • 296
  • 452
  • 1
    seems to work, but doesn't look particularly efficient. slice, then splice... I don't know. My arrays have 10,000's of items. – georg Jun 04 '13 at 09:47
  • The slice is needed to clone the array. If you are not going to use it again you could remove those 2 lines... Actually you don't need to clone `a`, I will update that... – BrunoLM Jun 04 '13 at 09:49
  • Works fine, but yours and Frédéric's solutions turned out to be quite slow for my needs. – georg Jun 05 '13 at 09:01
2

You can implement the following algorithm:

  • If a and b do not have the same length:
    • Return false.
  • Otherwise:
    • Clone b,
    • For each item in a:
      • If the item exists in our clone of b:
        • Remove the item from our clone of b,
      • Otherwise:
        • Return false.
    • Return true.

With Javascript 1.6, you can use every() and indexOf() to write:

function sameElements(a, b)
{
    if (a.length != b.length) {
        return false;
    }
    var ourB = b.concat();
    return a.every(function(item) {
        var index = ourB.indexOf(item);
        if (index < 0) {
            return false;
        } else {
            ourB.splice(index, 1);
            return true;
        }
    });
}

Note this implementation does not completely fulfill your requirements because indexOf() uses strict equality (===) internally. If you really want non-strict equality (==), you will have to write an inner loop instead.

Frédéric Hamidi
  • 258,201
  • 41
  • 486
  • 479
  • 1
    hmm, `sameElements([1,2],[1,2,3])` ?? – georg Jun 04 '13 at 09:43
  • @thg435, true, that completely escaped me. A length test is in order. I'll update my answer. – Frédéric Hamidi Jun 04 '13 at 09:46
  • This seems to work, thanks, but see my comment to @BrunoLM: my arrays are fairly big and repeated indexOf/splice doesn't look particularly efficient. Is there a better way? – georg Jun 04 '13 at 09:56
  • @thg435, without support for actual sets, I don't know if we can get much faster. [This question](http://stackoverflow.com/questions/7958292/mimicking-sets-in-javascript) explains how to emulate them, and [actual sets](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) are supported in Mozilla browsers. – Frédéric Hamidi Jun 04 '13 at 10:01
  • Although quite transparent, this version turned out to be the slowest (see jsperf links in the thread). – georg Jun 05 '13 at 08:57
2

UPDATE

As @Bergi and @thg435 point out my previous implementation was flawed so here is another implementation:

function sameElements(a, b) {
    var objs = [];
    // if length is not the same then must not be equal
    if (a.length != b.length) return false;

    // do an initial sort which will group types
    a.sort();
    b.sort();

    for ( var i = 0; i < a.length; i++ ) {

        var aIsPrimitive = isPrimitive(a[i]);
        var bIsPrimitive = isPrimitive(b[i]);

        // NaN will not equal itself
        if( a[i] !== a[i] ) {
            if( b[i] === b[i] ) {
                return false;
            }
        }
        else if (aIsPrimitive && bIsPrimitive) {

            if( a[i] != b[i] ) return false;
        }
        // if not primitive increment the __count property
        else if (!aIsPrimitive && !bIsPrimitive) {
            incrementCountA(a[i]);
            incrementCountB(b[i]);
            // keep track on non-primitive objects
            objs.push(i);
        }
        // if both types are not the same then this array
        // contains different number of primitives
        else {
            return false;
        }

    }

    var result = true;

    for (var i = 0; i < objs.length; i++) {
        var ind = objs[i];
        // if __aCount and __bCount match then object exists same
        // number of times in both arrays
        if( a[ind].__aCount !== a[ind].__bCount ) result = false;
        if( b[ind].__aCount !== b[ind].__bCount ) result = false;

        // revert object to what it was 
        // before entering this function
        delete a[ind].__aCount;
        delete a[ind].__bCount;
        delete b[ind].__aCount;
        delete b[ind].__bCount;
    }

    return result;
}

// inspired by @Bergi's code
function isPrimitive(arg) {
    return Object(arg) !== arg;
}

function incrementCountA(arg) {
    if (arg.hasOwnProperty("__aCount")) {
        arg.__aCount = arg.__aCount + 1;
    } else {
        Object.defineProperty(arg, "__aCount", {
            enumerable: false,
            value: 1,
            writable: true,
            configurable: true
        });
    }
}
function incrementCountB(arg) {
    if (arg.hasOwnProperty("__bCount")) {
        arg.__bCount = arg.__bCount + 1;
    } else {
        Object.defineProperty(arg, "__bCount", {
            enumerable: false,
            value: 1,
            writable: true,
            configurable: true
        });
    }
}

Then just call the function

sameElements( ["NaN"], [NaN] ); // false

// As "1" == 1 returns true
sameElements( [1],["1"] ); // true

sameElements( [1,2], [1,2,3] ); //false

The above implement actually defines a new property called "__count" that is used to keep track of non-primitive elements in both arrays. These are deleted before the function returns so as to leave the array elements as before.

Fiddle here

jsperf here.

The reason I changed the jsperf test case was that as @Bergi states the test arrays, especially the fact there were only 2 unique objects in the whole array is not representative of what we are testing for.

One other advantage of this implementation is that if you need to make it compatible with pre IE9 browsers instead of using the defineProperty to create a non-enumerable property you could just use a normal property.

Bruno
  • 5,772
  • 1
  • 26
  • 43
  • While sorting is a good idea, this fails on `sameElements([bar,bar,foo], [bar,foo,foo])` – Bergi Jun 04 '13 at 12:00
  • @Bergi It does actually succeed. The reason is that although the elements are sorted, for any element which is not a primitive, which in this case foo and bar are not, the function uses `Array.indexOf` method to check that the element exists in both arrays. Check the [fiddle](http://jsfiddle.net/bruno/rz3Vn/1/). – Bruno Jun 04 '13 at 12:15
  • But it should return `false`… – Bergi Jun 04 '13 at 12:29
  • This version indeed doesn't work correctly, but the jsperf link shows some more versions from you - consider reposting them here. – georg Jun 05 '13 at 08:56
  • @thg435 Just updated my answer with the code I have been working on, as well as the jsPerf tests :-) – Bruno Jun 05 '13 at 14:52
  • This is not working as well. Check `sameElements([o={},o], [p={},p])` which yields `true`! – Bergi Jun 05 '13 at 15:22
  • @Bergi Thanks for pointing that out :( I am on a steep learning curve. Issue has been corrected now. – Bruno Jun 05 '13 at 16:07
  • I added my example to the [JSPerf](http://jsperf.com/array-comparison-1/12) as well. *it yields `false` for `sameElements([o={},o], [p={},p],true)`* – Moritz Roessler Jun 06 '13 at 11:17
1

Thanks everyone for sharing ideas! I've came up with the following

function sameElements(a, b) {
    var hash = function(x) {
        return typeof x + (typeof x == "object" ? a.indexOf(x) : x);
    }
    return a.map(hash).sort().join() == b.map(hash).sort().join();
}

This isn't the fastest solution, but IMO, most readable one so far.

georg
  • 211,518
  • 52
  • 313
  • 390
1

i wasn't sure if "===" is ok, the question is a bit vauge... if so, this is quite a bit faster and simpler than some other possible ways of doing it:

function isSame(a,b){
  return a.length==b.length && 
      a.filter(function(a){ return b.indexOf(a)!==-1 }).length == b.length;
}
dandavis
  • 16,370
  • 5
  • 40
  • 36
1

Edit 2
1) Thanks to user2357112 for pointing out the Object.prototype.toString.call issue this also showed, the reason it was that fast, that it didn't consider Arrays ...

I fixed the code,it should be working now :), unfortunately its now at about 59ops/s on chrome and 45ops/s on ff.

Fiddle and JSPerf is updated.

Edit
1) I fixed the code, it supports mutliple variables referencing the same Object now. A little bit slower than before, but still over 100ops/s on chrome.

2) I tried using a bitmask instead of an array to keep multiple positions of the objects, but its nearly 15ops/s slow

3) As pointed ot in the comments i forgot to reset tmp after [[get]] is called fixed the code, the fiddle, and the perf.


So thanks to user2357112 with his Answer heres another approach using counting

var sameElements = (function () {
    var f, of, objectFlagName;
    of = objectFlagName = "__pos";
    var tstr = function (o) {
        var t = typeof o;
        if (o === null)
            t = "null";
        return t
    };
    var types = {};
    (function () {
        var tmp = {};
        Object.defineProperty(types, tstr(1), {
            set: function (v) {
                if (f)
                    tmp[v] = -~tmp[v];
                else
                    tmp[v] = ~-tmp[v];
            },
            get: function () {
                var ret = 1;
                for (var k in tmp) {
                    ret &= !tmp[k];
                }
                tmp = {};
                return ret;
            }
        });
    })();
    (function () {
        var tmp = {};
        Object.defineProperty(types, tstr(""), {

            set: function (v) {
                if (f) {
                    tmp[v] = -~tmp[v];
                } else {

                    tmp[v] = ~-tmp[v];
                }
            },
            get: function () {
                var ret = 1;
                for (var k in tmp) {
                    ret &= !tmp[k];
                }
                tmp = {};                
                return ret;
            }
        });
    })();

    (function () {
        var tmp = [];
        function add (v) {
            tmp.push(v);
            if (v[of]===undefined) {
                v[of] = [tmp.length -1];
            } else {
                v[of].push(tmp.length -1)
            }            

        }
        Object.defineProperty(types, tstr({}), {
            get: function () {
                var ret = true;
                for (var i = tmp.length - 1; i >= 0; i--) {
                    var c = tmp[i]
                    if (typeof c !== "undefined") {
                        ret = false
                        delete c[of]
                    }
                }
                tmp = [];
                return ret;
            },
            set: function (v) {
                var pos;
                if (f) {
                    add (v);
                } else if (!f && (pos = v[of]) !== void 0) {
                       tmp[pos.pop()] = undefined;
                       if (pos.length === 0)
                            delete v[of];
                } else {
                        add (v);
                }
            }
        });
    }());
    (function () {
        var tmp = 0;
        Object.defineProperty(types, tstr(undefined), {
            get: function () {
                var ret = !tmp;
                tmp = 0;
                return ret;

            },
            set: function () {
                tmp += f ? 1 : -1;
            }
        });
    })();
    (function () {
        var tmp = 0;
        Object.defineProperty(types, tstr(null), {
            get: function () {
                var ret = !tmp;
                tmp = 0;
                return ret;
            },
            set: function () {
                tmp += f ? 1 : -1;
            }
        });
    })();

    var tIt = [tstr(1), tstr(""), tstr({}), tstr(undefined), tstr(null)];

    return function eq(a, b) {

        f = true;
        for (var i = a.length - 1; i >= 0; i--) {
            var v = a[i];
            types[tstr(v)] = v;
        }
        f = false;
        for (var k = b.length - 1; k >= 0; k--) {
            var w = b[k];
            types[tstr(w)] = w;
        }
        var r = 1;
        for (var l = 0, j; j = tIt[l]; l++) {
            r &= types [j]
        }

        return !!r;
    }
    })()

Here is a JSFiddle and a JSPerf (it uses the same Arrays a and b as in the previous answers perf) with this code vs the Closure compiled

Heres the output. note: it doesn't support a deep comparison anymore, as is

var foo = {a:2}    
var bar = {a:1};
var a = [1, 2, foo, 2, bar, 2];
var b = [foo, 2, 2, 2, bar, 1];
var c = [bar, 2, 2, 2, bar, 1];
console.log(sameElements([NaN],[NaN])); //true
console.log (sameElements ( a,b))  //true
console.log (sameElements (b,c))   //false
Community
  • 1
  • 1
Moritz Roessler
  • 8,542
  • 26
  • 51
  • added the snippet to Brunos [JSPerf](http://jsperf.com/array-comparison-1/16) as well; ran out of battery can't run it – Moritz Roessler Jun 21 '13 at 09:06
  • I'm currently on mobile, but I noticed in the objects type Setter , I should change the `__pos` var to be an array instead in order to support vars referencing the same object, i'll update the answer accordingly – Moritz Roessler Jun 21 '13 at 10:50
  • `Object.prototype.toString.call(window) === "[object global]"` on my machine. I don't think using it is a good idea. See http://stackoverflow.com/a/4222755/2357112 – user2357112 Jun 21 '13 at 14:39
  • 1
    Also, what if you need to call this twice? All your data structures will be permanently messed up after the first call, especially if it returned false the first time. – user2357112 Jun 21 '13 at 14:47
  • @user2357112 Whoops i totally ment to reset `tmp` each time in the getter, thanks =) i'll fix it – Moritz Roessler Jun 21 '13 at 14:56
  • 1
    There has to be a cleaner way to code this. This code could only be loved by its mother. Please refactor it :) – Gili Jul 22 '14 at 22:29
  • @Gili True that :) Thanks for pointing that out, I'll do so when I find a bit of sparse time :) – Moritz Roessler Jul 23 '14 at 06:56
0

Using efficient lookup tables for the counts of the elements:

function sameElements(a) { // can compare any number of arrays
    var map, maps = [], // counting booleans, numbers and strings
        nulls = [], // counting undefined and null
        nans = [], // counting nans
        objs, counts, objects = [],
        al = arguments.length;

    // quick escapes:
    if (al < 2)
        return true;
    var l0 = a.length;
    if ([].slice.call(arguments).some(function(s) { return s.length != l0; }))
        return false;

    for (var i=0; i<al; i++) {
        var multiset = arguments[i];
        maps.push(map = {}); // better: Object.create(null);
        objects.push({vals: objs=[], count: counts=[]});
        nulls[i] = 0;
        nans[i] = 0;
        for (var j=0; j<l0; j++) {
            var val = multiset[j];
            if (val !== val)
                nans[i]++;
            else if (val === null)
                nulls[i]++;
            else if (Object(val) === val) { // non-primitive
                var ind = objs.indexOf(val);
                if (ind > -1)
                    counts[ind]++;
                else
                    objs.push(val), counts.push(1);
            } else { // booleans, strings and numbers do compare together
                if (typeof val == "boolean")
                    val = +val;
                if (val in map)
                    map[val]++;
                else
                    map[val] = 1;
            }
        }
    }

    // testing if nulls and nans are the same everywhere
    for (var i=1; i<al; i++)
        if (nulls[i] != nulls[0] || nans[i] != nans[0])
            return false;

    // testing if primitives were the same everywhere
    var map0 = maps[0];
    for (var el in map0)
        for (var i=1; i<al; i++) {
            if (map0[el] !== maps[i][el])
                return false;
            delete maps[i][el];
        }
    for (var i=1; i<al; i++)
        for (var el in maps[i])
            return false;

    // testing if objects were the same everywhere
    var objs0 = objects[0].vals,
        ol = objs0.length;
        counts0 = objects[0].count;
    for (var i=1; i<al; i++)
        if (objects[i].count.length != ol)
            return false;
    for (var i=0; i<ol; i++)
        for (var j=1; j<al; j++)
            if (objects[j].count[ objects[j].vals.indexOf(objs0[i]) ] != counts0[i])
                return false; 

    // else, the multisets are equal:
    return true;
}

It still uses indexOf search amongst all objects, so if you have multisets with many different objects you might want to optimize that part as well. Have a look at Unique ID or object signature (and it's duplicate questions) for how to get lookup table keys for them. And if you don't have many primitive values in the multisets, you might just store them in arrays and sort those before comparing each item-by-item (like @Bruno did).

Disclaimer: This solution doesn't try to get the [[PrimitiveValue]] of objects, they will never be counted as equal to primitives (while == would do).

Here is the update on @Bruno's jsperf test of the answers, yet I guess only two objects (each of them present 500 times in the 10k array) and no duplicate primitive values are not representative.

Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375