如何聚合对象属性?

时间:2014-05-07 15:22:44

标签: javascript

如果我有这样的对象(或类似的):

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

如何通过Value汇总属性Region的总和?答案可以是纯JavaScript或库。

最终结果应与此类似:

totals = {North: 250, South:120}

6 个答案:

答案 0 :(得分:6)

正如其他人指出的那样,没有内置的JavaScript函数可以做到这一点(有一些高阶函数,如map,但不足以完成任务)。但是,某些库(如Underscore.js)提供了许多实用程序来简化此类任务。

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

来源:this answer at SOpt

此答案假设您的数据的结构已知 - 与其他答案相反,后者侧重于概括结构。虽然上面的代码可以自己推广,但是通过删除硬编码的RegionValue并将嵌套级别改为除2之外的其他东西,并将聚合函数改为除sum之外的其他东西 - 只要包含叶子要分组的属性和要聚合的值。

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();
}

它被称为:

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

另一个例子(按地区查找最小值):

function min(a,b) { return a < b ? a : b; }
var mins = aggregate(sales, "Region", "Value", min, 999999);

答案 1 :(得分:4)

这里是一个函数,它将对象中的所有属性求和并进行分组(递归)http://jsfiddle.net/tjX8p/2/这与@MattBurland几乎相同,只是它完全概括,即,你可以使用任何属性作为分组或总和。

/**
 * @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;
}

此功能适用于您发布的数据,或类似

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} 

我远离错误检查以保持代码清晰。

答案 2 :(得分:3)

您是否在寻找类似的内容(根据@ Barmar的建议更新):

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/

对于给定的根对象,这将做的是向下走它的属性并尝试找到具有RegionValue属性的内容。如果找到它们,它会用总数填充一个对象。

使用这种方法,您无需了解有关对象嵌套的任何信息。您唯一需要知道的是,您要查找的对象具有RegionValue属性。

这可以进一步优化,包括一些错误检查(hasOwnPropertyundefined,循环引用等),但应该给你一个基本的想法。

答案 3 :(得分:1)

在JS中搜索查询选项并查看@mgibsonbr答案,似乎对此类问题的另一个好解决方案是使用jFunk之类的东西进行查询(即使jFunk仍然是原型)和{{ 3}}分组和减少。

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();

答案 4 :(得分:0)

有很多方法可以解决这个问题 - 其中大部分都已经解决了。所以我决定在这里加倍努力,创造一些既可以解决OP问题的可行解决方案,也可以在其定义中模糊地用于任何数据对象。

所以,这是我能够一起扔的东西......

aggPropByAssoc()按关联聚合属性用于根据数据的property名称从对象收集某些数据,通过关联的属性key/value标识符。目前,此函数假定关联的属性与所请求的属性位于相同的对象级别。

该函数不会假设对象中的哪个级别,可以找到所请求的属性。因此,该函数将通过对象递归,直到找到属性(和相关属性)。


语法

  

aggPropByAssoc(obj,ops [,cb]

  • 要解析的对象
  • 包含... 的对象
    • assocKey关联媒体资源的主要名称
    • assocVal a 字符串关联键值
    • property要聚合的属性
  • 回调函数 [可选]

实施例

使用OP的例子:

// 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}

来源

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;
}

答案 5 :(得分:0)

可以通过聚合解决此问题。但是为了使用聚合,我们需要首先将其从对象转换为数组。这可以通过使用Object.values(obj)来实现。由于源对象具有两个嵌套级别,因此我们需要对其应用两次并展平结果:

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

这给了我们

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

现在我们可以使用聚合了

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