How can an array be grouped by two properties?

后端 未结 3 1892
感情败类
感情败类 2021-01-20 20:25

Ex:

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


        
相关标签:
3条回答
  • 2021-01-20 21:04

    I would recommend to pass a callback function instead of a property name, this allows you to do the two-level-access easily:

    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 need to do for getting to your 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)
    );
    
    0 讨论(0)
  • 2021-01-20 21:17

    @Bergi's answer is really practical but I'll show you how building a multi-value "key" can be possible using JavaScript primitives – don't take this to mean Bergi's answer is bad in anyway; in fact, it's actually a lot better because of it's practicality. If anything, this answer exists to show you how much work is saved by using an approach like his.

    I'm going to go over the code bit-by-bit and then I'll have a complete runnable demo at the end.


    compound data equality

    Comparing compound data in JavaScript is a little tricky, so we're gonna need to figure out a way around this first:

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

    I want to cover a solution for the multi-value key because our entire answer will be based upon it - here I'm calling it a CollationKey. Our key holds some value and defines its own equality function which is used for comparing 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


    wishful thinking

    Now that we have a way to compare compound data, I want to make a custom reducing function that uses our multi-value key to group values. I'll call this function collateBy

    // 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 collateBy to work, 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())
    }
    

    Collation data container

    Ok, so we were being a little optimistic there too using Collation() as the starting value for the xs.reduce call. What should Collation be?

    What we know:

    • someCollation.set accepts a CollationKey and some value, and returns a new Collation
    • someCollation.get accepts a CollationKey and returns some value

    Well let's get to 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]])
      },
    })
    

    finishing up

    So far our collateBy function returns a Collation data container which is internally implemented with an array of [key, value] pairs, but what we really want back (according to your question) is just an array of values

    Let's modify collateBy in the slightest way that extracts the values – changes in 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 the values method to our Collation container

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

    runnable demo

    That's everything, so let's see it all work now – I used JSON.stringify in the output so that the deeply nested objects would 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}}
    //   ]
    // ]


    summary

    We made a custom collation function which uses a multi-value key for grouping our collated values. This was done using nothing but JavaScript primitives and higher-order functions. We now have a way to iterate thru a data set and collate it in an arbitrary way using keys of any complexity.

    If you have any questions about this, I'm happy to answer them ^_^

    0 讨论(0)
  • 2021-01-20 21:25

    @Bergi's answer is great if you can hard-code the inputs.

    If you want to use string inputs instead, you can use the sort() method, and walk 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);

    0 讨论(0)
提交回复
热议问题