How can an array be grouped by two properties?

Example:

const arr = [{
  group: 1,
  question: {
    templateId: 100
  }
}, {
  group: 2,
  question: {
    templateId: 200
  }
}, {
  group: 1,
  question: {
    templateId: 100
  }
}, {
  group: 1,
  question: {
    templateId: 300
  }
}];

      

Expected Result: const result = groupBy(arr, 'group', 'question.templateId');

const result = [
  [{
    group: 1,
    question: {
      templateId: 100
    }
  }, {
    group: 1,
    question: {
      templateId: 100
    }
  }],
  [{
    group: 1,
    question: {
      templateId: 300
    }
  }],
  [{
    group: 2,
    question: {
      templateId: 200
    }
  }]
];

      


For now: I can group the result with one property using Array.prototype.reduce () .

function groupBy(arr, key) {
  return [...arr.reduce((accumulator, currentValue) => {
    const propVal = currentValue[key],
      group = accumulator.get(propVal) || [];
    group.push(currentValue);
    return accumulator.set(propVal, group);
  }, new Map()).values()];
}

const arr = [{
  group: 1,
  question: {
    templateId: 100
  }
}, {
  group: 2,
  question: {
    templateId: 200
  }
}, {
  group: 1,
  question: {
    templateId: 100
  }
}, {
  group: 1,
  question: {
    templateId: 300
  }
}];

const result = groupBy(arr, 'group');

console.log(result);
      

Run codeHide result


+3


source to share


3 answers


I would recommend passing a callback function instead of a property name, this makes it easy to do two-level access:

function groupBy(arr, key) {
  return Array.from(arr.reduce((accumulator, currentValue) => {
    const propVal = key(currentValue),
//                  ^^^^            ^
          group = accumulator.get(propVal) || [];
    group.push(currentValue);
    return accumulator.set(propVal, group);
  }, new Map()).values());
}

      

Now you can do groupBy(arr, o => o.group)

and groupBy(arr, o => o.question.templateId)

.



All you have to do to get the expected result is group by the first property, and then group each result by the second property:

function concatMap(arr, fn) {
  return [].concat(...arr.map(fn));
}
const result = concatMap(groupBy(arr, o => o.group), res =>
  groupBy(res, o => o.question.templateId)
);

      

+4


source


@ Bergi's answer is really practical, but I'll show you how you can create a multi-valued "key" using JavaScript primitives - don't assume that this means Bergi's answer is bad anyway; in fact, it is actually much better because of its practicality. Anyway, this answer exists to show you how much work is saved with an approach like his.

I'm going to beat the code in stages and then I will have a complete runnable demo at the end.


Combining composite data

Comparing composite data in JavaScript is a bit tricky, so we need to figure out how to do it first:

console.log([1,2] === [1,2]) // false
      

Run codeHide result


I want to highlight the multivalued key solution because our entire answer will be based on it - here I call it CollationKey

. Our key has some value and defines its own equality function which is used to compare keys

const CollationKey = eq => x => ({
  x,
  eq: ({x: y}) => eq(x, y)
})

const myKey = CollationKey (([x1, x2], [y1, y2]) =>
  x1 === y1 && x2 === y2)

const k1 = myKey([1, 2])
const k2 = myKey([1, 2])
console.log(k1.eq(k2)) // true
console.log(k2.eq(k1)) // true

const k3 = myKey([3, 4])
console.log(k1.eq(k3)) // false
      

Run codeHide result



wishful thinking

Now that we have a way to compare composite data, I want to create a custom shortening function that uses our multivalued key to group values ​​together. I will call this functioncollateBy

// key = some function that makes our key
// reducer = some function that does our reducing
// xs = some input array
const collateBy = key => reducer => xs => {
  // ...?
}

// our custom key;
// equality comparison of `group` and `question.templateId` properties
const myKey = CollationKey ((x, y) =>
  x.group === y.group
    && x.question.templateId === y.question.templateId)

const result =
  collateBy (myKey) // multi-value key
            ((group=[], x) => [...group, x]) // reducing function: (accumulator, elem)
            (arr) // input array

      

So now that we know how we want to work collateBy

, let's implement it

const collateBy = key => reducer => xs => {
  return xs.reduce((acc, x) => {
    const k = key(x)
    return acc.set(k, reducer(acc.get(k), x))
  }, Collation())
}

      


Sort data container

Okay, so we were a little optimistic about using Collation()

as the starting value for the call xs.reduce

. What should it be Collation

?

What do we know:



  • someCollation.set

    takes a value CollationKey

    and some value and returns a new oneCollation

  • someCollation.get

    takes CollationKey

    and returns some value

Okay, let's work!

const Collation = (pairs=[]) => ({
  has (key) {
    return pairs.some(([k, v]) => key.eq(k))
  },
  get (key) {
    return (([k, v]=[]) => v)(
      pairs.find(([k, v]) => k.eq(key))
    )
  },
  set (key, value) {
    return this.has(key)
      ? Collation(pairs.map(([k, v]) => k.eq(key) ? [key, value] : [k, v]))
      : Collation([...pairs, [key, value]])
  },
})

      


completion

So far, our function collateBy

returns a data container Collation

which is internally implemented with an array of pairs [key, value]

, but what we really want to return (as per your question) is just an array of values

Let's modify in the collateBy

slightest way that extracts the values ​​- changes to bold

const collateBy = key => reducer => xs => {
  return xs.reduce((acc, x) => {
    let k = key(x)
    return acc.set(k, reducer(acc.get(k), x))
  }, Collation()).values()
}
      

So now we will add a method values

to our containerCollation

values () {
  return pairs.map(([k, v]) => v)
}

      


runnable demo

That's all, so let it work for now - I used JSON.stringify

in the output to have deeply nested objects display all content

// data containers
const CollationKey = eq => x => ({
  x,
  eq: ({x: y}) => eq(x, y)
})

const Collation = (pairs=[]) => ({
  has (key) {
    return pairs.some(([k, v]) => key.eq(k))
  },
  get (key) {
    return (([k, v]=[]) => v)(
      pairs.find(([k, v]) => k.eq(key))
    )
  },
  set (key, value) {
    return this.has(key)
      ? Collation(pairs.map(([k, v]) => k.eq(key) ? [key, value] : [k, v]))
      : Collation([...pairs, [key, value]])
  },
  values () {
    return pairs.map(([k, v]) => v)
  }
})

// collateBy
const collateBy = key => reducer => xs => {
  return xs.reduce((acc, x) => {
    const k = key(x)
    return acc.set(k, reducer(acc.get(k), x))
  }, Collation()).values()
}

// custom key used for your specific collation
const myKey =
  CollationKey ((x, y) =>
    x.group === y.group
      && x.question.templateId === y.question.templateId)

// your data
const arr = [ { group: 1, question: { templateId: 100 } }, { group: 2, question: { templateId: 200 } }, { group: 1, question: { templateId: 100 } }, { group: 1, question: { templateId: 300 } } ]

// your answer
const result =
  collateBy (myKey) ((group=[], x) => [...group, x]) (arr)

console.log(result)
// [
//   [
//     {group:1,question:{templateId:100}},
//     {group:1,question:{templateId:100}}
//   ],
//   [
//     {group:2,question:{templateId:200}}
//   ],
//   [
//     {group:1,question:{templateId:300}}
//   ]
// ]
      

Run codeHide result



Summary

We've created a custom sort function that uses a multi-valued key to group our matched values ​​together. This was done using only JavaScript primitives and higher order functions. We now have a way to iterate through the dataset and randomly match using keys of any complexity.

If you have any questions about this, I will be happy to answer them ^ _ ^

+4


source


@ Bergi's answer is great if you can hardcode the inputs.

If you want to use string inputs instead, you can use the method sort()

and loop through the objects as needed.

This solution will handle any number of arguments:

function groupBy(arr) {
  var arg = arguments;
  
  return arr.sort((a, b) => {
    var i, key, aval, bval;
    
    for(i = 1 ; i < arguments.length ; i++) {
      key = arguments[i].split('.');
      aval = a[key[0]];
      bval = b[key[0]];
      key.shift();
      while(key.length) {  //walk the objects
        aval = aval[key[0]];
        bval = bval[key[0]];
        key.shift();
      };
      if     (aval < bval) return -1;
      else if(aval > bval) return  1;
    }
    return 0;
  });
}

const arr = [{
  group: 1,
  question: {
    templateId: 100
  }
}, {
  group: 2,
  question: {
    templateId: 200
  }
}, {
  group: 1,
  question: {
    templateId: 100
  }
}, {
  group: 1,
  question: {
    templateId: 300
  }
}];

const result = groupBy(arr, 'group', 'question.templateId');

console.log(result);
      

Run codeHide result


+1


source







All Articles