I managed to implement this way:
I need to get the hashcode from objects.
Object.prototype.GetHashCode = function () {
var s = this instanceof Object ? stringify(this) : this.toString();
var hash = 0;
if (s.length === 0) return hash;
for (var i = 0; i < s.length; ++i) {
hash = ((hash << 5) - hash) + s.charCodeAt(i);
}
return hash;
};
Number.prototype.GetHashCode = function () { return this.valueOf(); };
As JSON.stringify
will fail at circular references, I created another method to stringify it in a way I can get the most of an object as a string and compute the hashcode over it, as follows:
function isPlainObject(obj)
{
if ((typeof (obj) !== "object" || obj.nodeType || (obj instanceof Window))
|| (obj.constructor && !({}).hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf"))
)
{
return false;
}
return true;
}
function stringify(obj, s)
{
s = s || "";
for (var i in obj)
{
var o = obj[i];
if (o && (o instanceof Array || isPlainObject(o)))
{
s += i + ":" + JSON.stringify(o);
}
else if (o && typeof o === "object")
{
s += i + ":" + "$ref#" + o;
}
else
{
s += i + ":" + o;
}
}
return s;
}
There isn't much impact on performance. For large object it is the same and for small objects it loses, but is still pretty fast and safe. Performance test here.
Name op/s
---------------------------------
JSON.stringify large 62
stringify large 62
JSON.stringify small 1,690,183
stringify small 1,062,452
My GroupBy method
function GroupBy(a, keySelector, elementSelector, comparer)
{
// set default values for opitinal parameters
elementSelector = elementSelector || function(e) { return e; };
comparer = comparer ||
{
Equals: function(a,b) { return a==b },
GetHashCode: function(e) { return e.GetHashCode(); }
};
var key, hashKey, reHashKey;
// keep groups separated by hash
var hashs = {};
for (var i = 0, n = a.length; i < n; ++i)
{
// in case of same hash, but Equals returns false
reHashKey = undefined;
// grabs the key
key = keySelector(a[i]);
// grabs the hashcode
hashKey = comparer.GetHashCode(key);
// if a hash exists in the list
// compare values with Equals
// in case it return false, generate a unique hash
if (typeof hashs[hashKey] !== "undefined")
reHashKey = comparer.Equals(key, hashs[hashKey].Key) ? hashKey : hashKey + " " + i;
// if a new hash has been generated, update
if (typeof reHashKey !== "undefined" && reHashKey !== hashKey)
hashKey = reHashKey;
// get/create a new group and add the current element to the list
hashs[hashKey] = hashs[hashKey] || { Key: key, Elements: [] };
hashs[hashKey].Elements.push(a[i]);
}
return hashs;
}
To test
var arrComplex =
[
{ N: { Value: 10 }, Name: "Foo" },
{ N: { Value: 10 }, Name: "Bar" },
{ N: { Value: 20 }, Name: "Foo" },
{ N: { Value: 20 }, Name: "Bar" }
];
//
var x = GroupBy(arrComplex
, function(e) { return e.N; }
, function(e) { return e.Name; }
, {
Equals: function(a,b) { return a.Value == b.Value },
GetHashCode: function(e) { return e.GetHashCode(); }
}
);
//
console.log(x);
Example on jsFiddle, now with Jedi.
But, accordingly to my tests, my implementation of GroupBy
is slower than linq.js's GroupBy
. It is only faster when I convert ToArray()
. Maybe linq.js only really executes when I convert to array, that's why the difference, I am not sure on this part.
Test results
Name op/s
---------------------------------
GroupBy 163,261
GroupByToArray 152,382
linq.js groupBy 243,547
linq.js groupBy toArray 26,309
Underscore.js
or other library? – BathsheebLo-Dash.js
is another alternative and it bills itself as faster and more efficient thanUnderscore.js
– Attitudinariancomparer
parameter. – Supererogationcomparer
? What happens ifcomparer
returns 'equality' for two elements with different keys? What key will be used for this group? – BathsheebKey
could be a class. – Supererogation.reduce
here most likely but that's not the most efficient, the most efficient is a Map and a for loop. – GenistaEquals
(ex:function(a,b){return a.N.Value == b.N.Value;}
) and maybe aGetHashCode
(ex:function(a) { return a.N.Value; }
). – Supererogation