Kauyumari
Kauyumari

Reputation: 31

Recursive function that flattens an object within an array of objects

So I have an array of objects that I receive like this:

[
  {
    accountName: {
      type: 'link',
      value: {
        value: '1234567890123456789',
        to: '/q?Id=1237896540789654',
      },
    },
    bank: {
      type: 'text',
      value: 'Foo Bank',
    },
  },
  {
    accountName: {
      type: 'link',
      value: {
        value: '9234567890123456789',
        to: '/q?Id=9234567890123456789',
      },
    },
    bank: {
      type: 'text',
      value: 'Foo Bank',
    },
  },
];

And I need an output like this:

[
  {
    accountName: '1234567890123456789', 
    bank: 'Foo Bank'
  },
  {
    accountName: '9234567890123456789', 
    bank: 'Foo Bank'
  }
]

How could I resolve it? The array of objects doesn't always have the same shape so I need a recursive function that holds the first key from the original object and flatten the value until it is no more an object.

Upvotes: 2

Views: 92

Answers (4)

Scott Sauyet
Scott Sauyet

Reputation: 50807

I would choose a version like this:

// helper functions
const mapObject = (fn) => (o) => Object .fromEntries (
  Object .entries (o) .map (([k, v]) => [k, fn (v)])
)
const map = (fn) => (xs) => xs .map(x => fn (x))

// main functions
const deepestValue = (o) => 
  typeof o .value== "object" ? deepestValue (o .value) : o .value

const transform = map (mapObject (deepestValue))

// demo
const input=[{accountName: {type: "link", value: {value: "1234567890123456789", to: "/q?Id=1237896540789654"}}, bank: {type: "text", value: "Foo Bank"}}, {accountName: {type: "link", value: {value: "9234567890123456789", to: "/q?Id=9234567890123456789"}}, bank: {type: "text", value: "Foo Bank"}}]

console.log (transform (input))

We can build this in a step-by-step manner:

We start by writing a simple recursive deepestValue function like this:

const deepestValue = (o) => 
  typeof o .value== "object" ? deepestValue (o .value) : o .value

That could be used to write our transformation function this way:

const transform = (xs) => xs .map (x => Object.fromEntries (
  Object .entries (x) .map (([k, v]) => [k, deepestValue (v)])
))

(If your environment does not support the relatively new Object.fromEntries, it's quite easy to shim.)

We could stop there. But it's worth noting that this entries->map->fromEntries dance is a very reusable pattern. It's basically mapping a function over the properties of an object. Perhaps a good name is mapObject. We can simply extract that and use it like this:

const mapObject = (fn) => (o) => Object .fromEntries (
  Object .entries (o) .map (([k, v]) => [k, fn (v)])
)

const transform = (xs) => xs .map (mapObject (deepestValue))

But there is another function we might want to abstract out of this, a map function that works like Array.prototype.map but which is a stand-alone function. That is also very easy to write.

const map = (fn) => (xs) => xs .map (x => fn (x))

(I don't write simply (fn) => (xs) => xs .map (fn) for reasons described many places, including in Why does parseInt yield NaN with Array#map?.)

With this, we can now write the snippet above.

Functions such as map and mapObject can go in our personal utility libraries to be reused in various places in our application. Libraries such as Underscore or Ramda (disclaimer: I'm a Ramda author) collect such things into useful collections. Ramda's map actually covers both these cases, so in Ramda we might write const transform = map (map (deepestValue)).

Note that I would not extract these helper functions on the basis of single cases. If I'd never seen the pattern before, I would be perfectly happy with the first version of transform. But I've done similar things often enough that this abstraction makes sense to me. And I think it always helps to break things down into really simple pieces.

Upvotes: 0

Code Maniac
Code Maniac

Reputation: 37755

You can do something like this, check if the value is an object or not, if it's object go to deeper level else use the value

let data = [{accountName: {type: 'link',value: {value: '1234567890123456789',to: '/q?Id=1237896540789654',},},bank: {type: 'text',value: 'Foo Bank',},},{accountName: {type: 'link',value: {value: '9234567890123456789',to: '/q?Id=9234567890123456789',},},bank: {type: 'text',value: 'Foo Bank',},},];

let getDeepestValue = obj => {
  let value = obj.value
  while(typeof value == 'object'){
    value = value.value 
  }
  return value
}

let final = data.map(({accountName,bank})=>{
  return{
    accountName: getDeepestValue(accountName),
    bank: getDeepestValue(bank)
  }
})

console.log(final)

Upvotes: 2

IceMetalPunk
IceMetalPunk

Reputation: 5556

This code should iteratively pull out the deepest value of each key in each object:

const processed = data.reduce((result, obj) => {
  return result.concat(Object.keys(obj).reduce((entry, key) => {
      entry[key] = obj[key];
      while (entry[key].value) { entry[key] = entry[key].value; }
      return entry;
    }, {}));
  }, []);

Upvotes: 2

Codebling
Codebling

Reputation: 11382

There are a number of libraries that might help, but maybe something like this would work?

results.map(result => ({
    accountName: result.accountName.value.value,
    bank: result.bank.value
}));

Except that you said "The array of objects doesn't always have the same shape", but I'm not sure how to address that. You said something about searching recursively, does this mean looking for the deepest "value" in the object?

Upvotes: 2

Related Questions