JavaScript native groupBy reduce

只愿长相守 提交于 2019-12-04 05:41:37

问题


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

回答1:


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



回答2:


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



回答3:


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



回答4:


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



回答5:


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


来源:https://stackoverflow.com/questions/49436776/javascript-native-groupby-reduce

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!