Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to split object into nested object? (Recursive way)

I have a data set containing underscore(_) variable name. Such as below:

const data = {
   m_name: 'my name',
   m_address: 'my address',
   p_1_category: 'cat 1',
   p_1_name: 'name 1',
   p_2_category: 'cat 2',
   p_2_name: 'name 2'
}

I want to split them into nested object/array, Below is the result I want.

{
 m: {
     name: "my name",
     address: "my address"
 },
 p: {
    "1": {category: 'cat 1', name: 'name 1'}, 
    "2": {category: 'cat 2', name: 'name 2'}
 } 
}

How can I write a recursive method to achive it instead of hardcoded? Maybe it should allow to handle deeper nested object such as "p_2_one_two_category: 'value'" into p:{2:{one:{two:category:'value'}}}

var data ={
  m_name: 'my name',
  m_address: 'my address',
  p_1_category: 'cat 1',
  p_1_name: 'name 1',
  p_2_category: 'cat 2',
  p_2_name: 'name 2',
  p_2_contact: '1234567',
  k_id: 111,
  k_name: 'abc'
}

var o ={};
Object.keys(data).forEach(key => {
  var splited =  key.split(/_(.+)/);
  if (!o[splited[0]]) {
    o[splited[0]] = {};
  }
  var splited1 = splited[1].split(/_(.+)/);
  if (splited1.length < 3) {
    o[splited[0]][splited[1]] = data[key];
  } else {
    if (!o[splited[0]][splited1[0]]){ 
      o[splited[0]][splited1[0]] = {};
    }
    o[splited[0]][splited1[0]][splited1[1]] = data[key];
  }
});
console.log(o);
like image 938
Devb Avatar asked Apr 08 '20 15:04

Devb


2 Answers

You could use reduce method that will create similar nested structure with just objects.

var data = {
  m_name: 'my name',
  m_address: 'my address',
  p_1_category: 'cat 1',
  p_1_name: 'name 1',
  p_2_category: 'cat 2',
  p_2_name: 'name 2',
  p_2_contact: '1234567',
  k_id: 111,
  k_name: 'abc'
}


const result = Object
  .entries(data)
  .reduce((a, [k, v]) => {
    k.split('_').reduce((r, e, i, arr) => {
      return r[e] || (r[e] = arr[i + 1] ? {} : v)
    }, a)

    return a
  }, {})

console.log(result)
like image 127
Nenad Vracar Avatar answered Sep 17 '22 16:09

Nenad Vracar


I don't know if that output format was what you were really looking for or simply the best you were able to accomplish. One alternative would be to generate something like this:

{
  m: {name: "my name", address: "my address"},
  p: [
    {category: "cat 1", name: "name 1"},
    {category: "cat 2", name: "name 2"}
  ]
}

There is one major difference between this and your code's output. p is now a plain array of objects rather than a 1- and 2-indexed object. It's quite possible that this is not helpful to you, but it's an interesting alternative. There is also a second difference from the sample output you supplied. Both your original code and the answer from Nenad return m: {name: "my name", address: "my address"} instead of the requested m: [{name: "my name"}, {address: "my address"}]. This seems much more logical to me, and I've also done it this way.

Here is some code that would do this:

// Utility functions

const isInt = Number.isInteger

const path = (ps = [], obj = {}) =>
  ps .reduce ((o, p) => (o || {}) [p], obj)

const assoc = (prop, val, obj) => 
  isInt (prop) && Array .isArray (obj)
    ? [... obj .slice (0, prop), val, ...obj .slice (prop + 1)]
    : {...obj, [prop]: val}

const assocPath = ([p = undefined, ...ps], val, obj) => 
  p == undefined
    ? obj
    : ps.length == 0
      ? assoc(p, val, obj)
      : assoc(p, assocPath(ps, val, obj[p] || (obj[p] = isInt(ps[0]) ? [] : {})), obj)

// Main function

const hydrate = (flat) => 
  Object .entries (flat) 
    .map (([k, v]) => [k .split ('_'), v])
    .map (([k, v]) => [k .map (p => isNaN (p) ? p : p - 1), v])
    .reduce ((a, [k, v]) => assocPath (k, v, a), {})

// Test data

const data = {m_name: 'my name', m_address: 'my address', p_1_category: 'cat 1', p_1_name: 'name 1', p_2_category: 'cat 2', p_2_name: 'name 2' }

// Demo

console .log (
  hydrate (data)
)
.as-console-wrapper {min-height: 100% !important; top: 0}

This code is inspired by Ramda (of which I'm an author). The utility functions path, assoc, and assocPath have similar APIs to Ramda's, but these are unique implementations (borrowed from another answer.) Since these are built into Ramda, only the hydrate function would be necessary if your project used Ramda.

The big difference between this and Nenad's (excellent!) answer is that our object hydration takes into account the difference between string keys, which are assumed to be for plain objects, and numeric ones, which are assumed to be for arrays. However, since these are split out of our initial string (p_1_category), this could lead to ambiguity if you might sometimes want those to be objects.

I also use a trick that is a bit ugly and maybe wouldn't scale to other numeric values: I subtract 1 from the number so that the 1 in p_1_category maps to the zeroth index. If your input data looked like p_0_category ... p_1_category instead of p_1_category ... p_2_category we could skip this.

In any case, there is a real chance that this is contrary to your underlying requirements, but it might be useful to others.

like image 36
Scott Sauyet Avatar answered Sep 19 '22 16:09

Scott Sauyet