JavaScript native groupBy reduce

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 = [
  {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"}
]

I want to group it and change it to this:

const 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}
    ]
  }
]

Answers:

Answer

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; }

Answer

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; }

Answer

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);

Answer

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 }
//     ]
//   }
// ]

Answer

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;
}

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us Javascript

©2020 All rights reserved.