简体   繁体   中英

GroupBy and reduce an array of object in a pointfree style

I recently started using Ramda and trying to find a pointfree way to write a method to reduce an array of objects.

Here is the array of object :

const someObj = [
    {
        name: 'A',
        city: 1,
        other: {
            playtime: 30
        }
    },
    {
        name: 'B',
        city: 2,
        other: {
            playtime: 20
        }
    },
    {
        name: 'c',
        city: 1,
        other: {
            playtime: 20
        }
    }
];

What I am trying is to reduce the object using ramda in poinfree style like

{
    '1': {
        count: 2,
        avg_play_time: 20 + 30 / count
    },
    '2': {
        count: 1,
        avg_play_time: 20 / count
    }
}

I can do it using an array reduce method but not sure how can I write the same in ramda pointfree style. Any suggestion will be appreciated.

One solution would be to do something like this:

// An optic to extract the nested playtime value
// Coupled with a `lift` operation which allows it to be applied over a collection
// Effectively A -> B => A[] -> B[]
const playtimes = R.lift(R.path(['other', 'playtime']))

R.pipe(
  // Group the provided array by the city value
  R.groupBy(R.prop('city')),
  // Return a body specification which computes each property based on the 
  // provided function value.
  R.map(R.applySpec({
    count: R.length,
    average: R.pipe(playtimes, R.mean)
  }))
)(someObj)

Ramda also has another function called R.reduceBy which provides something inbetween reduce and groupBy , allowing you to fold up values with matching keys together.

So you can create a data type like the following that tallies up the values to averaged.

const Avg = (count, val) => ({ count, val })
Avg.of = val => Avg(1, val)
Avg.concat = (a, b) => Avg(a.count + b.count, a.val + b.val)
Avg.getAverage = ({ count, val }) => val / count
Avg.empty = Avg(0, 0)

Then combine them together using R.reduceBy .

const avgCities = R.reduceBy(
  (avg, a) => Avg.concat(avg, Avg.of(a.other.playtime)),
  Avg.empty,
  x => x.city
)

Then pull the average values out of the Avg into the shape of the final objects.

const buildAvg = R.applySpec({
  count: x => x.count,
  avg_play_time: Avg.getAverage
})

And finally pipe the two together, mapping buildAvg over the values in the object.

const fn = R.pipe(avgCities, R.map(buildAvg))
fn(someObj)

Here's another suggestion using reduceBy with mapping an applySpec function on each property of the resulting object:

The idea is to transform someObj into this object using getPlaytimeByCity :

{ 1: [30, 20],
  2: [20]}

Then you can map the stats function on each property of that object:

stats({ 1: [30, 20], 2: [20]});
// { 1: {count: 2, avg_play_time: 25}, 
//   2: {count: 1, avg_play_time: 20}}

 const someObj = [ { name: 'A', city: 1, other: { playtime: 30 }}, { name: 'B', city: 2, other: { playtime: 20 }}, { name: 'c', city: 1, other: { playtime: 20 }} ]; const city = prop('city'); const playtime = path(['other', 'playtime']); const stats = applySpec({count: length, avg_play_time: mean}); const collectPlaytime = useWith(flip(append), [identity, playtime]); const getPlaytimeByCity = reduceBy(collectPlaytime, [], city); console.log( map(stats, getPlaytimeByCity(someObj)) ); 
 <script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script> <script>const {prop, path, useWith, flip, append, identity, applySpec, length, mean, reduceBy, map} = R;</script> 

I would write it like this, hope it helps!

 const stats = R.pipe( R.groupBy(R.prop('city')), R.map( R.applySpec({ count: R.length, avg_play_time: R.pipe( R.map(R.path(['other', 'playtime'])), R.mean, ), }), ), ); const data = [ { name: 'A', city: 1, other: { playtime: 30 } }, { name: 'B', city: 2, other: { playtime: 20 } }, { name: 'c', city: 1, other: { playtime: 20 } }, ]; console.log('result', stats(data)); 
 <script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script> 

I like all the other answers given so far. So naturally I want to add my own. ;-)

Here is a version that uses reduceBy to keep a running track of the count and mean. This would not work if you were looking for the median value or some other statistic, but given a count, an average, and a new value, we can calculate the new count and average directly. This allows us to iterate the data only once at the expense of doing some arithmetic on every iteration.

 const transform = reduceBy( ({count, avg_play_time}, {other: {playtime}}) => ({ count: count + 1, avg_play_time: (avg_play_time * count + playtime) / (count + 1) }), {count: 0, avg_play_time: 0}, prop('city') ) const someObj = [{city: 1, name: "A", other: {playtime: 30}}, {city: 2, name: "B", other: {playtime: 20}}, {city: 1, name: "c", other: {playtime: 20}}] console.log(transform(someObj)) 
 <script src="https://bundle.run/ramda@0.26.1"></script> <script> const {reduceBy, prop} = ramda </script> 

This is not point-free. Although I'm a big fan of point-free style, I only use it when it's applicable. I think seeking it out for its own sake is a mistake.

Note that the answer from Scott Christopher could easily be modified to use this sort of calculation

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM