I am using JavaScript native reduce, however I want to slightly change in the grouping to get my desired result. I have an array as follows:
const people = [
This is not a trivial problem. You first have to define what constitutes a grouping, and you also have to define how like terms will be combined. You problem is exacerbated by the need to group by a non-primitive value: city and state. Ie, we can't just group based on city
alone; more than half the states in the US have a city named Oakland. Other answers solve this by serializing the city and state in a string, but I will show you a more generic solution that works for compound data of any type.
This is tagged with functional programming, so I'll start with a module for separating our subtasks
const DeepMap =
{ has: (map, [ k, ...ks ]) =>
ks.length === 0
? map.has (k)
: map.has (k)
? DeepMap.has (map.get (k), ks)
: false
, set: (map, [ k, ...ks ], value) =>
ks.length === 0
? map.set (k, value)
: map.has (k)
? (DeepMap.set (map.get (k), ks, value), map)
: map.set (k, DeepMap.set (new Map, ks, value))
, get: (map, [ k, ...ks ]) =>
ks.length === 0
? map.get (k)
: map.has (k)
? DeepMap.get (map.get (k), ks)
: undefined
}
Now we can define our generic groupBy
function
const identity = x =>
x
const { has, set, get } =
DeepMap
const groupBy = (key = identity, value = identity, xs = []) =>
xs.reduce
((m, x) =>
has (m, key (x))
? set ( m
, key (x)
, [ ...get (m, key (x)), value (x) ]
)
: set ( m
, key (x)
, [ value (x) ]
)
, new Map
)
We use groupBy
by specifying a key
and value
functions – The key function specifies what an item is grouped by, and the value functions specifies the value to be added to the group
const people =
[ { name: "John", age: 23, city: "Seattle", state: "WA" }
, { name: "Mark", age: 25, city: "Houston", state: "TX" }
, { name: "Luke", age: 26, city: "Seattle", state: "WA" }
, { name: "Paul", age: 28, city: "Portland", state: "OR" }
, { name: "Matt", age: 21, city: "Oakland", state: "CA" }
, { name: "Sam", age: 24, city: "Oakland", state: "CA" }
]
const res =
groupBy ( k => [ k.state, k.city ]
, v => ({ name: v.name, age: v.age })
, people
)
console.log (res.get ('WA'))
// Map { 'Seattle' => [ { name: 'John', age: 23 }, { name: 'Luke', age: 26 } ] }
console.log (res.get ('WA') .get ('Seattle'))
// [ { name: 'John', age: 23 }, { name: 'Luke', age: 26 } ]
We can see how this intermediate result would be useful. It provides incredibly efficient lookup thanks to Map
. Of course you'll want to iterate thru the deep map in more meaningful ways though. Let's add an entries
procedure to our module
const DeepMap =
{ ...
, entries: function* (map, fields = [])
{
const loop = function* (m, path, [ f, ...fields ])
{
if (fields.length === 0)
for (const [ key, value ] of m)
yield [ { ...path, [ f ]: key }, value ]
else
for (const [ key, value ] of m)
yield* loop (value, { ...path, [ f ]: key }, fields)
}
yield* loop (map, {}, fields)
}
}
for (const [ key, value ] of DeepMap.entries (res, [ 'state', 'city' ]))
console.log (key, value)
// { state: 'WA', city: 'Seattle' } [ { name: 'John', age: 23 }, { name: 'Luke', age: 26 } ]
// { state: 'TX', city: 'Houston' } [ { name: 'Mark', age: 25 } ]
// { state: 'OR', city: 'Portland' } [ { name: 'Paul', age: 28 } ]
// { state: 'CA', city: 'Oakland' } [ { name: 'Matt', age: 21 }, { name: 'Sam', age: 24 } ]
Now that our deep map is iterable, we can easily produce your desired output using Array.from
const arranged =
Array.from ( entries (res, [ 'state', 'city' ])
, ([ key, persons ]) => ({ ...key, persons })
)
console.log (arranged)
// [
// {
// city: "Seattle",
// state: "WA",
// persons: [
// { name: "John", age: 23 },
// { name: "Luke", age: 26 }
// ]
// },
// {
// city: "Houston",
// state: "TX",
// persons: [
// { name: "Mark", age: 25 }
// ]
// },
// {
// city: "Portland",
// state: "OR",
// persons : [
// { name: "Paul", age: 28 }
// ]
// },
// {
// city: "Oakland",
// state: "CA",
// persons: [
// { name: "Matt", age: 21 },
// { name: "Sam", age: 24 }
// ]
// }
// ]
Program demonstration
const DeepMap =
{ has: (map, [ k, ...ks ]) =>
ks.length === 0
? map.has (k)
: map.has (k)
? DeepMap.has (map.get (k), ks)
: false
, set: (map, [ k, ...ks ], value) =>
ks.length === 0
? map.set (k, value)
: map.has (k)
? (DeepMap.set (map.get (k), ks, value), map)
: map.set (k, DeepMap.set (new Map, ks, value))
, get: (map, [ k, ...ks ]) =>
ks.length === 0
? map.get (k)
: map.has (k)
? DeepMap.get (map.get (k), ks)
: undefined
, entries: function* (map, fields = [])
{
const loop = function* (m, path, [ f, ...fields ])
{
if (fields.length === 0)
for (const [ key, value ] of m)
yield [ { ...path, [ f ]: key }, value ]
else
for (const [ key, value ] of m)
yield* loop (value, { ...path, [ f ]: key }, fields)
}
yield* loop (map, {}, fields)
}
}
const identity = x =>
x
const { has, set, get, entries } =
DeepMap
const groupBy = (key = identity, value = identity, xs = []) =>
xs.reduce
((m, x) =>
has (m, key (x))
? set ( m
, key (x)
, [ ...get (m, key (x)), value (x) ]
)
: set ( m
, key (x)
, [ value (x) ]
)
, new Map
)
const people =
[ { name: "John", age: 23, city: "Seattle", state: "WA" }
, { name: "Mark", age: 25, city: "Houston", state: "TX" }
, { name: "Luke", age: 26, city: "Seattle", state: "WA" }
, { name: "Paul", age: 28, city: "Portland", state: "OR" }
, { name: "Matt", age: 21, city: "Oakland", state: "CA" }
, { name: "Sam", age: 24, city: "Oakland", state: "CA" }
]
const res =
groupBy ( k => [ k.state, k.city ]
, v => ({ name: v.name, age: v.age })
, people
)
for (const [ key, value ] of entries (res, [ 'state', 'city' ]))
console.log (key, value)
// { state: 'WA', city: 'Seattle' } [ { name: 'John', age: 23 }, { name: 'Luke', age: 26 } ]
// { state: 'TX', city: 'Houston' } [ { name: 'Mark', age: 25 } ]
// { state: 'OR', city: 'Portland' } [ { name: 'Paul', age: 28 } ]
// { state: 'CA', city: 'Oakland' } [ { name: 'Matt', age: 21 }, { name: 'Sam', age: 24 } ]
const arranged =
Array.from ( entries (res, [ 'state', 'city '])
, ([ key, persons ]) => ({ ...key, persons })
)
console.log ('arranged', arranged)
// arranged [
// {
// city: "Seattle",
// state: "WA",
// persons: [
// { name: "John", age: 23 },
// { name: "Luke", age: 26 }
// ]
// },
// {
// city: "Houston",
// state: "TX",
// persons: [
// { name: "Mark", age: 25 }
// ]
// },
// {
// city: "Portland",
// state: "OR",
// persons : [
// { name: "Paul", age: 28 }
// ]
// },
// {
// city: "Oakland",
// state: "CA",
// persons: [
// { name: "Matt", age: 21 },
// { name: "Sam", age: 24 }
// ]
// }
// ]