1

This seems embarrassing to ask, but I'm unsure where to go from here.

I have an array of nested objects and I would like to create a new object that has the average from the original array.

const _ = require(`lodash`)

const data = JSON.parse(`
[
  {
      "thingOne": {
          "numberOne": 1758,
          "numberTwo": 97
      },
      "thingTwo": {
          "numberOne": 1758,
          "numberTwo": 97
      }
  },
  {
      "thingOne": {
          "numberOne": 1968,
          "numberTwo": 95
      },
      "thingTwo": {
          "numberOne": 2010,
          "numberTwo": 95
      }
  }
]`)

const results = {}

_.each(data, (value, key) => {
  _.each(value, (value, key) => {
    if (key in results) {
      results[key] = {
        numberOne: results[key].numberOne + value.numberOne,
        numberTwo: results[key].numberTwo + value.numberTwo,
      }
    } else {
      results[key] = value
    }
  })
})

console.log(results)

I can do this to sum up the array into a new object, but am unsure what to do from here. Do I need to loop this all over again to create an average? Any help appreciated (and I'm not required to use lodash, if there's a simpler answer).

Here's what I'm expected to get in the end:

const expected = {
  thingOne: {
    numberOne: 1863,
    numberTwo: 96,
  },
  thingTwo: {
    numberOne: 1884,
    numberTwo: 96,
  },
}
Mosè Raguzzini
  • 15,399
  • 1
  • 31
  • 43
duffn
  • 3,690
  • 8
  • 33
  • 68

3 Answers3

4

Noticed that you've used lodash you can take advantage of _.mergeWith():

From lodash DOCS:

_.mergeWith(object, sources, customizer)

This method is like _.merge except that it accepts customizer which is invoked to produce the merged values of the destination and source properties. If customizer returns undefined, merging is handled by the method instead. The customizer is invoked with six arguments: (objValue, srcValue, key, object, source, stack).

In our case, our customizer method retrieves the average, instead of merging values.

const data = JSON.parse(`
[
  {
      "thingOne": {
          "numberOne": 1758,
          "numberTwo": 97
      },
      "thingTwo": {
          "numberOne": 1758,
          "numberTwo": 97
      }
  },
  {
      "thingOne": {
          "numberOne": 1968,
          "numberTwo": 95
      },
      "thingTwo": {
          "numberOne": 2010,
          "numberTwo": 95
      }
  }
]`);


const getAvg = (data) => _.mergeWith({}, ...data, (a, b) => {
  if(_.isNumber(b)) {
    return ((b || 0) / data.length) + (_.isNumber(a) ? (a || 0) : 0);
  }
});


console.log(getAvg(data));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>

Credit for the implementation of the getAvg function goes to Ori Drori, who originally posted it in answer to a related question.

Cody Gray - on strike
  • 239,200
  • 50
  • 490
  • 574
Mosè Raguzzini
  • 15,399
  • 1
  • 31
  • 43
  • Thank you. I can't say I entirely understand this, but it does seem to work. – duffn Nov 05 '19 at 15:06
  • @duffn The customizer acts as a reducer on the array of values for EACH field, after merging. – Mr. Polywhirl Nov 05 '19 at 15:12
  • @Mr.Polywhirl the docs are quite clear about it: "If customizer returns undefined, merging is handled by the method instead". Look at the internal method https://github.com/lodash/lodash/blob/master/.internal/baseMerge.js – Mosè Raguzzini Nov 05 '19 at 16:22
  • It appears that you have copied some of the code from [Ori Drori's answer to another question](https://stackoverflow.com/questions/43924249/dynamically-calculate-the-average-for-a-nested-collection-using-lodash/43925076#43925076). Please make sure to *cite your sources* when you borrow code from someone else's answer. – Cody Gray - on strike Nov 06 '19 at 02:13
  • Hi @CodyGray, I've included the credits in original post, deleted by mistake when updating with docs, thanks – Mosè Raguzzini Nov 06 '19 at 07:33
1

Here is an explanation of what is going on in Mosè Raguzzini's code.

The code is essentially performing a rolling-average of every value as it comes in.

e.g. PREV_VAL += CURR_VAL / TOTAL_VALS

See: How to calculate moving average without keeping the count and data-total?

const DEBUG = true;

var data = [{
  "thingOne": { "numberOne": 1758, "numberTwo": 97 },
  "thingTwo": { "numberOne": 1758, "numberTwo": 97 }
}, {
  "thingOne": { "numberOne": 1968, "numberTwo": 95 },
  "thingTwo": { "numberOne": 2010, "numberTwo": 95 }
}, {
  "thingOne": { "numberOne":    1, "numberTwo":  1 },
  "thingTwo": { "numberOne":    1, "numberTwo":  1 }
}];

console.log(calculateRollingAverage(data));

/**
 * Calculates a rolling-average of data.
 * 
 * PREV_VAL += CURR_VAL / TOTAL_VALS
 * 
 * @param data {object[]} - an array of data objects
 * @return Returns the rolling-average of each data objects' fields.
 */
function calculateRollingAverage(data) {
  return _.mergeWith({}, ...data, (prev, curr) => {
    if (DEBUG) {
      console.log(`prev = ${JSON.stringify(prev)} | curr = ${JSON.stringify(curr)}`);
    }
    if (_.isNumber(curr)) {
      let p = _.isNumber(prev) ? (prev || 0) : 0, c = (curr || 0) / data.length;
      if (DEBUG) {
        console.log(`avg => ${p} + (${curr} / ${data.length}) = ${p + c}`);
      }
      return p + c;
    }
  });
}
.as-console-wrapper { top: 0; max-height: 100% !important; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132
0

You run through the keys and solve like below.

const data = JSON.parse(`
[
  {
      "thingOne": {
          "numberOne": 1758,
          "numberTwo": 97
      },
      "thingTwo": {
          "numberOne": 1758,
          "numberTwo": 97
      }
  },
  {
      "thingOne": {
          "numberOne": 1968,
          "numberTwo": 95
      },
      "thingTwo": {
          "numberOne": 2010,
          "numberTwo": 95
      }
  }
]`);

let expected = {};
data.forEach(function(thing) {

    let parentKeys = Object.keys(thing);
    
    parentKeys.forEach(function(parentKey) {
    
        if (!expected.hasOwnProperty(parentKey)) {
        
            expected[parentKey] = {};
        }
        
        let expectedParent = expected[parentKey];
        let parent = thing[parentKey];
        let childKeys = Object.keys(parent);
        
        childKeys.forEach(function(childKey) {
            
            if (!expectedParent.hasOwnProperty(childKey)) {
        
                 expectedParent[childKey] = 0;
            }
        
            expectedParent[childKey] += parent[childKey] /                    parentKeys.length;
        });
    });
});

console.log(expected);
Allabakash
  • 1,969
  • 1
  • 9
  • 15