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 = [
Try this. I use Array.prototype.forEach and Array.prototype.push
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"}
];
var arranged=[];
people.forEach(function(e){
var exist=false;
arranged.forEach(function(e1){
if(e1.state===e.state){
exist=true;
e1.persons.push({name:e.name,age:e.age});
}
});
if(!exist){
arranged.push({state:e.state,city:e.city,persons:[{name:e.name,age:e.age}]});
}
});
console.log(arranged);
You can use the function reduce
to group and build the desired output.
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 result = Object.values(people.reduce((a, {name, age, city, state}) => {
var key = [city, state].join('|');
(a[key] || (a[key] = {city, state, persons: []})).persons.push({name, age});
return a;
}, {}));
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
I have built a generic group by reducer, you pass it the keys by which you want to group and it gives you a custom reducer function. This reducer gives you an object indexed by a (composed or simple) key containing an array of items that share this key. You can reuse it to have it grouped by the key(s) you want to.
Here are two examples.
const people = Object.freeze([{
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 groupByReducer = (group) =>
(result, row) => {
const keygroup = group.map((v) => row[v]);
const key = keygroup.join(':');
if (result[key])
result[key].push(row);
else
result[key] = [row];
return result;
};
const byCityState = people.reduce(
groupByReducer(['city', 'state']), {});
const byState = people.reduce(groupByReducer(['state']), {});
console.log(byCityState);
console.log(byState);
.as-console-wrapper {
max-height: 100% !important;
top: 0;
}
You could use a Map and a stringified object as key for grouping.
Later render the wanted array with objects of the keys and the grouped persons.
var 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" }],
arranged = Array.from(
people.reduce((m, o) => {
var key = JSON.stringify(Object.assign(...['city', 'state'].map(k => ({ [k]: o[k] }))));
return m.set(key, (m.get(key) || []).concat({ name: o.name, age: o.age }));
}, new Map),
([key, persons]) => Object.assign(JSON.parse(key), { persons })
);
console.log(arranged);
.as-console-wrapper { max-height: 100% !important; top: 0; }
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 }
// ]
// }
// ]