如何减少/转换对象数组以得到每个属性的总和(理想情况下为lodash)

时间:2017-02-14 21:14:12

标签: functional-programming lodash reduce

我有一组营养对象看起来像这样:

[
  {
    calories: {total: 0, fat: 0},
    vitamins: {a: {total: 0, retinol: 0}, b6: 0, c: 0},
    fats: {total: 0},
    minerals: {calcium: 0}
  },
  {
    calories: {total: 150, fat: 40},
    vitamins: {a: {total: 100}, b6: 30, c: 200},
    fats: {total: 3}
  },
  {
    calories: {total: 100, fat: 60},
    vitamins: {a: {total: 120, retinol: 10}, b6: 0, c: 200},
    minerals: {calcium: 20}
  }
]

请注意,我添加了一个0填充对象,以便没有属性为null或未定义;并非所有属性都存在于后续对象中;并且这是一个非常深的对象(即obj.vitamins.a.total)

结果应该是

{
  calories: {total: 250, fat: 100},
  vitamins: {a: {total: 220, retinol: 10}, b6: 30, c: 400},
  fats: {total: 3},
  minerals: {calcium: 20}
}

我知道lodash的{​​{1}}和_.reduce()函数,但我不确定如何访问_.transform()上的属性。 如何实现理想的结果,最好使用函数式编程和lodash。

4 个答案:

答案 0 :(得分:3)

您可以使用_.mergeWith()执行此操作。我使用_.spread()创建了一个可以处理数组而不是单个参数的_.mergeWith()



var data = [{"calories":{"total":0,"fat":0},"vitamins":{"a":{"total":0,"retinol":0},"b6":0,"c":0},"fats":{"total":0},"minerals":{"calcium":0}},{"calories":{"total":150,"fat":40},"vitamins":{"a":{"total":100},"b6":30,"c":200},"fats":{"total":3}},{"calories":{"total":100,"fat":60},"vitamins":{"a":{"total":120,"retinol":10},"b6":0,"c":200},"minerals":{"calcium":20}}];

var mergeWith = _.spread(_.mergeWith);

function deepMerge(objs) {
  var args = [{}].concat(objs, function(objValue, srcValue) {
      if(_.isNumber(objValue)) {
        return objValue + srcValue;
      }
    });
  
  return mergeWith(args);
}

var result = deepMerge(data);

console.log(result);

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
&#13;
&#13;
&#13;

使用扩展语法的ES6版本:

&#13;
&#13;
var data = [{"calories":{"total":0,"fat":0},"vitamins":{"a":{"total":0,"retinol":0},"b6":0,"c":0},"fats":{"total":0},"minerals":{"calcium":0}},{"calories":{"total":150,"fat":40},"vitamins":{"a":{"total":100},"b6":30,"c":200},"fats":{"total":3}},{"calories":{"total":100,"fat":60},"vitamins":{"a":{"total":120,"retinol":10},"b6":0,"c":200},"minerals":{"calcium":20}}];

function deepMerge(objs) {
  return _.mergeWith({}, ...objs, (objValue, srcValue) => {
    if (_.isNumber(objValue)) {
      return objValue + srcValue;
    }
  });
}

var result = deepMerge(data);

console.log(result);
&#13;
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
&#13;
&#13;
&#13;

根据@naomik's suggestion,我们还可以将自定义程序提取为外部方法,以便您可以决定非对象值的合并程度。

&#13;
&#13;
var data = [{"calories":{"total":0,"fat":0},"vitamins":{"a":{"total":0,"retinol":0},"b6":0,"c":0},"fats":{"total":0},"minerals":{"calcium":0}},{"calories":{"total":150,"fat":40},"vitamins":{"a":{"total":100},"b6":30,"c":200},"fats":{"total":3}},{"calories":{"total":100,"fat":60},"vitamins":{"a":{"total":120,"retinol":10},"b6":0,"c":200},"minerals":{"calcium":20}}];

var mergeWith = _.spread(_.mergeWith);

function customizer(objValue, srcValue) {
  if(_.isNumber(objValue)) {
    return objValue + srcValue;
  }
}

function deepMerge(objs, customizer) {
  var args = [{}].concat(objs, function(objValue, srcValue) {
      return customizer(objValue, srcValue);
    });
  
  return mergeWith(args);
}

var result = deepMerge(data, customizer);

console.log(result);
&#13;
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
&#13;
&#13;
&#13;

答案 1 :(得分:2)

我可以给你一个从Monoids获得灵感的答案。如果没有太过密集的类别理论,你需要两件事来让Monoid工作

  • 表示Monoid类型的(或中性)的某个值
  • 和一些将你的类型的两个Monoid结合在一起的功能

Sum Monoid

对于初学者,让我们来看看Sum Monoid。有许多方法可以在JavaScript中实现这一点,但我们只是看看这个,这样你就可以得到一个粗略的想法

const Sum = x => ({
  constructor: Sum,
  value: x,
  concat: ({value:y}) => Sum(x + y),
  fold: f => f (x)
})

Sum.empty = Sum(0)

Sum(3).concat(Sum(4)).fold(x => console.log(x))                // 7
Sum(3).concat(Sum(4)).concat(Sum(9)).fold(x => console.log(x)) // 16

下一级monoid

使用Sum monoid,我们可以很容易地组合数字,但如果我们的数据更复杂怎么办?例如,如果我们的数据是一个对象怎么办?

想象一下,我们有xy并希望明智地将它们结合起来

let x = SumObject({ a: Sum(1), b: Sum(2) })
let y = SumObject({ b: Sum(3), c: Sum(4) })

x.concat(y)
// => SumObject({ a: Sum(1), b: Sum(5), c: Sum(4) })

那肯定会很棒!让我们现在实现SumObject幺半群 - 对不起,我真的想不出更好的名字!

const SumObject = x => ({
  constructor: SumObject,
  value: x,
  concat: ({value:y}) => {
    return SumObject(Object.keys(y).reduce((acc, k) => {
      if (acc[k] === undefined)
        return Object.assign(acc, { [k]: y[k] })
      else
        return Object.assign(acc, { [k]: acc[k].concat(y[k]) })
    }, Object.assign({}, x)))
  },
  fold: f => f(x)
})

SumObject.empty = SumObject({})

圣洁的烟!那个更难一点,但我希望你能看到它。请注意粗体代码。我们对SumObject的实现假设属性也是monoid - 意味着值将采用.concat方法。

让我们成型!

为了让monoids与您的数据一起使用,我们需要将您原始的ObjectNumber类型转换为相应的Monoid类型。

最后,为了在原始类型中获得答案,我们必须将Monoid类型返回转换为原始类型。

如果一切顺利,结果表达式将如下所示

sumToPrimitive(data.map(x =>
  primitiveToSum(x)).reduce((acc, x) =>
    acc.concat(x), SumObject.empty))

让我们创建将原语转换为Monoid类型的函数

const primitiveToSum = x => {
  switch (x.constructor) {
    case Object:
      return SumObject(Object.keys(x).reduce((acc, k) =>
        Object.assign(acc, { [k]: primitiveToSum(x[k]) }), {}))
    case Number:
      return Sum(x)
    default:
      throw Error(`unsupported type: ${x}`)
  }
}

然后我们将把Monoid转换成原始的

const sumToPrimitive = x => {
  switch (x.constructor) {
    case SumObject:
      return x.fold(x =>
        Object.keys(x).reduce((acc, k) =>
          Object.assign(acc, { [k]: sumToPrimitive(x[k]) }), {}))
    case Sum:
      return x.fold(identity)
    default:
      throw Error(`unsupported type: ${x}`)
  }
}

“那就是它?”

好吧,因为你支持嵌套的对象,它有点乱,但这真的是我们能做的最好的,我很害怕。如果你愿意将数据限制为扁平物体,事情会更加清晰。

尽管如此,让我们看看它的全部工作

const SumObject = x => ({
  constructor: SumObject,
  value: x,
  concat: ({value:y}) => {
    return SumObject(Object.keys(y).reduce((acc, k) => {
      if (acc[k] === undefined)
        return Object.assign(acc, { [k]: y[k] })
      else
        return Object.assign(acc, { [k]: acc[k].concat(y[k]) })
    }, Object.assign({}, x)))
  },
  fold: f => f(x)
})

SumObject.empty = SumObject({})

const Sum = x => ({
  constructor: Sum,
  value: x,
  concat: ({value:y}) => Sum(x + y),
  fold: f => f (x)
})

Sum.empty = Sum(0)

const primitiveToSum = x => {
  switch (x.constructor) {
    case Object:
      return SumObject(Object.keys(x).reduce((acc, k) =>
        Object.assign(acc, { [k]: primitiveToSum(x[k]) }), {}))
    case Number:
      return Sum(x)
    default:
      throw Error(`unsupported type: ${x}`)
  }
}

const sumToPrimitive = x => {
  switch (x.constructor) {
    case SumObject:
      return x.fold(x =>
        Object.keys(x).reduce((acc, k) =>
          Object.assign(acc, { [k]: sumToPrimitive(x[k]) }), {}))
    case Sum:
      return x.fold(identity)
    default:
      throw Error(`unsupported type: ${x}`)
  }
}

const identity = x => x

const data = [
  {
    calories: {total: 0, fat: 0},
    vitamins: {a: {total: 0, retinol: 0}, b6: 0, c: 0},
    fats: {total: 0},
    minerals: {calcium: 0}
  },
  {
    calories: {total: 150, fat: 40},
    vitamins: {a: {total: 100}, b6: 30, c: 200},
    fats: {total: 3}
  },
  {
    calories: {total: 100, fat: 60},
    vitamins: {a: {total: 120, retinol: 10}, b6: 0, c: 200},
    minerals: {calcium: 20}
  }
]

const result = sumToPrimitive(data.map(x =>
  primitiveToSum(x)).reduce((acc, x) =>
    acc.concat(x), SumObject.empty))

console.log(result)

<强>输出

{ calories: { total: 250, fat: 100 },
  vitamins: { a: { total: 220, retinol: 10 }, b6: 30, c: 400 },
  fats: { total: 3 },
  minerals: { calcium: 20 } }

<强>说明

Monoids是一个很好的界面,用于维护泛型,可重用的方式来组合术语 - 这就是函数式编程的全部内容。但是,由于您的数据嵌套复杂(以及本机JavaScript缺少自定义类型),因此需要更多手动编码。

我无法想象Lodash解决方案会是什么样子 - 但我可以告诉你的是,它会将泛型和可重用性抛到窗外 - 特别是考虑到数据的结构。

答案 2 :(得分:2)

尽管Ori Drori's answer已经指定了使用mergeWith的方向,但他的用法在于创建递归调用mergeWith的递归函数。此解决方案忽略了mergeWith本质上是递归合并的事实,其中定制器仅在返回任何不是undefined的值时才转换结果值。另外,另一个问题是mergeWith会改变第一个对象参数,这在你想要保留源对象状态的情况下可能并不理想。

下面的解决方案使用Function.prototype.applymergeWith而不是将其包裹在spread中,您可以根据自己的喜好决定将其切换为spread。为了解决源对象变异的问题,我们提供一个空对象作为第一个参数,然后将其与其余的源对象连接起来。最后,我们连接定制器函数,如果value是数字,则返回参数的总和。

var result = _.mergeWith.apply(
  null, 
  [{}].concat(source).concat(function(value, src) {
    if(_.isNumber(value)) {
      return value + src;
    }
  })
);

var source = [
  {
    calories: {total: 0, fat: 0},
    vitamins: {a: {total: 0, retinol: 0}, b6: 0, c: 0},
    fats: {total: 0},
    minerals: {calcium: 0}
  },
  {
    calories: {total: 150, fat: 40},
    vitamins: {a: {total: 100}, b6: 30, c: 200},
    fats: {total: 3}
  },
  {
    calories: {total: 100, fat: 60},
    vitamins: {a: {total: 120, retinol: 10}, b6: 0, c: 200},
    minerals: {calcium: 20}
  }
];

var result = _.mergeWith.apply(
  null, 
  [{}].concat(source).concat(function(value, src) {
    if(_.isNumber(value)) {
      return value + src;
    }
  })
);

console.log(result);
body>div {
  min-height: 100%;
  top: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.js"></script>

答案 3 :(得分:0)

我更喜欢以无点的方式与Ramda一起做这件事,但OP在Lodash要求,所以我们在这里,简单的递归解决方案:

let data = [{calories: {total: 0, fat: 0},vitamins: {a: {total: 0, retinol: 0}, b6: 0, c: 0},fats: {total: 0},minerals: {calcium: 0}},{calories: {total: 150, fat: 40},vitamins: {a: {total: 100}, b6: 30, c: 200},fats: {total: 3}},{calories: {total: 100, fat: 60},vitamins: {a: {total: 120, retinol: 10}, b6: 0, c: 200},minerals: {calcium: 20}}];

        const recursor = result => (value, key) => {
            if ( _.isNumber(value) ) {
                result[key] = ((result[key] || (result[key] = 0)) + value);
                return result[key];
            } else {
                return  _.mapValues(value, recursor(result[key] || (result[key] = {}) ));
            }
        }

        const reducer = (result, value, key) => _.mapValues(value, recursor(result));

        let final = _.transform(data, reducer, {});
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>