Create a function that recursively maps data across multiple levels

Currently, I have a data mapping function that can take JSON data with up to four levels and transform it into a different format.

The input JSON format looks like this:

[{
    "InventoryLevel2Id": "1234",
    "InventoryLevel2Information": "Test Data",
    "InventoryLevel2Name": "Test Data",
    "InventoryLevel3s": [
        {
            "InventoryLevel3Id": "5678",
            "InventoryLevel3Name": "Inner data at 1",
            "InventoryLevel3Information": "Inner info at 1",
            "InventoryLevel4s": [
                {
                    "InventoryLevel4Id": "9101112",
                    "InventoryLevel4Name": "Inner data at 2",
                    "InventoryLevel4Information": "Inner info at 2",
                    "InventoryLevel5s": [
                        {
                            "InventoryLevel5Id": "131415",
                            "InventoryLevel5Name": "Inner data at 3",
                            "InventoryLevel5Information": "Inner info at 3"
                        }
                    ]
                }
            ]
        }
    ]
}]

The output JSON format should be transformed to:

[{
    label1: 'TestData',
    label2: "Test data",
    uniquieId: "1234",
    innerState: {
        data : {
            label1: 'Inner data at 1',
            label2: "Inner info at 1",
            uniquieId: "5678",
            innerState: {
                data: {
                    label1: 'Inner data at 2',
                    label2: "Inner info at 2",
                    uniquieId: "9101112",
                    innerState: {
                        data: {
                            label1: 'Inner data at 3',
                            label2: "Inner info at 4",
                            uniquieId: "131415"
                        }
                    }
                }
            }
        }
    }
}]

To enhance the efficiency and reduce repetitive calls to the mapper function, I am considering creating a recursive function that will handle all mapping for multiple levels.

const recursiveDataMapper = (inputData) => {
    const mappedData = inputData?.map((a) => ({
      label1: a.InventoryLevel2Name,
      label2: a.InventoryLevel2Information,
      uniquieId: a.InventoryLevel2Id,
      innerState: {
        data: a.InventoryLevel3s?.map((b) => ({
          label1: b.InventoryLevel3Name,
          label2: b.InventoryLevel3Information,
          uniquieId: b.InventoryLevel3Id,
          innerState: {
            data: b.InventoryLevel4s?.map((c) => ({
              label1: c.InventoryLevel4Name,
              label2: c.InventoryLevel4Information,
              uniquieId: c.InventoryLevel4Id,
              innerState: {
                data: c.InventoryLevel5s?.map((d) => ({
                  label1: d.InventoryLevel5Name,
                  label2: d.InventoryLevel5Information,
                  uniquieId: d.InventoryLevel5Id
                }))
              }
            }))
          }
        }))
      }
    }));
    return mappedData;
};

While trying to optimize the mapping process, my initial attempt at a mapper function only covers up until InventoryLevel3s. It lacks completeness and could benefit from further improvement.

const tryRecursiveDataMapper = (inputData) => {
  const mappedData = inputData?.map((a) =>
    helper(
      a,
      "InventoryLevel2Name",
      "InventoryLevel2Information",
      "InventoryLevel3Id",
      "InventoryLevel3s"
    )
  );
  return mappedData;
};

const helper = (entity, field1, field2, uniquieId, childEntityName) => {
  if (entity) {
    return {
      label1: entity[field1],
      label2: entity[field2],
      uniquieId: entity[uniquieId],
      concurrencyId: entity.concurrencyId,
      innerState: {
        data: entity[childEntityName]?.map((innerData) =>
          helper(
            innerData,
            "InventoryLevel3Name",
            "InventoryLevel3Information",
            "InventoryLevel3Id",
            "InventoryLevel4s"
          )
        ),
      },
    };
  }
};

Answer №1

Below is a recursive solution that will provide the desired output. It identifies the current level by extracting numeric values from the first key of the input object, and then proceeds to copy the relevant properties to the output based on this information:

const data = [{
  "InventoryLevel2Id": "1234",
  "InventoryLevel2Information": "Test Data",
  "InventoryLevel2Name": "Test Data",
  "InventoryLevel3s": [{
    "InventoryLevel3Id": "5678",
    "InventoryLevel3Name": "Inner data at 1",
    "InventoryLevel3Information": "Inner info at 1",
    "InventoryLevel4s": [{
      "InventoryLevel4Id": "9101112",
      "InventoryLevel4Name": "Inner data at 2",
      "InventoryLevel4Information": "Inner info at 2",
      "InventoryLevel5s": [{
        "InventoryLevel5Id": "131415",
        "InventoryLevel5Name": "Inner data at 3",
        "InventoryLevel5Information": "Inner info at 3",
      }],
    }]
  }]
}]

const mapdata = (data) => {
  let level = +Object.keys(data)[0].replace(/[^\d]/g, '');
  const obj = {
    label1: data['InventoryLevel' + level + 'Name'],
    label2: data['InventoryLevel' + level + 'Information'],
    uniqueId: data['InventoryLevel' + level + 'Id']
  };
  level++;
  if (data.hasOwnProperty('InventoryLevel' + level + 's')) {
    obj.innerState = data['InventoryLevel' + level + 's'].map(mapdata);
  }
  return obj;
}

out = data.map(mapdata);
console.log(out);

Answer №2

My approach is to utilize a wildcard for the number matching, allowing for a simple recursive function to handle the task.

const data = [{
    "InventoryLevel2Id": "1234",
    "InventoryLevel2Information": "Test Data",
    "InventoryLevel2Name": "Test Data",
    "InventoryLevel3s": [
        {
            "InventoryLevel3Id": "5678",
            "InventoryLevel3Name": "Inner data at 1",
            "InventoryLevel3Information": "Inner info at 1",
            "InventoryLevel4s": [
                {
                    "InventoryLevel4Id": "9101112",
                    "InventoryLevel4Name": "Inner data at 2",
                    "InventoryLevel4Information": "Inner info at 2",
                    "InventoryLevel5s": [
                        {
                            "InventoryLevel5Id": "131415",
                            "InventoryLevel5Name": "Inner data at 3",
                            "InventoryLevel5Information": "Inner info at 3",
                        }
                    ],
                }
            ]
        }
    ]
}]

const mapRecursive = (inputData) =>
    inputData.map(item =>
        Object.keys(item).reduce((obj, key) => {
            if (key.match("InventoryLevel.Id")) return { ...obj, uniquieId: item[key] };
            if (key.match("InventoryLevel.Name")) return { ...obj, label1: item[key] };
            if (key.match("InventoryLevel.Information")) return { ...obj, label2: item[key] };
            if (key.match("InventoryLevel.s")) return { ...obj, innerState: { data: mapRecursive(item[key]) } };
        }, {})
    );

console.log(mapRecursive(data));

Answer №3

It could be beneficial to enhance the declarative nature of this by utilizing a helper function.

In this updated version, a list of mappings between regular expressions and new keys is used, with a boolean flag indicating whether to iterate. By leveraging the transform helper function, the code for addressing this problem appears as follows:

const mapData = transform ([
  {oldKey: /^InventoryLevel\d+Id$/, newKey: 'uniqueId'},
  {oldKey: /^InventoryLevel\d+Name$/, newKey:'label1'},
  {oldKey: /^InventoryLevel\d+Information$/, newKey: 'label2'},
  {oldKey: /^InventoryLevel\d+s$/, newKey: 'innerState', recur: true},
])

The utility of the transform function has now expanded beyond just this specific issue. What's crucial here is not only the reusability but also the clear separation between node conversion and recursion on one side, and the specifics of individual node transformations on the other. This approach makes it easy to identify where additional nodes can be incorporated.

Here is an implementation of the transform function:

const transform = (config) => (objs) =>
  objs .map ((obj) =>
    Object .fromEntries (Object .entries (obj) .flatMap (([k, v]) => { 
      const {newKey = '', recur = false} = config .find (({oldKey}) => oldKey .test (k))
      return newKey
        ? [[newKey, recur ? transform (config) (v) : v]]
        : []       
    }))
  )

const mapData = transform ([
  {oldKey: /^InventoryLevel\d+Id$/, newKey: 'uniqueId'},
  {oldKey: /^InventoryLevel\d+Name$/, newKey:'label1'},
  {oldKey: /^InventoryLevel\d+Information$/, newKey: 'label2'},
  {oldKey: /^InventoryLevel\d+s$/, newKey: 'innerState', recur: true},
])

const input = [{InventoryLevel2Id: "1234", InventoryLevel2Information: "Test Data", InventoryLevel2Name: "Test Data", Inventory...
console .log (mapData (input))
.as-console-wrapper {max-height: 100% !important; top: 0}

It's worth noting that the output aligns with the format in Nick's response. Although the requested format doesn't match the comment mentioning multiple values at each level, even after removing the data node. The key point is that the innerState elements are arrays rather than plain objects.

There's flexibility to further generalize the transform function. However, there's a tradeoff involved. The more generic we make it, the more complexity we introduce in the configuration. To illustrate, my initial version looked something like this:

const transform = (config) => (objs) =>
  objs .map ((obj) => 
    Object .fromEntries (Object .entries (obj) .flatMap (([k, v]) => 
      (config .find (({test}) => test(k)) || {result: () => []}) .result (v)
    ))
  )

const mapData = transform ([
  {test: (k) => /^InventoryLevel\d+Id$/.test(k), result: (v) => [['uniqueId', v]]},
  {test: (k) => /^InventoryLevel\d+Name$/.test(k), result: (v) => [['label1', v]]},
  {test: (k) => /^InventoryLevel\d+Information$/.test(k), result: (v) => [['label2', v]]},
  {test: (k) => /^InventoryLevel\d+s$/.test(k), result: (v) => [['innerState', mapData (v)]]},
])

The transform function offers more versatility in this case. It accommodates arbitrary test functions in the configuration combined with output functions that can generate necessary nodes. For instance, it allows transforming one input node into three output nodes. Yet, notice how much intricate the configuration array becomes. Instead of {oldKey: /^InventoryLevel\d+Id$/, we end up using

test: (k) => /^InventoryLevel\d+Id$/.test(k)
. Moreover, calling the recursion explicitly is required instead of relying on a boolean property.

If desired, we could simplify the configuration further at the expense of a slightly more complex transform function. An alternative approach may look like this:

const transform = (config) => (objs) =>
  objs .map ((obj) =>
    Object .fromEntries (Object .entries (obj) .flatMap (([k, v]) => { 
      const {newKey, recur} = config .find (
        ({oldKey}) => new RegExp (`^${oldKey .replace ('#', '\\d+')}$`) .test (k)
      )
      return newKey
        ? [[newKey, recur ? transform (config) (v) : v]]
        : []       
    }))
  )

const mapData = transform ([
  {oldKey: 'InventoryLevel#Id', newKey: 'uniqueId'},
  {oldKey: 'InventoryLevel#Name', newKey:'label1'},
  {oldKey: 'InventoryLevel#Information', newKey: 'label2'},
  {oldKey: 'InventoryLevel#s', newKey: 'innerState', recur: true},
])

In this modified setup, the configuration requires no knowledge about regular expressions, allowing us to employ a simple # wildcard for a sequence of digits. While this streamlines the configuration and makes transform less powerful, it simplifies the setup. If you know that transform won't be reused, this streamlined variant could be favorable.

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

Issue with AngularJS script halting when reaching factory function that returns a promise

I have been working on a beginner project that is essentially a simple task manager (similar to those todo list projects). I created a user login page for this project and here is how it functions. There are two functions, siteLogin() for logging in, and ...

Is there a way to transform a PNG image binary string into base64 encoding without saving it to disk?

After successfully passing an image as a binary string out of puppeteer's page.evaluate() function back to node.js, I used the following code: async function getBinaryString(url) { return new Promise(async (resolve, reject) => { const ...

Executing a method during the initialization process in app.component.ts

One thing I've noticed is that the <app-root> component in Angular doesn't implement OnInit like all the other components. It may sound silly, but let's say I wanted to add a simple console.log('Hello World') statement to dis ...

Will there ever be a possibility in the future to compile from D 2.0 to Javascript?

An experienced C++ programmer (that's me) is venturing into other programming languages and considering the value of learning more about D 2.0. The clean, fresh rewrite of D has caught my eye with its pragmatic and wise choices. Now, I'm eager to ...

GWT integration for TinyMCE

I've been attempting to incorporate TinyMCE with GWT's RichTextBox, but so far I haven't had any luck. The issue seems to be that GWT turns the Rich text area into a #document, rather than a standard textarea in HTML. Does anyone know how to ...

The functionality of Vue slot-scope seems to be restricted when used within nested child components

Here is the component configuration being discussed: Vue.component('myComponent', { data () { return { msg: 'Hello', } }, template: ` <div class="my-component"> <slot :ms ...

Observe the present time in a specific nation

Is there an authorized method to obtain and showcase the current accurate GMT time instead of relying on the easily manipulable local time on a computer? I am looking for a reliable way to acquire the current time in hours/minutes, so I can make calculati ...

Enriching JSON Data with Additional Values

Imagine having a JSON data set structured like this: { "fruits": { "apple": { "color": red, "size": small } "orange": { "color": orange, "size": small } } Is there a way to dynamically add a name attribute ...

Encountering an issue with Angular: The type 'boolean | Observable<boolean>' cannot be assigned to the type 'boolean'

Check out this answer to understand the approach I am using for implementing the master guard. I have multiple guards that need to be executed in sequence. In Master Guard - return guard.canActivate(this.route, this.state); The complete function is outli ...

Conceal content after a specific duration and display an alternative in its position, continuously repeating the loop

Imagine creating a captivating performance that unfolds right before your eyes. Picture this: as soon as the page loads, the initial text gracefully fades away after just three seconds. In its place, a mesmerizing animation reveals the second text, which ...

Is it possible to use JavaScript to toggle the visibility of the date picker when using the HTML5 input type=date?

Looking to personalize the HTML5 input type="date" element by integrating a separate button that triggers the visibility of the date picker dropdown. Struggling to find any information on this customization. Any assistance would be greatly valued! ...

Angular is not displaying the data from the dynamically injected component in the main component

I have encountered an issue where I am attempting to showcase a component's HTML view within another component in a chatbot scenario. Let's refer to them as the chat component and component 2. Essentially, the chat component – responsible for r ...

What is the reason for the excessive width of the final column in this table?

I am currently working with a dataset that I am displaying using the handsontable library in the form of a table. One issue I am facing is that the last column of my table appears too wide even though I did not specify any width for it. Below you can see t ...

Creating a unique custom selector with TypeScript that supports both Nodelist and Element types

I am looking to create a custom find selector instead of relying on standard javascript querySelector tags. To achieve this, I have extended the Element type with my own function called addClass(). However, I encountered an issue where querySelectorAll ret ...

What is the best way to adjust the color of a button element?

I've been troubleshooting the mouseover function of my JavaScript button object. The goal is to have a function call (specifically show()) within the object that detects the mouseover event and changes the button's color to grey. I suspect that t ...

The issue of WordPress failing to correctly resolve logout URLs within menu items

UPDATE: Adding the code below: alert(LogoutURL); reveals that the URL is not correctly passed to the JS variable. It appears to be encoded when transferred to the JS var. I confirmed this by executing the following in my PHP: <?php echo wp_logout_ur ...

Using logic to eliminate elements

During the concluding ceremony, I have a plan in mind: In case there is only one input field: Do nothing If multiple input fields exist and none of them are empty: Do nothing For multiple input fields where at least one field has content: Remove all ...

I'm curious if there's a method to ensure that the content within a mat-card stays responsive

So I'm working with this mat-card: <mat-card> <mat-card-content> <div *ngFor="let siteSource of siteSources | paginate: { itemsPerPage: 5, currentPage: page};"> <site-details [site]='siteSource'></s ...

The function is trying to access a property that has not been defined, resulting in

Here is a sample code that illustrates the concept I'm working on. Click here to run this code. An error occurred: "Cannot read property 'myValue' of undefined" class Foo { myValue = 'test123'; boo: Boo; constructor(b ...

Ways to close jQuery Tools Overlay with a click, regardless of its location

I have integrated the Overlay effect from jQuery Tools to my website, with the "Minimum Setup" option. However, I noticed that in order to close it, the user has to specifically target a small circle in the upper right corner which can affect usability. It ...