Home > Software design >  How to Group Element in Javascript to get the Cartesian product in javascript
How to Group Element in Javascript to get the Cartesian product in javascript

Time:09-27

I have a scenario where I have an object just like this.

   [
    {attributeGroupId:2, attributeId: 11, name: 'Diamond'}
    {attributeGroupId:1, attributeId: 9, name: '916'}
    {attributeGroupId:1, attributeId: 1, name: '24K'}
    {attributeGroupId:2, attributeId: 12, name: 'Square'}
]

Expected result:

[
    {attributeGroupId:2, attributeId: 11, name: 'Diamond'},
    {attributeGroupId:2, attributeId: 12, name: 'Square'}
]

,

[
    {attributeGroupId:1, attributeId: 9, name: '916'},
    {attributeGroupId:1, attributeId: 1, name: '24K'}
]

So I can make the cartesian product of it just like this,

[
     {attributeId: 11-9, name: 'Diamond-916'}, 
     {attributeId: 11-1, name: 'Diamond-24K'},
     {attributeId: 12-9, name: 'Square-916'}, 
     {attributeId: 12-1, name: 'Square-24K'},
]

Now I want to keep this logic as generic as possible as the number of attributeGroupId is not known during runtime.

I think that splitting the array into multiple smaller array on the basis of attributeGroupId should be the first step.

CodePudding user response:

You can implement a basic filter function like this:

function filterForAttributeGroupId(data, id) {
  return data.filter((item) => item.attributeGroupId === id);
}

console.log(filterForAttributeGroupId(data, 1)) //contains all elements with attributeGroupId = 1
console.log(filterForAttributeGroupId(data, 2)) //contains all elements with attributeGroupId = 2

Here you have a generic solution returning an array of filtered Arrays:

function filterForAttributeGroupId(data) {
  const mem = {};
  data.forEach((item) => {
    if ( mem[item.attributeGroupId] ) {
      mem[item.attributeGroupId].push(item);
    } else {
      mem[item.attributeGroupId] = [item];
    }
  })
  return Object.values(mem);
}

Edit after feedback from comment If the order of concatenated attributes does not matter, you can use the following code to get the "cartesian concatenation" of n different arrays:

function cartesianProduct(arrays) {
  if (arrays.length <= 1 ) return arrays[0];
  const first = arrays[0];
  const second = cartesianProduct(arrays.slice(1));
  const result =  [];
   first.forEach(( itemFirst ) => {
      second.forEach( (itemSecond) => {
        result.push({attributeId: `${itemFirst.attributeId}-${itemSecond.attributeId}`, name: `${itemFirst.name}-${itemSecond.name}`})
      });
   });
   return result;
}

This way calling the following:

console.log(cartesianProduct(filterForAttributeGroupId(data)));

results in the expected data (although the strings are concatenated in another order):

[
  {
    "attributeId":"9-11",
    "name":"916-Diamond"
  },
  {
    "attributeId":"9-12",
    "name":"916-Square"
  },
  {
    "attributeId":"1-11",
    "name":"24K-Diamond"
  },
  {
    "attributeId":"1-12",
    "name":"24K-Square"
  }
]

CodePudding user response:

I think it's better to break this logic into pieces. First, a cartesian product function for an array of arrays seems very useful. So let's make a utility function for that. Second, breaking an array into subarrays based on some shared feature is also common. So let's make a second one for that.

Then we can write a simple main function which groups the id by attributeGroupId, and calls the cartesian product on that, then for each item in the resulting array like

[
  {attributeGroupId: 2, attributeId: 11, name: "Diamond"},
  {attributeGroupId: 1, attributeId: 9, name: "916"}
]

We can create a new object by combining the attributeId and name properties

const cartesian = ([xs, ...xss]) =>
  xs == undefined ? [[]] : xs .flatMap (x => cartesian (xss) .map (ys => [x, ...ys]))

const group = (fn, k) => (xs) => Object .values (xs .reduce (
  (a, x) => ((k = 'x'   fn (x)), (a [k] = a [k] || []), (a [k] .push (x)), a), {}
))

const regroupAtts = (xs) => 
  cartesian (group (x => x .attributeGroupId) (xs))
    .map (xs => ({
      attributeId: xs .map (x => x .attributeId) .join ('-'),
      name: xs .map (x => x.name) .join ('-')
    }))

const atts = [{attributeGroupId: 2, attributeId: 11, name: 'Diamond'}, {attributeGroupId: 1, attributeId: 9, name: '916'}, {attributeGroupId: 1, attributeId: 1, name: '24K'}, {attributeGroupId: 2, attributeId: 12, name: 'Square'}]
const atts2 = [{attributeGroupId: 2, attributeId: 11, name: 'Diamond'}, {attributeGroupId: 1, attributeId: 9, name: '916'}, {attributeGroupId: 3, attributeId: 101, name: 'foo'}, {attributeGroupId: 1, attributeId: 1, name: '24K'}, {attributeGroupId: 3, attributeId: 102, name: 'bar'}, {attributeGroupId: 2, attributeId: 12, name: 'Square'}, {attributeGroupId: 3, attributeId: 103, name: 'baz'}]


console .log ('Original:', regroupAtts (atts))
console .log ('With third group added:', regroupAtts (atts2))
.as-console-wrapper {max-height: 100% !important; top: 0}

We add a third attribute with entries of name "foo", "bar", and "baz" to show how this extends to more attributes. Of course, you'll have to consider combinatorial explosions if you add too many such attributes, but the code is designed to handle it.

And interesting abstraction from here is if you wanted to parameterize the names "attributeGroupId", "attributeId", and "name". We might choose to do this:

const regroup = (key, props) => (xs) => 
  cartesian (group (x => x [key]) (xs))
    .map (xs => Object .fromEntries (
      props .map (prop => [prop, xs .map (x => x [prop]) .join ('-')])
    ))

const regroupAtts = regroup ('attributeGroupId', ['attributeId', 'name']) 

const cartesian = ([xs, ...xss]) =>
  xs == undefined ? [[]] : xs .flatMap (x => cartesian (xss) .map (ys => [x, ...ys]))

const group = (fn, k) => (xs) => Object .values (xs .reduce (
  (a, x) => ((k = 'x'   fn (x)), (a [k] = a[k] || []), (a[k] .push (x)), a), {}
))

const regroup = (key, props) => (xs) => 
  cartesian (group (x => x [key]) (xs))
    .map (xs => Object .fromEntries (
      props .map (prop => [prop, xs .map (x => x [prop]) .join ('-')])
    ))

const regroupAtts = regroup ('attributeGroupId', ['attributeId', 'name']) 


const atts = [{attributeGroupId: 2, attributeId: 11, name: 'Diamond'}, {attributeGroupId: 1, attributeId: 9, name: '916'}, {attributeGroupId: 1, attributeId: 1, name: '24K'}, {attributeGroupId: 2, attributeId: 12, name: 'Square'}]
const atts2 = [{attributeGroupId: 2, attributeId: 11, name: 'Diamond'}, {attributeGroupId: 1, attributeId: 9, name: '916'}, {attributeGroupId: 3, attributeId: 101, name: 'foo'}, {attributeGroupId: 1, attributeId: 1, name: '24K'}, {attributeGroupId: 3, attributeId: 102, name: 'bar'}, {attributeGroupId: 2, attributeId: 12, name: 'Square'}, {attributeGroupId: 3, attributeId: 103, name: 'baz'}]

console .log ('Original:', regroupAtts (atts))
console .log ('With third group added:', regroupAtts (atts2))
.as-console-wrapper {max-height: 100% !important; top: 0}

In writing it this way, we now have reusable cartesian and group functions, which appear often. In fact, I stole those from previous answers I've written. (I did alter group a bit by adding the 'x' to the key generation. This is because your keys (attributeGroupIds) are simple numbers. When Object.values iterates an object like that, it does so by doing those first, in numeric order, then iterates the remaining String keys in insertion order, then the Symbols. By prepending a string, I make this all into insertion order, and get back the order you preferred. I'm trying to figure out if this variant of the function replaces the one in my usual toolbox.)

  • Related