maiermic
maiermic

Reputation: 4984

Group array of objects nested by several groupBy functions

I try to write a function groupByMult using Ramda that applies several groupBy functions groupBys to an array of objects input:

function groupByMult(groupBys, input) { ... }

It should return a nested object that has child properties of groupBys[0], grandchild properties of groupBys[1], grand grandchildren properties of groupBys[2] and so on. The last grand child property has as value an array of objects that belong to this group path (see expected output at the bottom).

I explain the desired behaviour with an example:

My input is an array of objects. In this example all objects have a property g1, g2 and g3.

const input = [
  { g1: 'g1a', g2: 'g2a', g3: true },
  { g1: 'g1b', g2: 'g2b', g3: 42 },
  { g1: 'g1a', g2: 'g2a', g3: 'text' },
  { g1: 'g1a', g2: 'g2a', g3: false },
  { g1: 'g1a', g2: 'g2b', g3: 0 },
  { g1: 'g1a', g2: 'g2b', g3: 1 },
  { g1: 'g1a', g2: 'g2b', g3: true },
];

In my example, my group functions are:

const groupBys = [
  R.prop('g1'),
  R.prop('g2'),
  R.compose(R.type, R.prop('g3'))
];

I call groupByMult like this

const outpout = groupByMult(groupBys, input);

And I expect output to deeply equal expected:

const expected = {
  g1a: {
    g2a: {
      Boolean: [
        { g1: 'g1a', g2: 'g2a', g3: true },
        { g1: 'g1a', g2: 'g2a', g3: false },
      ],
      String: [
        { g1: 'g1a', g2: 'g2a', g3: 'text' },
      ],
    },
    g2b: {
      Number: [
        { g1: 'g1a', g2: 'g2b', g3: 0 },
        { g1: 'g1a', g2: 'g2b', g3: 1 },
      ],
      Boolean: [
        { g1: 'g1a', g2: 'g2b', g3: true },
      ],
    },
  },
  g1b: {
    g2b: {
      Number: [
        { g1: 'g1b', g2: 'g2b', g3: 42 },
      ]
    },
  },
}

output or respectively expected has child properties g1a, g1b, etc. of groupBys[0], grandchild properties g2a, g2b, etc. of groupBys[1], grand grandchildren properties Boolean, Number or String of groupBys[2], which have an array of objects that belong to this group path. For example, all objects of the array output.g1a.g2b.Boolean look like { g1: 'g1a', g2: 'g2b', Boolean: <boolean> } where <boolean> represents any boolean value.

How can I implement groupByMult to get the described behaviour?

Upvotes: 4

Views: 2192

Answers (3)

Scott Sauyet
Scott Sauyet

Reputation: 50797

It looks to me that what you want to do, for your example is something like this:

const groups = R.pipe(
  R.groupBy(R.prop('g1')),
  R.map(R.groupBy(R.prop('g2'))),
  R.map(R.map(R.groupBy(R.compose(R.type, R.prop('g3')))))
);

based on an input like this:

[
  R.prop('g1'),
  R.prop('g2'),
  R.compose(R.type, R.prop('g3'))
]

There is no difficulty wrapping each function in the list with a groupBy. That's just a map call. But there is some work involved in making that increasing list of maps for each element. I'm not sure if there's something more elegant -- or something more efficient -- than this recursive addMap helper, but this is what I came up with:

const groupByMults = (() => {
  const addMap = fns => fns.length < 1 ? [] : 
      [R.head(fns)].concat(addMap(R.map(R.map, R.tail(fns))));

  return R.curry((groupers, obj) => {
    var fns = addMap(R.map(R.groupBy, groupers))
    return R.apply(R.pipe)(fns)(obj);
  });
}());

You can see this in action on the Ramda REPL

Upvotes: 2

Scott Christopher
Scott Christopher

Reputation: 6516

This should be possible via a function that recursively maps over the resulting object values after applying R.groupBy.

groupByMany = R.curry((fns, items) => 
  R.isEmpty(fns) ? items
                 : R.map(groupByMany(R.tail(fns)),
                         R.groupBy(R.head(fns), items)));

This function will take a list of grouping functions along with a list of objects and continue calling itself recursively until there are no more functions to group by.

For example:

input = [
  { g1: 'g1a', g2: 'g2a', g3: 'g3a' },
  { g1: 'g1b', g2: 'g2c', g3: 'g3d' },
  { g1: 'g1c', g2: 'g2a', g3: 'g3b' },
  { g1: 'g1a', g2: 'g2b', g3: 'g3a' },
  { g1: 'g1a', g2: 'g2b', g3: 'g3b' }
];

groupByMany([R.prop('g1'), R.prop('g2'), R.prop('g3')], input);

Results in something like:

{ "g1a": { "g2a": { "g3a": [{ "g1": "g1a", "g2": "g2a", "g3": "g3a" }] },
           "g2b": { "g3a": [{ "g1": "g1a", "g2": "g2b", "g3": "g3a" }], 
                    "g3b": [{ "g1": "g1a", "g2": "g2b", "g3": "g3b" }] } },
  "g1b": { "g2c": { "g3d": [{ "g1": "g1b", "g2": "g2c", "g3": "g3d" }] } },
  "g1c": { "g2a": { "g3b": [{ "g1": "g1c", "g2": "g2a", "g3": "g3b" }] } } }

Upvotes: 7

Dmitry  Yaremenko
Dmitry Yaremenko

Reputation: 2570

Reduce array of functions and accumulate a mapped composition. For nested grouped object we use map(map(fn), input) to group values again:

const groupByMult = R.curry(function(fns, input) {
  const groupByMultReduced = R.reduce(function(acc, fn) {
    if (acc === null) {
      return {
        fn: R.groupBy(fn),
        map: R.map
      };
    }

    return {
      // main function
      fn: R.compose(acc.map(R.groupBy(fn)), acc.fn), 

      // accumulating helper map(map(map(...
      map: R.compose(R.map, acc.map)
    };
  }, null, fns);

  return groupByMultReduced !== null ? groupByMultReduced.fn(input) :  input;
});

let output = groupByMult(groupBys, input);

Upvotes: 1

Related Questions