Group by and sum with multiple properties using Javascript

Viewed 442

I'm trying to solve this problem in many ways, but unable to move further.

I've a data something like this:

const dummyData = [{
    metric: {
        id: "A",
        code: "00",
        advice: "123"
    },
    values: [[123, 1], [332, 2]]
}, {
    metric: {
        id: "A",
        code: "01",
        advice: "123"
    },
    values: [[123, 5], [332, 3]]
}, {
    metric: {
        id: "B",
        code: "01",
        advice: "123"
    },
    values: [[123, 3]]
}]

I wanted this in 3 different formats with various group by:

  • Group by id, and sum the values[1] For example:

    [{
        id: 'A'
        values: [{
            valueId: 123,
            value: 6    // 1 + 5
        }, {
            valueId: 332,
            value: 5    // 2 + 3
        }]
    }, {
        id: 'B'
        values: [{
            valueId: 123,
            value: 3
        }]
    }]
    
  • Group by id and code, then sum the values[1]

    [{
        id: 'A'
        code: '00'
        values: [{
            valueId: 123,
            value: 1
        }, {
            valueId: 332,
            value: 2
        }]
    }, {
        id: 'A'
        code: '01'
        values: [{
            valueId: 123,
            value: 5
        }, {
            valueId: 332,
            value: 3
        }]
    }, {
        id: 'B'
        code: '01'
        values: [{
            valueId: 123,
            value: 3
        }]
    }]
    
  • Group by id and advice, then sum the values[1]

    [{
        id: 'A'
        advice: '123'
        values: [{
            valueId: 123,
            value: 6    // 1 + 5
        }, {
            valueId: 332,
            value: 5    // 2 + 3
        }]
    }, {
        id: 'B'
        advice: '123'
        values: [{
            valueId: 123,
            value: 3
        }]
    }]
    

So far I've managed to do some simple group by with a single key like this:

const a = dummyData.reduce((accumulator, currentValue) => {
    (accumulator[currentValue.metric["id"]] = accumulator[currentValue.metric["id"]] || []).push(currentValue.values)
    return accumulator
}, {})

Though it is not in my expected format. I know that I've to use reduce to achieve this, however I'm unable to make it work or I don't know how to do.

2 Answers

You could group with an object as has table and fin the nested values for grouping.

This approach uses a double grouping, one for the outer groups with the wanted properties and another for the valueId/value.

The outer grouping takes a joint key with the values for grouping, separated by a |. I generates a new object with the given keys and an empty array for the values.

The inner grouping takes place with the valueId and a seach for an object with this values. If not found, a new object is added to the values array.

Finally the value is added to the specific group.

function groupBy(data, keys) {
    return Object.values(data.reduce((r, { metric, values }) => {
        const key = keys.map(k => metric[k]).join('|');
        r[key] = r[key] || { ...Object.fromEntries(keys.map(k => [k, metric[k]])), values: [] };
        values.forEach(([valueId, value]) => {
            let temp = r[key].values.find(q => q.valueId === valueId);
            if (!temp) r[key].values.push(temp = { valueId, value: 0 });
            temp.value += value;
        });
        return r;
    }, {}));
}

const
    dummyData = [{ metric: { id: "A", code: "00", advice: "123" }, values: [[123, 1], [332, 2]] }, { metric: { id: "A", code: "01", advice: "123" }, values: [[123, 5], [332, 3]] }, { metric: { id: "B", code: "01", advice: "123" }, values: [[123, 3]] }];

console.log(groupBy(dummyData, ['id']));
console.log(groupBy(dummyData, ['id', 'code']));
console.log(groupBy(dummyData, ['id', 'advice']));
.as-console-wrapper { max-height: 100% !important; top: 0; }

Added explanation in comments.

const dummyData = [{ metric: { id: "A", code: "00", advice: "123" }, values: [[123, 1], [332, 2]] }, { metric: { id: "A", code: "01", advice: "123" }, values: [[123, 5], [332, 3]] }, { metric: { id: "B", code: "01", advice: "123" }, values: [[123, 3]] }];

// Param : array = array to be process
// spread list of props = pass properties on which you want to group.
function groupBy(array, ...props) {
  return array.reduce((a, c) => {
    // Find matching values exist for given properties.
    let obj = a.filter(x => props.every(y => x[y] == c.metric[y]))[0];
    // If not then create new object and push into result array.
    if (!obj) {
      obj = {};
      // Assign values for each properties.
      props.forEach(x => obj[x] = c.metric[x]);
      obj.values = [];
      a.push(obj);
    }

    // Loop through each values and find matching valueId. If exist add value else push value object.
    c.values.forEach(val => {
      let v = obj.values.filter(x => x.valueId == val[0])[0];
      if (!v) {
        obj.values.push({ valueId: val[0], value: val[1] });
      } else {
        v.value += val[1];
      }
    });
    
    return a;
  }, []);
}

console.log(groupBy(dummyData, 'id'));
console.log(groupBy(dummyData, 'id', 'code'));
console.log(groupBy(dummyData, 'id', 'advice'));
.as-console-wrapper { max-height: 100% !important; top: 0; }

Related