2

I have data as follows:

const results = [
  { make: "audi", fuel: "gasoline", model: "a1", count: 8 },
  { make: "audi", fuel: "diesel", model: "a3", count: 2 },
  { make: "audi", fuel: "gasoline", model: "a3", count: 5 }
];

And I want to map it to get the combinations of all keys with sum of count. Thus I want to get something as follows:

const mappedResults = [
  { make: "audi", fuel: undefined, model: undefined, count: 8 + 2 + 5 },
  { make: "audi", fuel: "diesel", model: undefined, count: 2 },
  { make: "audi", fuel: "gasoline", model: undefined, count: 8 + 5 },
  { make: "audi", fuel: "gasoline", model: "a1", count: 8 },
  { make: "audi", fuel: "diesel", model: "a3", count: 2 },
  { make: "audi", fuel: "gasoline", model: "a3", count: 5 },

  { make: "audi", fuel: undefined, model: "a1", count: 8 },
  { make: "audi", fuel: undefined, model: "a3", count: 2 + 5 },

  { make: undefined, fuel: undefined, model: "a1", count: 8 },
  { make: undefined, fuel: undefined, model: "a3", count: 2 + 5 },

  { make: undefined, fuel: "gasoline", model: "a1", count: 8 },
  { make: undefined, fuel: "diesel", model: "a3", count: 2 },
  { make: undefined, fuel: "gasoline", model: "a3", count: 5 },

  { make: undefined, fuel: "gasoline", model: undefined, count: 8 + 5 },
  { make: undefined, fuel: "diesel", model: undefined, count: 2 }
];

I 'm really not sure how to start.

Any help would be appreciated.

UPDATE

I ended up doing something as follows:

const groupedByMake = groupBy(results, "make");
const groupedByModel = groupBy(results, "model");
const groupedByFuel = groupBy(results, "fuel");
let groupedByMakeModel = {}
results.reduce(function (r, o) {
  var key = o.make + "-" + o.model;

  if (!groupedByMakeModel[key]) {
    groupedByMakeModel[key] = Object.assign({}, o); // create a copy of o
    r.push(groupedByMakeModel[key]);
  } else {
    groupedByMakeModel[key].count += o.count;
  }

  return r;
}, []);

let groupedByMakeFuel = {}
results.reduce(function (r, o) {
  var key = o.make + "-" + o.fuel;

  if (!groupedByMakeFuel[key]) {
    groupedByMakeFuel[key] = Object.assign({}, o); // create a copy of o
    r.push(groupedByMakeFuel[key]);
  } else {
    groupedByMakeFuel[key].count += o.count;
  }

  return r;
}, []);

let groupedByModelFuel = {}
results.reduce(function (r, o) {
  var key = o.model + "-" + o.fuel;

  if (!groupedByModelFuel[key]) {
    groupedByModelFuel[key] = Object.assign({}, o); // create a copy of o
    r.push(groupedByModelFuel[key]);
  } else {
    groupedByModelFuel[key].count += o.count;
  }

  return r;
}, []);

let groupedByMakeModelFuel = {}
results.reduce(function (r, o) {
  var key = o.make + "-" + o.model + "-" + o.fuel;

  if (!groupedByMakeModelFuel[key]) {
    groupedByMakeModelFuel[key] = Object.assign({}, o); // create a copy of o
    r.push(groupedByMakeModelFuel[key]);
  } else {
    groupedByMakeModelFuel[key].count += o.count;
  }

  return r;
}, []);


const result = []

each(keys(groupedByMake), key => {
  return result.push({
    make: key,
    model: undefined,
    fuel: undefined,
    count: sumBy(groupedByMake[key], 'count')
  })
})
each(keys(groupedByModel), key => {
  return result.push({
    make: undefined,
    model: key,
    fuel: undefined,
    count: sumBy(groupedByModel[key], 'count')
  })
})
each(keys(groupedByFuel), key => {
  return result.push({
    make: undefined,
    model: undefined,
    fuel: key,
    count: sumBy(groupedByFuel[key], 'count')
  })
})

each(keys(groupedByMakeModel), key => {
  return result.push({
    make: groupedByMakeModel[key]?.make,
    model: groupedByMakeModel[key]?.model,
    fuel: undefined,
    count: groupedByMakeModel[key]?.count
  })
})

each(keys(groupedByMakeFuel), key => {
  return result.push({
    make: groupedByMakeFuel[key]?.make,
    model: undefined,
    fuel: groupedByMakeFuel[key]?.fuel,
    count: groupedByMakeFuel[key]?.count
  })
})

each(keys(groupedByModelFuel), key => {
  return result.push({
    make: undefined,
    model: groupedByModelFuel[key]?.model,
    fuel: groupedByModelFuel[key]?.fuel,
    count: groupedByModelFuel[key]?.count
  })
})

each(keys(groupedByMakeModelFuel), key => {
  return result.push({
    make: groupedByMakeModelFuel[key]?.make,
    model: groupedByMakeModelFuel[key]?.model,
    fuel: groupedByMakeModelFuel[key]?.fuel,
    count: groupedByMakeModelFuel[key]?.count
  })
})

console.log("result: ", result)

Here is the playground.

But is there a better or faster way?

Boky
  • 11,554
  • 28
  • 93
  • 163
  • This answer might help you: https://stackoverflow.com/questions/14446511/most-efficient-method-to-groupby-on-an-array-of-objects – Helio Sep 01 '22 at 10:17

3 Answers3

3

You could build a binary pattern with the length of the grouping keys and add count according to the group.

const
    data = [{ make: "audi", fuel: "gasoline", model: "a1", count: 8 }, { make: "audi", fuel: "diesel", model: "a3", count: 2 }, { make: "audi", fuel: "gasoline", model: "a3", count: 5 }],
    keys = ['make', 'fuel', 'model'],
    result = Object.values(data.reduce((r, o) => {
        let i = 2 ** keys.length;
        while (i--) {
            const
                pattern = i.toString(2).padStart(keys.length, 0),
                key = keys.map((k, j) => +pattern[j] ? o[k] : '').join('|');

            r[key] ??= { ...Object.fromEntries(keys.map((k, j) => [k, +pattern[j] ? o[k] : undefined])), count: 0 };
            r[key].count += o.count;  
        }
        return r;
    }, {}));

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

A different approach by building a tree first with recursion and then take all totals from the leaves and their path properties.

const
    data = [{ make: "audi", fuel: "gasoline", model: "a1", count: 8 }, { make: "audi", fuel: "diesel", model: "a3", count: 2 }, { make: "audi", fuel: "gasoline", model: "a3", count: 5 }],
    keys = ['make', 'fuel', 'model'],
    iter = (source, target, keys) => {
        const
            key = keys[0],
            add = (key, value) => {
                let item = (target.children ??= []).find(q => q[key] === value);
                if (!item) target.children.push(item = { [key]: value });
                iter(source, item, keys.slice(1));
            };
            
        if (keys.length) {
            add(key, source[key]);
            add(key, undefined);
        } else {
            target.count = (target.count || 0) + source.count;
        }
    },
    totals = p => ({ children, ...o }) => children?.flatMap(totals({ ...p, ...o })) || { ...p, ...o },
    temp = data.reduce((r, o) => {
        iter(o, { children: r }, keys);
        return r;
    }, []),
    result = temp.flatMap(totals({}));

console.log(result);
console.log(temp); // how it's done
.as-console-wrapper { max-height: 100% !important; top: 0; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • Thanks for the answer. I get _Error: Unexpected token_ for `r[key] ??= {` – Boky Sep 02 '22 at 05:56
  • replace [logical nullish assignment `??=`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_nullish_assignment) pattern `x ??= y;` with assignment with [logical OR `||`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_Operators#Logical_OR) `x = x || y;`. – Nina Scholz Sep 02 '22 at 06:28
1

you can do something like this

const data = [
  { make: "audi", fuel: "gasoline", model: "a1", count: 8 },
  { make: "audi", fuel: "diesel", model: "a3", count: 2 },
  { make: "audi", fuel: "gasoline", model: "a3", count: 5 }
];

const combinations = Object.values(data.reduce((res, {count, ...rest}) => 
    Object.entries(rest).reduce(
      (keys, [k, v]) =>  [{[k]: undefined }, {[k]: v}]
        .flatMap(r => keys.length > 0? keys.flatMap(k => ({...k, ...r})): [r])
   , []).reduce((res, d) => {
      const k = JSON.stringify(d)
     if(k === "{}"){
       return res
     }
     const existing = res[k] || {...d, count:0}
     return {
       ...res,
       [k]: {...existing, count: existing.count + count}
     }
   }, res)
, {} ))

console.log(combinations)

this part create all the combination of key value and undefined

Object.entries(rest).reduce(
      (keys, [k, v]) =>  [{[k]: undefined }, {[k]: v}]
        .flatMap(r => keys.length > 0? keys.flatMap(k => ({...k, ...r})): [r])
   , [])

once you have got all the combinations you can create the key (I've used JSON.stringify for that)

then you just create an object with the keys and sum the counts if that key is already present

.reduce((res, d) => {
      const k = JSON.stringify(d)
     if(k === "{}"){
       return res
     }
     const existing = res[k] || {...d, count:0}
     return {
       ...res,
       [k]: {...existing, count: existing.count + count}
     }
   }, res)

And finally you get rid of the keys and return just the values using Object.values

with this implementation you can count elements with different attributes (count must be present tough)

R4ncid
  • 6,944
  • 1
  • 4
  • 18
0

Here is a way to handle this:

  1. Create an object that has the unique values of all keys (except count), we should get:
    {make: [undefined, 'audi'], fuel: [undefined, 'gasoline', 'diesel'], model: [undefined, 'a1', 'a3']}
    
  2. Create a calculateCount function that calculates the count provided the values, example:
    calculateCount(results, {make: 'audi'}) === 15
    
  3. Use a permutation functions such as this one, to create all possible combinations from the object that we created on step 1
  4. Calculate the sum of each combination, using the calculateCount function

const results = [
  { make: "audi", fuel: "gasoline", model: "a1", count: 8 },
  { make: "audi", fuel: "diesel", model: "a3", count: 2 },
  { make: "audi", fuel: "gasoline", model: "a3", count: 5 },
];

const keys = ['make', 'fuel', 'model'];

// create sets from keys, so that we can have the unique values of each key + undefined
const sets = results.reduce((obj, item) => {
  Object.entries(item).forEach(([key, value]) => {
    if (keys.includes(key)) {
      if (obj.hasOwnProperty(key)) {
        if (!obj[key].includes(value)) obj[key].push(value)
      }
      else {
        obj[key] = [undefined, value]
      }
    }
  });
  
  return obj;
}, {});

function calculateCount(arr, values) {
  return arr.reduce((sum, item) => {
    const match = Object.entries(values).every(([key, value]) => {
      // proceed if the value is undefined, or equal with item's value
      return value === undefined || value === item[key];
    })
            
    return match ? sum + item.count : sum;
  }, 0)
}

// https://stackoverflow.com/a/66483297/1354378
function getPermutations(object, index = 0, current = {}, results = []) {
  const keys = Object.keys(object);
  const key = keys[index];
  const values = object[key];

  for (const value of values) {
    current[key] = value;
    const nextIndex = index + 1;

    if (nextIndex < keys.length) {
      this.getPermutations(object, nextIndex, current, results);
    } else {
      const result = Object.assign({}, current);
      results.push(result);
    }
  }
  return results;
}

const all = getPermutations(sets).map(item => {
  return {
    ...item,
    count: calculateCount(results, item), // you can do this in getPermutations as well
  }
})

console.log(all)
Idrizi.A
  • 9,819
  • 11
  • 47
  • 88