Skip to content

Instantly share code, notes, and snippets.

@joyrexus
Created March 28, 2014 16:59
Show Gist options
  • Star 55 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save joyrexus/9837596 to your computer and use it in GitHub Desktop.
Save joyrexus/9837596 to your computer and use it in GitHub Desktop.
Nested grouping of arrays

nest.js

A multi-level groupBy for arrays inspired by D3's nest operator.

Nesting allows elements in an array to be grouped into a hierarchical tree structure; think of it like the GROUP BY operator in SQL, except you can have multiple levels of grouping, and the resulting output is a tree rather than a flat table. The levels in the tree are specified by key functions.

See this fiddle for live demo.

Implementation

Depends on lodash's groupBy and mapValues:

_ = require('lodash');

var nest = function (seq, keys) {
    if (!keys.length)
        return seq;
    var first = keys[0];
    var rest = keys.slice(1);
    return _.mapValues(_.groupBy(seq, first), function (value) { 
        return nest(value, rest)
    });
};

module.exports = nest;

Usage

Input data to be nested:

var data = [
  { type: "apple", color: "green", quantity: 1000 }, 
  { type: "apple", color: "red", quantity: 2000 }, 
  { type: "grape", color: "green", quantity: 1000 }, 
  { type: "grape", color: "red", quantity: 4000 }
];

Key functions used for grouping criteria:

var byType = function(d) {
  return d.type;
};

var byColor = function(d) {
  return d.color;
};

var byQuantity = function(d) {
  return d.quantity;
};

First Example

Expected output when grouping by color and quantity:

var expected = {
  green: {
    "1000": [
      { type: 'apple', color: 'green', quantity: 1000 }, 
      { type: 'grape', color: 'green', quantity: 1000 }
    ]
  },
  red: {
    "2000": [
      { type: 'apple', color: 'red', quantity: 2000 }
    ],
    "4000": [
      { type: 'grape', color: 'red', quantity: 4000 }
    ]
  }
};

Nest by key name:

deepEqual(nest(data, ['color', 'quantity']), expected);

Nest by key functions:

deepEqual(nest(data, [byColor, byQuantity]), expected);

Second Example

Expected output when grouping by type and color:

expected = {
  apple: {
    green: [ { "type": "apple", "color": "green", "quantity": 1000 } ],
    red: [ { "type": "apple", "color": "red", "quantity": 2000 } ]
  },
  grape: {
    green: [ { "type": "grape", "color": "green", "quantity": 1000 } ],
    red: [ { "type": "grape", "color": "red", "quantity": 4000 } ]
  }
};

Nest by key names:

deepEqual(nest(data, ['type', 'color']), expected);

Nest by key functions:

deepEqual(nest(data, [byType, byColor]), expected);
var nest = require('nest');
var assert = require('assert');
var eq = assert.deepEqual;
// input data to be nested
var data = [
{ type: "apple", color: "green", quantity: 1000 },
{ type: "apple", color: "red", quantity: 2000 },
{ type: "grape", color: "green", quantity: 1000 },
{ type: "grape", color: "red", quantity: 4000 }
];
/* FIRST EXAMPLE */
// expected output, grouping by `color` and `quantity`
var expected = {
green: {
"1000": [
{ type: 'apple', color: 'green', quantity: 1000 },
{ type: 'grape', color: 'green', quantity: 1000 }
]
},
red: {
"2000": [
{ type: 'apple', color: 'red', quantity: 2000 }
],
"4000": [
{ type: 'grape', color: 'red', quantity: 4000 }
]
}
};
// key functions used for grouping criteria
var byType = function(d) {
return d.type;
};
var byColor = function(d) {
return d.color;
};
var byQuantity = function(d) {
return d.quantity;
};
// nest by key name
eq(nest(data, ['color', 'quantity']), expected);
// nest by key function
eq(nest(data, [byColor, byQuantity]), expected);
/* SECOND EXAMPLE */
// expected output, grouping by `type` and `color`
expected = {
apple: {
green: [ { "type": "apple", "color": "green", "quantity": 1000 } ],
red: [ { "type": "apple", "color": "red", "quantity": 2000 } ]
},
grape: {
green: [ { "type": "grape", "color": "green", "quantity": 1000 } ],
red: [ { "type": "grape", "color": "red", "quantity": 4000 } ]
}
};
// nest by key names
eq(nest(data, ['type', 'color']), expected);
// nest by key function
eq(nest(data, [byType, byColor]), expected);
_ = require('lodash');
var nest = function (seq, keys) {
if (!keys.length)
return seq;
var first = keys[0];
var rest = keys.slice(1);
return _.mapValues(_.groupBy(seq, first), function (value) {
return nest(value, rest)
});
};
module.exports = nest;
@SpencerCarstens
Copy link

Nice application of lodash!

How would you go about doing the same for the d3.entries() operator instead of d3.map()?

https://github.com/mbostock/d3/wiki/Arrays#nest_entries

I'm still debating what structure makes the most sense in application...

@rebolyte
Copy link

rebolyte commented Mar 6, 2017

With destructuring!

function nest(seq, keys) {
    if (!keys.length) { return seq; }
    let [ first, ...rest ] = keys;
    return _.mapValues(_.groupBy(seq, first), value => nest(value, rest));
};

@djKianoosh
Copy link

@rebolyte the destructuring one is very nice, kudos +100

@kachkaev
Copy link

kachkaev commented Apr 6, 2018

TypeScript version:

import * as _ from "lodash";

const nest = (seq: any[], keys: Array<string | ((obj: any) => string)>) => {
  if (!keys.length) {
    return seq;
  }
  const [first, ...rest] = keys;
  return _.mapValues(_.groupBy(seq, first), (value) => nest(value, rest));
};

export default nest;

@jpkontreras
Copy link

nice one guys. I been trying to do this all week.
But i have one doubt

What if i want to the final property to be an object aswell

expected = {
  apple: {
    green: [ { "type": "apple", "color": "green", "quantity": 1000 } ], //CURRENT
    red: { "type": "apple", "color": "red", "quantity": 2000 }  // HOW I WANT IT TO BE
  }
};

@MartinMuzatko
Copy link

MartinMuzatko commented May 7, 2018

Is there a npm module for this? Also I would love to have an object instead of an array, if there is only one item left like @jpkontreras suggested

@viranmalaka
Copy link

@jpkontreras, @MartinMuzatko doing the following simple adjustment will solve your problem.

function nest(seq, keys) {
    if (!keys.length) { return seq.length === 1 ? seq[0] : seq } // HERE
    let [ first, ...rest ] = keys;
    return _.mapValues(_.groupBy(seq, first), value => nest(value, rest));
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment