Home > Enterprise >  How to dynamically create nested arrays based on a substring in input data in javascript?
How to dynamically create nested arrays based on a substring in input data in javascript?

Time:11-02

I have an input data like this:

[
    {
        "name": "outField2",
        "value": "something"
    },
    {
        "name": "outField3[index].outField4",
        "value": "something"
    },
    {
        "name": "outField3[index].outField5",
        "value": "something"
    },
    {
        "name": "outField3[index].outField6.outField7",
        "value": "something"
    }
]

I am trying to achieve an output like this based on substring "[index]" (i.e. if that substring is not present then that element should be an object instead of an array):

{
  "outField2": "something",
  "outField3[index]": [{
    "outField4": "something",
    "outField5": "something",
    "outField6": {
      "outField7": "something"
    }
  }]
}

My current code (below) is able to produce the outField3 as an object if there is no substring '[index]' but I unable to find a good solution to generate it as an array in the presence of the substring. Can someone help out? I tried a few options but none give me the desired result.

const map = [
    {
        "name": "outField2",
        "value": "something"
    },
    {
        "name": "outField3[index].outField4",
        "value": "something"
    },
    {
        "name": "outField3[index].outField5",
        "value": "something"
    },
    {
        "name": "outField3[index].outField6.outField7",
        "value": "something"
    }
];

let mapOutput = {};

map.forEach(doThis);

function doThis(item, index) {
    let path = map[index].name.split(".");
    if (path.length > 1) {
        createNestedObject(mapOutput, path, map[index].value);
    } else {
        mapOutput[map[index].name] = map[index].value;
    };
};

function createNestedObject(element, path, value) {
    var lastElement = arguments.length === 3 ? path.pop() : false;

    for (var i = 0; i < path.length; i  ) {
        if (path[i].includes('[index]')) {
            /*some logic here to push the child elements
            that do not contain [index] as an array into
            the ones that contain [index]*/
        } else {
            element = element[path[i]] = element[path[i]] || {};
        };
    }

    if (lastElement) element = element[lastElement] = value;

    return element;
};

let mapOutputJSON = JSON.stringify(mapOutput, null, 2);
console.log(mapOutputJSON);

CodePudding user response:

you can do something like this

const data = [{
    "name": "outField2",
    "value": "something"
  },
  {
    "name": "outField3[index].outField4",
    "value": "something"
  },
  {
    "name": "outField3[index].outField5",
    "value": "something"
  },
  {
    "name": "outField3[index].outField6.outField7",
    "value": "something"
  }
]

const buildObject = (paths, value, obj) => {
  if (paths.length === 0) {
    return value
  }
  const [path, ...rest] = paths
  if(path.includes('[index]')) {
    
    return {
    ...obj,
    [path]: [buildObject(rest, value, (obj[path] || [])[0] || {})]
  }
  }
  return {
    ...obj,
    [path]: buildObject(rest, value, obj[path] || {})
  }
}

const result = data.reduce((res, {
    name,
    value
  }) => buildObject(name.split('.'), value, res), {})

console.log(result)

CodePudding user response:

A possible generic approach which in my opinion also assigns the correct type of the OP's "outField3[index]" property (object type instead of an Array instance) is based on reduce where ...

  • the outer loop iterates the array of { name, value } items
  • by executing a single function accumulateObjectTypeFromPathAndValue where ...
    • this function does split each name-value into an array of object-path keys which then gets iterated by the inner reduce method where the passed object programmatically accumulates nested key-value pairs.

function accumulateObjectTypeFromPathAndValue(root, path, value) {
  path
    .split('.')
    .reduce((obj, key, idx, arr) => {

      if (!obj.hasOwnProperty(key)) {
        Object.assign(obj, {
          [ key ]: (idx === arr.length - 1)
            ? value
            : {},
        });
      }
      return obj[key];

    }, root);

  return root;
}
console.log(
  [{
    "name": "outField2",
    "value": "something"
  }, {
    "name": "outField3[index].outField4",
    "value": "something"
  }, {
    "name": "outField3[index].outField5",
    "value": "something"
  }, {
    "name": "outField3[index].outField6.outField7",
    "value": "something"

  }].reduce((result, { name: path, value }) => {
    return accumulateObjectTypeFromPathAndValue(result, path, value);
  }, {})
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

The above implementation of the 2nd reducer function then could by changed according to the OP's custom array-type requirements ...

function accumulateCustomObjectTypeFromPathAndValue(root, path, value) {
  path
    .split('.')
    .reduce((obj, key, idx, arr) => {

      if (!obj.hasOwnProperty(key)) {
        Object.assign(obj, {
          [ key ]: (idx === arr.length - 1)
            ? value
            : {},
        });
        if (key.endsWith('[index]')) {
          obj[ key ] = [obj[ key ]];
        }
      }
      return Array.isArray(obj[ key ])
      //? obj[ key ].at(-1)                 // last item.
        ? obj[ key ][obj[ key ].length - 1] // last item.
        : obj[ key ];

    }, root);

  return root;
}
console.log(
  [{
    "name": "outField2",
    "value": "something"
  }, {
    "name": "outField3[index].outField4",
    "value": "something"
  }, {
    "name": "outField3[index].outField5",
    "value": "something"
  }, {
    "name": "outField3[index].outField6.outField7",
    "value": "something"

  }].reduce((result, { name: path, value }) => {
    return accumulateCustomObjectTypeFromPathAndValue(result, path, value);
  }, {})
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

  • Related