Expand a simple object containing an array of simple objects into a single, flat object

update: The issue was incorrectly described at first. I have completely rewritten the description, along with sharing a code snippet that may not be aesthetically pleasing but gets the job done to some extent.


Imagining an object:

const input = {
  a: 1,
  b: '2',
  c: {
    d: true,
    e: '4'
  },
  f: [{
    g: 5,
    h: {
      i: '6'
    }
  }, {
    g: 7,
    h: {
      i: '8'
    }
  }]
}

My goal is to create a collection of all potential arrangements of nested arrays, where keys of the object are flattened and joined with ".", such as:

[{
  a: 1,
  b: '2',
  'c.d': true,
  'c.e': '4',
  'f.g': 5,
  'f.h.i': '6'
}, {
  a: 1,
  b: '2',
  'c.d': true,
  'c.e': '4',
  'f.g': 7,
  'f.h.i': '8'
}]

Note that non-primitive values for keys like 'f.h' pointing to an object are not included in this scenario.

To achieve this, I start by gathering all keys and adding a "#" sign to array item keys to signify "every index in that array":

function columns(data, prefix = '') {
  if (_.isArray(data)) {
    return columns(_.first(data), `${prefix}.#`);
  } else if (_.isObject(data)) {
    return _.filter(_.flatMap(_.keys(data), key => {
      return _.concat(
        !_.isObject(_.result(data, key)) ? `${prefix}.${key}` : null,
        columns(data[key], `${prefix}.${key}`)
      );
    }));
  } else {
    return null;
  }
}

console.log(columns(input)); // -> [".a", ".b", ".c.d", ".c.e", ".f.#.g", ".f.#.h.i"]

Using lodash, I flatten the object into a single-level object with unconventional keys:

function flattenKeys(original, keys) {
  return _.mapValues(_.groupBy(_.map(keys, key => ({
    key,
    value: _.result(original, key)
  })), 'key'), e => _.result(e, '0.value'));
}

console.log(flattenKeys(input, columns(input))) // -> {".a":1,".b":"2",".c.d":true,".c.e":"4"}

By traversing through every array-like property of the original object, I produce an array of objects setting keys like .f.#.h.i with values from .f.0.h.i for the first element, and so on:

function unfold(original, keys, iterables) {
  if (!_.isArray(iterables)) {
    return unfold(original, keys, _.uniq(_.map(_.filter(keys, key => /#/i.test(key)), key => _.replace(key, /\.\#.*/, '')));
  } else if (_.isEmpty(iterables)) {
    return [];
  } else {
    const first = _.first(iterables);
    const rest = _.tail(iterables);
    const values = _.result(original, first);
    const flatKeys = _.mapKeys(_.filter(keys, key => _.includes(key, first)));
    const updated = _.map(values, (v, i) => ({
      ...flattenKeys(original, keys),
      ..._.mapValues(flatKeys, k => _.result(original, _.replace(k, /\#/, i)))
    }));

    return _.concat(updated, unfold(original, keys, rest));
  }
}

console.log(unfold(input, columns(input))) // -> [{".a":1,".b":"2",".c.d":true,".c.e":"4",".f.#.g":5,".f.#.h.i":"6"},{".a":1,".b":"2",".c.d":true,".c.e":"4",".f.#.g":7,".f.#.h.i":"8"}]

In conclusion, although the code may appear messy, cleaning up the keys isn't essential in this case.

The main concern remains how to adapt this solution for scenarios involving multiple array-like properties in original objects.


Upon reflection, it seems that this question would be better suited for CodeReview StackExchange. If someone decides to move it there, I am perfectly fine with that decision.

Answer №1

After restructuring, the recursive function below can efficiently handle the task:

function unfold(input) {
  function flatten(obj) {
    var result = {},
        f,
        key,
        keyf;

    for(key in obj) {
      if(obj[key] instanceof Array) {
        obj[key].forEach(function(k) {
          f = flatten(k);
          for(keyf in f) {
            result[key+'.'+keyf] = f[keyf];
          }
          output.push(JSON.parse(JSON.stringify(result))); //simple object cloning
        });
      } else if(obj[key] instanceof Object) {
        f = flatten(obj[key]);
        for(keyf in f) {
          result[key+'.'+keyf] = f[keyf];
        }
      } else {
        result[key] = obj[key];
      }
    }
    return result;
  } //flatten

  var output = [];
  flatten(input);
  return output;
} //unfold

Code Snippet:

function unfold(input) {
  function flatten(obj) {
    var result = {},
        f,
        key,
        keyf;

    for(key in obj) {
      if(obj[key] instanceof Array) {
        obj[key].forEach(function(k) {
          f = flatten(k);
          for(keyf in f) {
            result[key+'.'+keyf] = f[keyf];
          }
          output.push(JSON.parse(JSON.stringify(result))); //simple object cloning
        });
      } else if(obj[key] instanceof Object) {
        f = flatten(obj[key]);
        for(keyf in f) {
          result[key+'.'+keyf] = f[keyf];
        }
      } else {
        result[key] = obj[key];
      }
    }
    return result;
  } //flatten
  
  var output = [];
  flatten(input);
  return output;
} //unfold

const input = {
  a: 1,
  b: '2',
  c: {
    d: true,
    e: '4'
  },
  f: [{
    g: 5,
    h: {
      i: '6'
    }
  }, {
    g: 7,
    h: {
      i: '8'
    }
  }]
};

document.body.innerHTML+= '<pre>' + JSON.stringify(unfold(input), null, 2) + '</pre>';


I'll preserve my initial solution that was compatible with your original structure:

var o = {a: [{b: 1, c: 2}], d: [{e: 4, f: 5}]},
    keys = Object.keys(o),
    result = [];

keys.forEach(function(i, idx1) {
  keys.forEach(function(j, idx2) {
    if(idx2 > idx1) { //avoid duplicates
      for(var k in o[i][0]) {
      for(var l in o[j][0]) {
          result.push({
            [i + '.' + k]: o[i][0][k],
            [j + '.' + l]: o[j][0][l]
          });
        }
      }
    }
  });
});

console.log(JSON.stringify(result));

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the best way to assign three different dates in my protractor test?

I am facing an issue with setting random dates in 3 date fields in a row using Protractor. The problem is that Protractor sets the dates too quickly and sometimes assigns invalid dates like: first data: 01/08/1990 (correct) second data: 01/09/0009 (inva ...

Enhancing .gitignore with conditional logic for more efficient file exclusion

In my TypeScript project, I am facing an issue with unnecessary merge conflicts caused by the generated .d.ts and .js files. Since my project is quite large and only halfway converted from JS to TS, I cannot simply ignore all .js files using .gitignore. A ...

What is the process for adding a layer to an HTML view or adjusting the z-index of a layer?

Why is my menu option appearing below the scrolling view? I have a menu button on my screen, and when I click on it, the menu options appear but the content scrolls above the menu. How can I make the menu option appear above the scrolling content? Link ...

Creating a test suite with Jasmine for an Angular ui-grid component compiled without using $scope

I have encountered an issue while using $compile to compile a ui-grid for Jasmine testing. Initially, everything worked smoothly when I passed $scope as a parameter to the controller. However, I am now transitioning to using vm, which has resulted in $comp ...

Refresh the default selection in the select list after applying a filter

Trying to customize a drop down list of options based on radio button selection. <div> <div ng-controller="sampleCtrl"> <label> <input type="radio" name="type" ng-value="1" ng-model="selectedType" />A</la ...

Retrieve the value of a cell in Google Sheets

Seeking assistance on how to retrieve the content of a cell in Google Sheets and store it for comparison purposes at a later time. My search efforts have led me to various online resources such as the Visualization API and the Sheets Script, but I have n ...

Having difficulty with Axios due to the URL containing a potent # symbol

When I pass a URL in axios, such as: https://jsonplaceholder.typicode.com/todos/#abc?pt=1 I only seem to receive the base URL in my network requests: https://jsonplaceholder.typicode.com/todos/ If anyone has insight on correctly passing URLs with #, yo ...

ruby - what is the way to get access to a specific variable

Below is an example array: { :info=> [ { "name" =>"John", "age" =>25 }, { "name" =>"Sarah", "age" =>30 } ] } If I want to access the second element's "name" in ...

Thinking about how to incorporate the value of a JavaScript variable into a CSS variable?

I am attempting to utilize Window.innerWidth and load the resulting value into a CSS variable, but it doesn't appear to be functioning correctly. What could I be doing incorrectly? function myFunction() { var w = window.innerWidth; document. ...

Troubleshooting: Dealing with ng-click and invalid functions

Prior to resolving the issue, I encountered a component (within an HTML template) containing a ng-click that was invoking a nonexistent function. Is there a method to enable a strict mode (similar to 'use strict' in JS) or something equivalent t ...

What's the reason behind JavaScript's Every method not functioning properly?

I'm struggling to understand why the array method 'every' is not functioning properly in my project (working on a roguelike dungeon crawler game). Here's an example of the array of objects I am working with: { x: newrm.x, ...

Internet Explorer 11 displays a white X instead of the image

While developing a Single Page Application on Angular 1.5, I encountered a frustrating bug in IE11. Instead of rendering my pictures correctly, I see a white X in a black box. Strangely, the browser is receiving a 200 code response for the image request. C ...

Create an interface object in TypeScript with no initial properties

I'm currently developing an app with angular 5 (TS) and I've encountered a problem while trying to initialize an empty object of my interface. The solution that I found and attempted is as follows: article: Article = {} as Article; However, ...

AngularJS directives adapting to changes in DOM attributes

I have been developing a scrollspy module for AngularJS, and I am facing the challenge of keeping the scrollspy data up to date when dealing with dynamic content on the page. What is the best approach in AngularJS to address this issue? Is it advisable fo ...

Angularjs directive experiencing intermittent firing of IntersectionObserver

Currently, I am in the process of integrating lazy loading using the IntersectionObserver within my angularjs application. However, there seems to be an issue where the callback function of the observer is not always being triggered when scrolling up and ...

Achieving success by correctly reaching the window's edge during an event (onscroll)

This happens to be my 1st inquiry. Specifically, I have a navigation menu with a transparent background and I am attempting to alter the background once it reaches the top edge of the window. window.addEventListener("scroll", navSticky); function navSt ...

How to Display a Row of a 2D Character Array in C++

As a beginner, I am facing an issue with displaying the rows of a 2D array one by one in my program. Despite executing the code properly, I am seeing all the rows in the matrix starting from the one I require. Below is the code snippet: #include<iostr ...

Open a new tab using the JavaScript href

I've encountered an issue while working on a website where the developer has used href="javascript" for the links. Whenever a user clicks on these links, I want them to open in a new tab. However, when I set the target attribute to _blank or the base ...

Selecting the appropriate technology or library for incorporating user-defined text along a designated path within established areas

I am currently developing an admin dashboard with CodeIgniter 2 that allows the admin to upload custom images, particularly ones with blank spaces for text overlay. The goal is to enable regular users to add their desired text in specific areas defined by ...

"Troubangular: Troubleshooting unexpected behavior when trying to update ngFor after setting a property

Dealing with what seems like a straightforward component here. I'm fetching an array of objects from an API, storing them in a property, and then displaying them in a select list for the user to choose from. When the value changes, I filter the result ...