7

If I have an object like this (or similar):

sales = { 
    obs1:{
        Sales1:{
            Region:"North", Value: 200}, 
        Sales2:{
            Region:"South", Value:100}}, 
    obs2:{
        Sales1:{
            Region:"North", Value: 50}, 
        Sales2:{
            Region:"South", Value:20}
    }
}

How could I aggregate the sum of the property Value by Region? Answers could be in pure JavaScript or a library.

The end result should be something similar to this:

totals = {North: 250, South:120}
0

6 Answers 6

8

As others pointed out, there's no built-in JavaScript functions to do that (there are a few high-order functions like map, but not enough for the task). However, some libraries such as Underscore.js provide many utilities to simplify this kind of task.

var totals = _
    .chain(sales) // Wraps up the object in an "underscore object",
                  // so methods can be chained
    // First: "flatten" the sales
    .map(function(v) { 
        return _
            .chain(v)
            .map(function(v2) {
                return v2;
            })
            .value(); 
    })
    .flatten()
    // Second: group the sales by region
    .groupBy('Region')
    // Third: sum the groups and create the object with the totals
    .map(function(g, key) {
        return {
            type: key, 
            val: _(g).reduce(function(m, x) {
                return m + x.Value;
            }, 0)
        };
    })
    .value(); // Unwraps the "underscore object" back to a plain JS object

Source: this answer at SOpt

This answer assumes the structure of your data is known - contrary to the other answers, which focus on generalizing the structure. Though the code above can be generalized itself, by removing the hardcoded Region and Value and varying the nesting level to something other than two and the aggregation function to something other than sum - as long as the leaves contain both a property you want to group by, and a value you want to aggregate.

function aggregate(object, toGroup, toAggregate, fn, val0) {
    function deepFlatten(x) {
        if ( x[toGroup] !== undefined ) // Leaf
            return x;
        return _.chain(x)
                .map(function(v) { return deepFlatten(v); })
                .flatten()
                .value();
    }

    return _.chain(deepFlatten(object))
            .groupBy(toGroup)
            .map(function(g, key) {
                return {
                    type: key,
                    val: _(g).reduce(function(m, x) {
                        return fn(m, x[toAggregate]);
                    }, val0 || 0)
                };
            })
            .value();
}

It's called like this:

function add(a,b) { return a + b; }
var totals = aggregate(sales, "Region", "Value", add);

Another example (finds minimum value by region):

function min(a,b) { return a < b ? a : b; }
var mins = aggregate(sales, "Region", "Value", min, 999999);
Sign up to request clarification or add additional context in comments.

Comments

4

Are you looking for something like this (updated based on @Barmar's suggestion):

var totals = {};

function ExtractSales(obj) {
    if (obj.Region && obj.Value) {
        if (totals[obj.Region]) {
            totals[obj.Region] += obj.Value;
        } else {
            totals[obj.Region] = obj.Value;
        }
    } else {
        for (var p in obj) {
            ExtractSales(obj[p]);
        }
    }
}

ExtractSales(sales);

console.log(totals);

http://jsfiddle.net/punF8/3/

What this will do, for a given root object, is walk down it's properties and try and find something with a Region and a Value property. If it finds them, it populates an object with your totals.

With this approach, you don't need to know anything about the nesting of objects. The only thing you need to know is that the objects you are looking for have Region and Value properties.

This can be optimized further and include some error checking (hasOwnProperty, undefined, circular references, etc), but should give you a basic idea.

8 Comments

I think the OP has code that works, I think they want a generalization.
@JuanMendes: This is a generalization.
No, he isn't. It recurses until it finds an object with Region and Value properties.
@JuanMendes: I'm hardcoding the property name (I assumed the OP knows what they are looking for), but the depth is absolutely not hardcoded. It wouldn't be that hard to adapt it to take the property names as arguments.
@MattBurland Do you really need SalesArray? You could just add to the totals object when you find the leaf objects.
|
4

Here's a function that will sum and group all all the properties in an object (recursively) http://jsfiddle.net/tjX8p/2/ This is almost the same as @MattBurland, except that it's fully generalized, that is, you can use any property as what to group-by or sum.

/**
 * @param {object} obj Arbitrarily nested object that must contain the 
 * given propName and groupPropName at some level
 * @param {string} propName The property to be summed up
 * @param {string} groupPropName The property to group by
 * @param {object} This is used internally for recursion, feel free to pass 
 * in an object with existing totals, but a default one is provided
 * @return {object} An object keyed by by the groupPropName where the value 
 * is the sum of all properties with the propName for that group
 */
function sumUp(obj, propName, groupPropName, totals) {
    var totals = totals || {};
    for (var prop in obj) {
        if (prop === propName) {
            if (!totals[obj[groupPropName]]) {
                totals[obj[groupPropName]] = 0
            } 
            totals[obj[groupPropName]] += obj[propName]
        } else if (typeof obj[prop] == 'object'){
            sumUp(obj[prop], propName, groupPropName, totals);
        }
    }
    return totals;
}

This function will work with the data you posted, or with something like

var sales2 = { 
    a: {
        obs1:{
          Sales1:{
            Region:"North", Value: 100, OtherValue: 12}, 
          Sales2:{
              Region:"South", Value:50, OtherValue: 15}}}, 
    b: {
        obs2:{
          Sales1:{
            Region:"North", Value: 50, OtherValue: 18}, 
          Sales2:{
            Region:"South", Value:100, OtherValue: 20}}
    }
};

console.log (sumUp(sales2, 'Value', 'Region'));
// Object {North: 150, South: 150}
console.log (sumUp(sales2, 'OtherValue', 'Value'));
// Object {50: 33, 100: 32} 

I've stayed away from error checking to keep to code clear.

2 Comments

You are summing up, but not grouping.
@MattBurland Thanks, I've fixed it to add grouping. It's almost the same as yours, but lets you specify the group-by and sum-prop names, also it returns the summed up object instead of modifying a variable outside of the function.
1

Searching for querying options in JS and looking at @mgibsonbr answer, it seems that another good solution for problems like this would be using something like jFunk to query (even though jFunk is still a prototype) and Underscore to group and reduce.

totals= _.chain(jF("*[Region]", sales).get()).groupBy('Region').map(function(g, key) {
        return {
            type: key, 
            val: _(g).reduce(function(m, x) {
                return m + x.Value;
            }, 0)
        };
    })
    .value();

Comments

0

There are a lot of ways to approach this scenario -- most of which have been addressed already. So I decided to go the extra mile here and create something that would both be a viable solution for the OP's question, and vague enough in its definition to be uses with any data object.

So here's what I was able to throw together...

aggPropByAssoc() or Aggregate Property By Association is used to gather certain data from an object, based of the data's property name, by an associated property key/value identifier. Currently, this function assumes that the associated property resides in the same object level as the property being requested.

The function does not make assumptions about on which level in the object, that the requested property can be found. As such, the function will recurse through the object until the property (and the associated property) have been found.


Syntax

aggPropByAssoc (obj, ops [, cb])

  • the object to parse
  • an object containing...
    • assocKey : the key name of the associated property
    • assocVal : a string or array of assocKey value
    • property : the property to aggregate
  • a callback function [optional]

Examples

Using the OP's example:

// I've removed the object definition for brevity.
var sales = { ... };

aggPropByAssoc( /* obj, ops, cb */
    sales,
    {
        assocKey: 'Region',
        assocVal: ['North', 'South'],
        property: 'Value'
    },
    function (e) {
        // As the function only returns the data found, and does not
        // automatically manipulate it, we use the callback function
        // to return the sum of each region, as requested. 
        return {
            North: e.North.reduce(function (p, c) { return p + c}, 0),
            South: e.South.reduce(function (p, c) { return p + c}, 0)
        }
    }
)

// var regionSums = aggPropByAssoc( ... );
// returns --> Object {North: 250, South: 120}

Source

function aggPropByAssoc(obj, ops, cb) {
    if (typeof obj !== "object" || typeof ops !== "object") { return; }
    if (!(ops.assocKey && ops.assocVal && ops.property)) { return; }

    function recurseObj(robj) {
        for (var k in robj) {
            if (! (robj[k][ops.assocKey] && robj[k][ops.property])) { recurseObj(robj[k]); continue; }
            if (robj[k][ops.assocKey] === ops.assocVal) { aggArr.push(robj[k][ops.property]); }
        }
    }

    var assocVObj = ops.assocVal, aggArr = [], aggObj = {};
    if (typeof ops.assocVal !== "object" ) { 
        recurseObj(obj), aggObj = aggArr;
    } else {
        for (var op in assocVObj) { 
            ops.assocVal = assocVObj[op];
            recurseObj(obj);
            aggObj[ops.assocVal] = aggArr, aggArr = [];
        }
    }

    if (typeof cb === "function") { return cb(aggObj); }
    return aggObj;
}

Comments

0

This problem can be solved by an aggregation. But in order to use aggregation we need to convert it from an object to an array first. This can be achieved by using Object.values(obj). Because the source object has two levels of nesting, we need to apply it twice and flatten the result:

intermediate = Object.values(sales)
  .map(x => Object.values(x))
  .flat()

This gives us

[
  {
    "Region": "North",
    "Value": 200
  },
  {
    "Region": "South",
    "Value": 100
  },
  {
    "Region": "North",
    "Value": 50
  },
  {
    "Region": "South",
    "Value": 20
  }
]

And now we can use aggregation

totals = intermediate.reduce((r,v) => {
  r[v.Region] = (r[v.Region] || 0) + v.Value; 
  return r;
}, {});

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.