Home > Net >  Generate nesting object by path
Generate nesting object by path

Time:04-10

I want to make function which takes object path, for example

{ 'person.data.info.more.favorite': 'smth' }

and returns nested object:

{
  person: {
    data: {
      info: {
        more: {
          favorite: 'smth',
        },
      },
    },
  },
}

What's the most simple method to do it?

CodePudding user response:

If you only need to do a single key/value pair, a straightforward solution of nest using split and reduceRight -

const nest = ([path, value]) =>
  path.split('.').reduceRight((v, p) => ({ [p]: v }), value)

console.log(nest(['a', 'smth']))
console.log(nest(['a.b', 'smth']))
console.log(nest(['person.data.info.more.favorite', 'smth']))

{
  "a": "smth"
}
{
  "a": {
    "b": "smth"
  }
}
{
  "person": {
    "data": {
      "info": {
        "more": {
          "favorite": "smth"
        }
      }
    }
  }
}

If you need to apply this to an entire object where the key paths could possibly overlap with each other, we can write expand which depends on nest -

const input = {
  "server.host": "localhost",
  "server.port": 9999,
  "database.host": "localhost",
  "database.name": "mydb",
  "database.port": 7777,
  "database.user": "root",
  "debug.log": true
}

console.log(expand(input))
{
  "server": {
    "host": "localhost",
    "port": 9999
  },
  "database": {
    "host": "localhost",
    "name": "mydb",
    "port": 7777,
    "user": "root"
  },
  "debug": {
    "log": true
  }
}

We can write expand(o) to take Object.entries from the input object o and .map(nest) over each, and finally .reduce(merge) the result -

const expand = o =>
  Object.entries(o).map(nest).reduce(merge, {})

const nest = ([path, value]) =>
  path.split(".").reduceRight((v, p) => ({ [p]: v }), value)

const merge = (left = {}, right = {}) =>
  Object
    .entries(right)
    .map(([ k, v ]) =>
      isobject(v) && isobject(left[k])
        ? [ k, merge(left[k], v) ]
        : [ k, v ]
    )
    .reduce(assign, left)
      
const assign = (o, [ k, v ]) =>
  Object.assign(o, { [k]: v })

const isobject = t =>
  t?.constructor === Object   
    
const input = {
  "server.host": "localhost",
  "server.port": 9999,
  "database.host": "localhost",
  "database.name": "mydb",
  "database.port": 7777,
  "database.user": "root",
  "debug.log": true
}

console.log(expand(input))

CodePudding user response:

EDIT: Here is the correct answer sorry I misread it the first time.

let myObj = { 'person.data.info.more.favorite': 'smth' }

function getNestedObj( object ) {

  let keys = Object.keys( object )[0];
  let value = Object.values( object )[0];
  let keyArray = keys.split(".");

  let nextObject;
  let returnObj = {};
  

  keyArray.forEach( ( key ) => {
 
    let newObj = {};

    if( !nextObject ) {
      returnObj[key] = newObj
      nextObject = newObj;
    } else {
      nextObject[key] = newObj
      nextObject = newObj;
    }
    console.log(key)
  });

  nextObject[0] = value

  console.log( returnObj );

  return returnObj;
}

getNestedObj( myObj);

OLD ANSWER THAT READ QUESTION WRONG:

Hey you can accomplish this with the below code. It will only work with string values though with this implementation

"use strict";
let myObject = {
    person: {
        data: {
            info: {
                more: {
                    favorite: 'smth',
                },
                less: {
                    favourite: 'smthing else'
                },
                random: "a string"
            },
        },
    },
};
function concatKeys(object, key) {
    let keys = Object.keys(object);
    let keyDictionary = {};
    if (key && typeof object === 'string') {
        return { [key]: object };
    }
    if (keys.length > 0) {
        keys.forEach((nextKey) => {
            let newKey = key ? `${key}.${nextKey}` : nextKey;
            let nextObj = object[nextKey];
            let newDictionary = concatKeys(nextObj, newKey);
            keyDictionary = Object.assign(Object.assign({}, keyDictionary), newDictionary);
        });
    }
    return keyDictionary;
}
let output = concatKeys(myObject);
console.log(output);

You can see it working here Typescript playground

Running the above code will log this to the console.

[LOG]: {
  "person.data.info.more.favorite": "smth",
  "person.data.info.less.favourite": "smthing else",
  "person.data.info.random": "a string"
} 

Edit: Sorry I realize now you asked for this in javascript updated the answer to be in JS but the playground is still typescript but the above is JS.

CodePudding user response:

This version has a similar idea to the answer from Mulan, but with a different breakdown in responsibilities. It goes through a useful intermediate format that I've been calling pathEntries, a parallel to the result of Object.entries output, but containing full paths rather than just top-level properties. For this example, it would be just

[['person', 'data', 'info', 'more', 'favorite'], 'smth']

Our expand function calls Object .entries on your input, splits the keys on dots, and then calls hydrate on the result. hydrate is in my regular utility belt and is just a thin wrapper around setPath, folding the path entries into a single object. It looks like this:

const setPath = ([p, ...ps]) => (v) => (o) =>
  p == undefined ? v : Object .assign (
    Array .isArray (o) || Number .isInteger (p) ? [] : {},
    {...o, [p]: setPath (ps) (v) ((o || {}) [p])}
  )

const hydrate = (xs) =>
  xs .reduce ((a, [p, v]) => setPath (p) (v) (a), {})

const expand  = (o) =>
  hydrate (Object .entries (o) .map (([k, v]) => [k.split ('.'), v]))

console .log (expand ({'person.data.info.more.favorite': 'smth'}))

const input = {"server.host": "localhost", "server.port": 9999, "database.host": "localhost", "database.name": "mydb", "database.port": 7777, "database.user": "root", "debug.log": true}

console .log (expand (input))
.as-console-wrapper {max-height: 100% !important; top: 0}

One useful thing about this intermediate format is that it can also represent arrays, using integers for array indices and strings for object properties:

hydrate ([
  [[0, 'foo', 'bar', 0, 'qux'], 42], 
  [[0, 'foo', 'bar', 1, 'qux'], 43], 
  [[0, 'foo', 'baz'], 6],
  [[1, 'foo', 'bar', 0, 'qux'], 44], 
  [[1, 'foo', 'bar', 1, 'qux'], 45], 
  [[1, 'foo', 'baz'], 7],  
]) //=> 
// [
//   {foo: {bar: [{qux: 42}, {qux: 43}], baz: 6}}, 
//   {foo: {bar: [{qux: 44}, {qux: 45}], baz: 7}}
// ]

But to use that feature here, we would probably need a more sophisticated string format, something like { '[0].foo.bar[0].qux': 42 } and we'd need a somewhat more sophisticated parser than just k .split ('.'). This is not difficult, but would take us further afield, as would dealing with the still more sophisticated system that allows for arbitrary string keys, including quotation marks, brackets, and periods.

  • Related