davidpauljunior
davidpauljunior

Reputation: 8338

Reducing an Array of unknown length to a nested Object

I'm trying to take an array and create a nested object from it, where each item in the array, is a property of the previous item.

I think reduce is the way to do this, but I find reduce hard to grasp, and everything I try I get stuck knowing how to push into the next level. JS: Reduce array to nested objects is a similar question, but I still can't work it having tried many variations of that.

const myArray = ['one', 'two', 'three'];

// Intended Output (note, the staticCount is always 1)
{
    one: {
        staticCount: 1,

        two: {
            staticCount: 1,

            three: {
                staticCount: 1
            }
        }
    }
}

Upvotes: 1

Views: 1324

Answers (6)

tex
tex

Reputation: 2766

Some jobs call for Array.prototype.reduceRight:

const myArray = ['one', 'two', 'three']

const nestNode = (acc, key) => {
  acc.staticCount = 1
  return { [key]: acc }
}
console.log(myArray.reduceRight(nestNode, {}))

Let's take a look at reduceRight (and, by extension, reduce):

I moved the iterator function definition out of the call to reduceRight to make the example easier to talk about (see nestNode).

reduce and reduceRight are similar:

  1. Each takes two arguments, an iterator function and an initial value for that function's accumulator. The second argument is optional, but I will ignore that here.

  2. Each iterates over all of the items in the array on which they're called, calling the iterator function for each item in the array with four arguments, the accumulator, the current item in the array, the current iteration count and the whole array on which you called reduce. The last two arguments are not relevant here (and I rarely use them).

  3. The first time the iterator function is called, it will be passed the second argument you provided to reduce or reduceRight (the initial accumulator value). Afterwards, it will be passed whatever was returned by the iterator function in the previous step.

Because I think reduce (and by extension reduceRight) are powerful abstractions that are worth understanding, I'll step through the first two steps in the code example:

  1. On the first step in the iteration, our iterator function is called like this: nestNode(acc = {}, key = 'three'). Inside nestNode, we add a staticCount property to acc and set it to 1, giving us acc = { staticCount: 1 }. Then we create and return a new object with a property named 'three' that has a value equal to acc. The value returned by nestNode in the first step is { three: { staticCount: 1 } }, and nestNode will be called with this value in the second step.

  2. On the second step in the iteration, our iterator function is called like this: nestNode(acc = { three: { staticCount: 1 } }, key = 'two'). Again, we add a staticCount property to acc and set it to 1, giving us acc = { three: { staticCount: 1 }, staticCount: 1 }. We then create and return a new object with a property named 'two' that has a value equal to acc. The value we return is { two: { three: { staticCount: 1 }, staticCount: 1 } }. Again, this value will be used in the next step.

I'll skip the last step, since I hope that taking a look at the first two steps, in detail, is enough to clear things up a little. If you have other questions or still find something unclear or confusing, please let me know.

reduce (and reduceRight) are powerful, flexible tools that are worth learning and becoming comfortable with.

As a coda, I'll leave you with the return value of the iterator function after each step:

  1. { three: { staticCount: 1 } }

  2. { two: { three: { staticCount: 1 } }, staticCount: 1 }

  3. { one: { two: { three: { staticCount: 1 } }, staticCount: 1 }, staticCount: 1 }

Upvotes: 5

AnonymousSB
AnonymousSB

Reputation: 3604

reduce, like map, will loop over each item in an array and return a result. The key difference is that map will return an array of equal size to the original with any modifications you made. reduce takes what’s called an accumulator and returns that as the final result.

reduce() takes two parameters:

  1. a function()
  2. a starting value for accumulator

The function() you provide is given three values:

  1. value of accumulator
  2. value of current item in array
  3. value of current iteration (not used in this example)
  4. value of original array (not used in this example)

The most important thing to understand about the accumulator is that it will become the value of whatever your function() returns, and your function ALWAYS has to return something, otherwise accumulator will be undefined on the next loop.

Following the solution to your problem is a basic example using reduce.

Solution

const myArray = ['one','two','three'];
const result = {};

myArray.reduce((accumulator, num) => {
  accumulator[num] = { staticCount: 1}
  return accumulator[num];
}, result);

console.log(result);

Performance

The reduce solution provided here can perform 7.6 million operations per second, vs. reduceRight at 2.2 million operations per second.

https://jsperf.com/reduceright-vs-reduce/

Basic Example

var numbers = [1, 2, 3, 4, 5];

// Reduce will assign sum whatever
// the value of result is on the last loop

var sum = numbers.reduce((result, number) => {
  return result + number;
}, 0); // start result at 0

console.log(sum);

Another example

var numbers = [1, 2, 3, 4, 5];

// Here we're using the iterator, and
// assinging "too much" to sum if there
// are more than 4 numbers.

var sum = numbers.reduce((result, number, i) => {
  if (i >= 4) return "too much";
  return result + number;
}, 0);

console.log(sum);

Documentation

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce

Upvotes: 2

customcommander
customcommander

Reputation: 18921

This can also be achieved with a recursive function:

const createObj = (keys) => keys.length > 0 && ({
  [keys[0]]: {
    staticCount: 1,
    ...createObj(keys.slice(1))
  }
});

console.log(createObj(['one', 'two', 'three']));

Upvotes: 1

Danyal Aytekin
Danyal Aytekin

Reputation: 4215

With thanks to Tex for making me replace reverse().reduce() with reduceRight:

['one', 'two', 'three'].reduceRight((a, c) => ({[c]: { staticCount: 1, ...a }}), {});

Upvotes: 2

Icepickle
Icepickle

Reputation: 12806

The trick would be not to start from an empty object, but from some declared variable. Then just pass the newly child down as the aggregate for the next recursion.

The reference gets updated, and you can then print the root again.

const myArray = ['one', 'two', 'three'];
const root = {};
myArray.reduce( (agg, c) => {
  agg[c] = { staticCount: 1 };
  return agg[c];
}, root );

console.log( root );

Upvotes: 1

Ariel
Ariel

Reputation: 1436

Just use reduce. It works because objects are passed by reference.

const myArray = ['one', 'two', 'three'];
const newObject = {};

myArray.reduce((acummulator, element) => {
  acummulator[element] = {
    staticCount: 1
  };
  return acummulator[element];
}, newObject);

// Intended Output (note, the staticCount is always 1)

console.log(newObject);

Read about reduce here.

Upvotes: 1

Related Questions