Based on this example, I want to group by object in a slightly other way. The outcome should be as follows:
[{
key: \"audi\"
items: [
{
\"make\": \
@Nina's answer is practical, efficient, and definitely the answer you should be reading. However, these problems are interesting to me and I like thinking about solving them in other ways, even if that means making trades.
Compound data equality in JavaScript
Testing for compound data equality in JavaScript can be a bother
console.log (1 === 1) // true
console.log ('a' === 'a') // true
console.log ([1,2] === [1,2]) // false
console.log ({a:1} === {a:1}) // false
This simple-natured equality test can make it somewhat challenging to deal with JavaScript's native Set
and Map
too
const m = new Map ()
m.set ([1,2], 'hello')
console.log (m.get ([1,2])) // undefined
console.log (m.get (Array.from (m.keys ()) [0])) // 'hello'
Bugger! Compound data equality bit us again. m.get
cannot find the key [1,2]
because the first key (that we set) [1,2]
is different from the second key (to get) [1,2]
– ie, the two instances of [1,2]
are in different memory locations and are therefore considered (by JavaScript) to be inequal (!==
)
Compound data equality, take 2
We don't have to play by JavaScript's rules, if we don't want to. In this part of the answer, we make our own Dict
(dictionary) compound data type that accepts a function that is used to determine key equality
Imagine Dict
working something like this
const d = Dict (({a} => a)
d.has ({a:1}) // false
d.set ({a:1}, 'hello') .has ({a:1}) // true
d.set ({a:1}, 'hello') .get ({a:1}) // 'hello'
d.get ({a:2}) // undefined
d.set ({a:2}, 'world') .get ({a:2}) // 'world'
If we had a data type that worked like Dict
, then we could easily write the necessary transformation for our data
// our Dict type with custom key comparator
const DictByMake =
Dict (x => x.make)
const dict =
data.reduce((d, item) =>
d.set (item, d.has (item)
? d.get (item) .concat ([item])
: [item]), DictByMake ())
I say if we had a data type like Dict
because it's good to be optimistic. Why should I make sacrifices and pick a data type incapable of fulfilling my needs when I don't have to? If a type I need doesn't exist, I can just make one. Thanks in advance, JavaScript !
Below I implement Dict
with some consistency to JS's Set
and Map
– the most notable difference here is Dict
is persistent (immutable) (a matter of preference, in this case)
const Pair = (left, right) => ({
left,
right
})
const Dict = eq => (pairs=[]) => ({
equals (x, y) {
return eq (x) === eq (y)
},
has (k) {
for (const {left} of pairs)
if (this.equals (k, left))
return true
return false
},
get (k) {
for (const {left, right} of pairs)
if (this.equals (k, left))
return right
return undefined
},
set (k, v) {
for (const [i, {left, right}] of pairs.entries ())
if (this.equals (k, left))
return Dict (eq) (pairs
.slice (0, i)
.concat ([Pair (k, v)])
.concat (pairs.slice (i+1)))
return Dict (eq) (pairs.concat ([Pair (k, v)]))
},
entries () {
return {
*[Symbol.iterator] () {
for (const {left, right} of pairs)
yield [eq (left), right]
}
}
}
})
const DictByMake =
Dict (x => x.make)
const main = data => {
// build the dict
const dict =
data.reduce((d, x) =>
d.set(x, d.has (x)
? [...d.get (x), x]
: [x]), DictByMake ())
// convert dict key/value pairs to desired {key, items} shape
return Array.from (dict.entries (), ([key, items]) =>
({ key, items }))
}
const data =
[{ make: 'audi', model: 'r8', year: '2012' }, { make: 'audi', model: 'rs5', year: '2013' }, { make: 'ford', model: 'mustang', year: '2012' }, { make: 'ford', model: 'fusion', year: '2015' }, { make: 'kia', model: 'optima', year: '2012' }]
console.log (main (data))
Compound data equality, take 3
OK, that was a little intense inventing our very own data type ! In the example above, we based Dict
off of an Array
(native) of pairs – this naïve implementation detail makes Dict
inefficient compared to other associative types that instead use a binary search tree or hash table to get/set keys. I used this an example to show how to build a more complex type from a more primitive one, but we could've just as easily made our own Tree type and used that instead.
In reality, we are given Map
by JavaScript and don't have (get) to worry about how it's implemented – and while it doesn't have the exact behavior we want, we can adapt its behavior slightly without having to invent an entirely new type from scratch.
Worth noting, MapBy
is not implemented as a persistent structure here
const MapBy = ord => (map = new Map ()) => ({
has: k =>
map.has (ord (k)),
get: k =>
map.get (ord (k)),
set: (k, v) =>
MapBy (ord) (map.set (ord (k), v)),
keys: () =>
map.keys (),
values: () =>
map.values (),
entries: () =>
map.entries ()
})
// the rest of the program stays exactly the same (with exception to variable names)
const MapByMake =
MapBy (x => x.make)
const main = data => {
const map =
data.reduce((m, x) =>
m.set(x, m.has (x)
? [...m.get (x), x]
: [x]), MapByMake ())
return Array.from (map.entries (), ([key, items]) =>
({ key, items }))
}
const data =
[{ make: 'audi', model: 'r8', year: '2012' }, { make: 'audi', model: 'rs5', year: '2013' }, { make: 'ford', model: 'mustang', year: '2012' }, { make: 'ford', model: 'fusion', year: '2015' }, { make: 'kia', model: 'optima', year: '2012' }]
console.log (main (data))
You could use a hash table for grouping by make
and an array for the wanted result.
For every group in hash
, a new object, like
{
key: a.make,
items: []
}
is created and pushed to the result set.
The hash table is initialized with a really empty object. There are no prototypes, to prevent collision.
var cars = [{ make: 'audi', model: 'r8', year: '2012' }, { make: 'audi', model: 'rs5', year: '2013' }, { make: 'ford', model: 'mustang', year: '2012' }, { make: 'ford', model: 'fusion', year: '2015' }, { make: 'kia', model: 'optima', year: '2012' }],
hash = Object.create(null),
result = [];
cars.forEach(function (a) {
if (!hash[a.make]) {
hash[a.make] = { key: a.make, items: [] };
result.push(hash[a.make]);
}
hash[a.make].items.push(a);
});
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }