0

I'm stucked in a (in my opinion) complex reduce method.

Given is an array of objects.

const data = 
[
  {
    "key" : "test1",
    "value" : 32,
    "type"  : "OUT"
  },
  {
    "key" : "test1",
    "value" : 16,
    "type"  : "OUT"
  },
  {
    "key" : "test1",
    "value" : 8,
    "type"  : "IN"
  },
  {
    "key" : "test2",
    "value" : 32,
    "type"  : "OUT"
  },
  {
    "key" : "test2",
    "value" : 16,
    "type"  : "IN"
  },
  {
    "key" : "test2",
    "value" : 8,
    "type"  : "OUT"
  },  
];

I want to get the sum of values of each object grouped by key attribute. There are two type attributes (IN, OUT) where OUT should be interpreted as negative value.

So in the example above, I'm expecting following result object:

 //-32 - 16 + 8 = -40
 {
    "key" : "test1",
    "value" : -40,
    "type"  : "-"
  },
 //-32 + 16 - 8 = -24
 {
    "key" : "test2",
    "value" : -24,
    "type"  : "-"
  },

I'm grouping the data using the groupBy function of this SO answer.

Now I'm trying to get the sum using reduce with a filter, like in this SO answer.

However, it delivers me the wrong sums (16 and 8) + since I use filter - only one type is considered.


Here is my code:

const data = 
[
  {
    "key" : "test1",
    "value" : 32,
    "type"  : "OUT"
  },
  {
    "key" : "test1",
    "value" : 16,
    "type"  : "OUT"
  },
  {
    "key" : "test1",
    "value" : 8,
    "type"  : "IN"
  },
  {
    "key" : "test2",
    "value" : 32,
    "type"  : "OUT"
  },
  {
    "key" : "test2",
    "value" : 16,
    "type"  : "IN"
  },
  {
    "key" : "test2",
    "value" : 8,
    "type"  : "OUT"
  },  
];

//group by key
const groupBy = function(xs, key) {
  return xs.reduce(function(rv, x) {
    (rv[x[key]] = rv[x[key]] || []).push(x);
    return rv;
  }, {});
};

const grouped = groupBy(data,"key");

for (const [key, value] of Object.entries(grouped))
{
    let x = value.filter(({type}) => type === 'OUT')
    .reduce((sum, record) => sum + record.value)
  console.log(x);
}
//const filtered = grouped.filter(({type}) => type === 'OUT');




console.log(Object.values(grouped));

Question 1: Why does the reduce give me the wrong sum for type OUT?

Question 2: Is there a way to consider both types (IN, OUT) without doing the same procedure again?

3limin4t0r
  • 19,353
  • 2
  • 31
  • 52
toffler
  • 1,231
  • 10
  • 27

3 Answers3

2

You can combine the grouping + counting in 1 reduce() if you set the default value to 0, you can always add (or remove) the value from the current key (type)

const data = [{"key" : "test1", "value" : 32, "type"  : "OUT"}, {"key" : "test1", "value" : 16, "type"  : "OUT"}, {"key" : "test1", "value" : 8, "type"  : "IN"}, {"key" : "test2", "value" : 32, "type"  : "OUT"}, {"key" : "test2", "value" : 16, "type"  : "IN"}, {"key" : "test2", "value" : 8, "type"  : "OUT"}, ];

const res = data.reduce((p, c) => {
  (p[c['key']] = p[c['key']] || { ...c, value: 0 });
   
  p[c['key']].value = 
      (c.type === 'IN') 
          ? (p[c['key']].value + c.value)
          : (p[c['key']].value - c.value);
  
  return p;
},{});

console.log(res)

Output:

{
  "test1": {
    "key": "test1",
    "value": -40,
    "type": "OUT"
  },
  "test2": {
    "key": "test2",
    "value": -24,
    "type": "OUT"
  }
}
0stone0
  • 34,288
  • 4
  • 39
  • 64
  • 1
    wow - u saved me a lot of work in a few lines of code! Thank you very much. – toffler Jan 30 '23 at 14:50
  • some of the values have a lot of decimals.... is there a possibility to round the final result to 2 decimals without looping over the result again? (`toFixed(2)`) – toffler Feb 01 '23 at 09:01
  • I'd just [loop over the result](https://stackoverflow.com/questions/14810506/map-function-for-objects-instead-of-arrays) and then apply `toFixed`. – 0stone0 Feb 01 '23 at 09:25
2

I would break this into two problems:

  1. How to reduce each data value (reduce)
  2. How to evaluate existing/new values (switch)

This way your code is less-coupled and it affords you with greater extensibility. Adding a new operator is as simple as adding a new case in the switch.

const reduceValue = (type, existingValue, newValue) => {
  switch (type) {
    case 'IN'  : return existingValue + newValue;
    case 'OUT' : return existingValue - newValue;
    default    : return existingValue; // or throw new Error(`Unsupported type: ${type}`)
  }
};

const processValues = (data) =>
  data.reduce((acc, { key, type, value }) => {
    acc[key] ??= { key, type: '-', value: 0 };
    acc[key].value = reduceValue(type, acc[key].value, value);
    return acc;
  },{});

const testData = [
  { "key" : "test1", "value" : 32, "type"  : "OUT" },
  { "key" : "test1", "value" : 16, "type"  : "OUT" },
  { "key" : "test1", "value" :  8, "type"  : "IN"  },
  { "key" : "test2", "value" : 32, "type"  : "OUT" },
  { "key" : "test2", "value" : 16, "type"  : "IN"  },
  { "key" : "test2", "value" :  8, "type"  : "OUT" }
];

console.log(processValues(testData))
.as-console-wrapper { top: 0; max-height: 100% !important; }
Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132
  • indeed - this answer is (for me) easier to understand that the above one (and may be more flexible). Both works perfectly fine. +1 – toffler Jan 30 '23 at 14:57
1

I would create 2 functions for applying the sign and store them in a variable.

const applySign = { "IN": nr => +nr, "OUT": nr => -nr };

Then do a simple for...of loop (with object destructuring). If there is no running total at the moment for the current key, set the initial value to 0 (using nullish coalescing assignment ??=). Finally add the current value with applied sign to the running total.

const sums = {};
for (const { key, value, type } of data) {
  sums[key] ??= 0;
  sums[key] += applySign[type](value);
}

const data = [
  { key: "test1", value: 32, type: "OUT" },
  { key: "test1", value: 16, type: "OUT" },
  { key: "test1", value:  8, type: "IN"  },
  { key: "test2", value: 32, type: "OUT" },
  { key: "test2", value: 16, type: "IN"  },
  { key: "test2", value:  8, type: "OUT" },
];

const applySign = { "IN": nr => +nr, "OUT": nr => -nr };

const sums = {};
for (const { key, value, type } of data) {
  sums[key] ??= 0;
  sums[key] += applySign[type](value);
}

console.log(sums);

With a few simple tweaks you can change the above in the output you're looking for:

const sums = {};
for (const { key, value, type } of data) {
  sums[key] ??= { key, value: 0 };
  sums[key].value += applySign[type](value);
}

const expected = Object.values(sums);

This gives you the base answer, though the type properties that you expect are currently missing. To add them you'll have to do another loop and check the final sum result.

for (const sum of expected) {
  sum.type = sum.value < 0 ? "-" : "+";
}

const data = [
  { key: "test1", value: 32, type: "OUT" },
  { key: "test1", value: 16, type: "OUT" },
  { key: "test1", value:  8, type: "IN"  },
  { key: "test2", value: 32, type: "OUT" },
  { key: "test2", value: 16, type: "IN"  },
  { key: "test2", value:  8, type: "OUT" },
];

const applySign = { "IN": nr => +nr, "OUT": nr => -nr };

const sums = {};
for (const { key, value, type } of data) {
  sums[key] ??= { key, value: 0 };
  sums[key].value += applySign[type](value);
}

const expected = Object.values(sums);
console.log(expected);

// add type based on the value sign (don't know why)
for (const sum of expected) {
  sum.type = sum.value < 0 ? "-" : "+";
}
console.log(expected);

If type is a static "-" and was not supposed to depend on the sign of value, then you can add it when you initially create the sum object.

sums[key] ??= { key, value: 0, type: "-" };
3limin4t0r
  • 19,353
  • 2
  • 31
  • 52