0

I am trying to get all the items in a flat dataset that are grouped together to form a hierarchical dataset using Javascript/Node.JS.

I have a solution, but I don't think it's the most elegant and it could probably be improved.

I based my solution of the answer here Find all objects with matching Ids javascript

My dataset is as follows:

let data = [{cid: 1, clbl: 'Rush Shipping', pid:5, plbl: 'FedEx'},
        {cid: 2, clbl: 'Standard Shipping', pid:5, plbl: 'FedEx'},
        {cid: 3, clbl: 'First Class', pid:8, plbl: 'USPS'},
        {cid: 4, clbl: 'Std', pid:9, plbl: 'DHL'},
        {cid: 5, clbl: 'Canada Post', pid:1, plbl: 'Canada Post'},
       ];

I would like my output to be something like this:

[ { pid: 5,
    plbl: 'FedEx',
    methods: [
       {
         cid: 1,
         clbl: 'Rush Shipping',
       },
       {
         cid: 2,
         clbl: 'Standard Shipping',
       },
   },
   { pid: 8,
    plbl: 'USPS',
    methods: [
       {
         cid: 3,
         clbl: 'First Class',
       },
   },
   { pid: 9,
    plbl: 'DHL',
    methods: [
       {
         cid: 4,
         clbl: 'Std',
       },
   },
   { pid: 1,
    plbl: 'Canada Post',
    methods: [
       {
         cid: 5,
         clbl: 'Canada Post',
       },
   },
 ]

I threw together some code that works, but I imagine there has be be a more optimized way to do this and thought I would put it to the SO community.

Here's my solution:

var roots = [];

var all = {};
data.forEach(function(item) {
    all[item.pid] = item;
})
Object.keys(all).forEach(function(pid) {
  var items = data.filter(x => x.pid == pid);
  var addItem = {};
    items.forEach(function(item, j) {
    if (j === 0){
        addItem = {pid:item.pid, label:item.plbl, methods:[]};
     }
    addItem.methods.push({cid: item.cid, label: item.clbl});
    });
  roots.push(addItem);
})
console.log(roots);
SpaceCowboy74
  • 1,367
  • 1
  • 23
  • 46

2 Answers2

1

I don't think this is more 'optimized' from a memory/speed standpoint but it is a little shorter.

let new_data = Object.values(data.reduce(function(o, d) {
    o[d.pid] = o[d.pid] || {pid: d.pid, plbl: d.plbl, methods:[]};
    o[d.pid].methods.push({cid: d.cid, clbl: d.clbl});
    return o;
}, {}));

Basically take advantage of the reduce method in order to build one combined all object. Then use Object.values() to create an array from the values stored in the all object instead of manually pushing them.

kht
  • 580
  • 2
  • 8
0

I'm going to suggest a way to do this which is harder, not easier. But it will also involve creating a number of reusable functions.

I'm a big fan of the Ramda programming library (disclaimer: I'm one of its authors.) So when I try to do something like this, I reach for Ramda. And I find that I can code such transformations in its REPL simply by piping together a number of simpler steps.

My pass at this with Ramda looks like:

const transform = pipe(
  groupBy(prop('pid')),
  map(applySpec({
    pid: path([0, 'pid']),
    plbl: path([0, 'plbl']),
    methods: project(['cid', 'clbl'])
  })),
  values
)

let data = [{cid: 1, clbl: 'Rush Shipping', pid:5, plbl: 'FedEx'}, {cid: 2, clbl: 'Standard Shipping', pid:5, plbl: 'FedEx'}, {cid: 3, clbl: 'First Class', pid:8, plbl: 'USPS'}, {cid: 4, clbl: 'Std', pid:9, plbl: 'DHL'}, {cid: 5, clbl: 'Canada Post', pid:1, plbl: 'Canada Post'}];

console.log(transform(data))
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://bundle.run/ramda@0.26.1"></script><script>
const {pipe, groupBy, prop, map, applySpec, path, project, values} = ramda   </script>

All the functions in there are fairly reusable. So I could start my own little library by including simple versions of each of these functions. Then this code would be equally simple, and I could use those functions in other places in my application.

Here is another approach, using simplified versions of these functions. (Note that I rename map to mapObject here, as any such library I write would contain a simple map function that works much like Array.prototype.map. In Ramda, one function covers them both, but that's not as simple to do here.)

// In my utility library
const pipe = (fn1, ...fns) => (...args) =>
  fns.reduce((r, fn) => fn(r), fn1(...args))
const prop = (name) => (obj) => 
  obj[name]
const values = (obj) => 
  Object.values(obj)
const mapObject = (fn) => (obj) => 
  Object.keys(obj).reduce((a, k) => ({...a, [k]: fn(obj[k])}), {})
const groupBy = (fn) => (xs) =>
  xs.reduce((a, x) => ({...a, [fn(x)]: (a[fn(x)] || []).concat(x)}), {})
const applySpec = (s) => (o) =>
  Object.entries(s).reduce((a, [k, fn]) => ({...a, [k]: fn(o)}), {})
const path = (ns) => (obj) =>
  ns.reduce((v, n) => (v[n] || {}), obj)
const project = (ns) => (xs) =>
  xs.map(x => ns.reduce((a, n) => ({...a, [n]: x[n]}), {}))

// In current module
const transform = pipe(
  groupBy(prop('pid')),
  mapObject(applySpec({
    pid: path([0, 'pid']),
    plbl: path([0, 'plbl']),
    methods: project(['cid', 'clbl'])
  })),
  values
)

let data = [{cid: 1, clbl: 'Rush Shipping', pid:5, plbl: 'FedEx'}, {cid: 2, clbl: 'Standard Shipping', pid:5, plbl: 'FedEx'}, {cid: 3, clbl: 'First Class', pid:8, plbl: 'USPS'}, {cid: 4, clbl: 'Std', pid:9, plbl: 'DHL'}, {cid: 5, clbl: 'Canada Post', pid:1, plbl: 'Canada Post'}];

console.log(transform(data))
.as-console-wrapper { max-height: 100% !important; top: 0; }
<!-- Look, Ma, no Ramda -->

All those functions are available in the Ramda documentation. Many of them are more sophisticated there, but these simple implementations will take us a long way.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103