Skip to content

Instantly share code, notes, and snippets.

@joyrexus
Created March 28, 2014 14:49
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save joyrexus/9834587 to your computer and use it in GitHub Desktop.
Save joyrexus/9834587 to your computer and use it in GitHub Desktop.
Nested grouping of arrays

nest

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.


Warning!

If you're the sort who's offended by the mere sight of coffeescript, please see this gist instead.


Implementation

Depends on lodash's groupBy and mapValues:

_ = require 'lodash'

nest = (seq, iters...) ->
  return seq unless iters.length
  [first, rest...] = iters
  _.mapValues _.groupBy(seq, first), (value) -> nest(value, rest...) 

Usage

Input data to be nested:

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
]

Criteria for grouping:

byType = (d) -> d.type                  # group by type
byColor = (d) -> d.color                # group by color
byQuantity = (d) -> d.quantity          # group by quantity

Expected output when grouping by color and quantity:

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} ]
deepEqual nest(data, byColor, byQuantity), expected

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 } ]
deepEqual nest(data, byType, byColor), expected

Alternatives

As mentioned above, this approach was inspired by D3's nest operator.

D3's nest implementation is very powerful due to its method chaining approach, enabling flexible combinations of nestings and rollups. See a quick demo here.

Note that you don't actually need to use/require all of D3 just to use the nest operator. Use this extracted version of nest if this is all you need for your project.

nest = require 'nest'
{ok, deepEqual} = require 'assert'
eq = deepEqual
# input data to be nested
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
]
# expected output
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} ]
byType = (d) -> d.type # group by type
byColor = (d) -> d.color # group by color
byQuantity = (d) -> d.quantity # group by quantity
eq nest(data, byColor, byQuantity), expected
# expected output
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 } ]
eq nest(data, byType, byColor), expected
_ = require 'lodash'
# like `groupBy`, but for multi-level grouping
nest = (seq, iters...) ->
return seq unless iters.length
[first, rest...] = iters
_.mapValues _.groupBy(seq, first), (value) -> nest(value, rest...)
module.exports = nest
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment