'Javascript Equivalent to C# LINQ GroupBy

This is a follow up question to the following question:

Javascript Equivalent to C# LINQ Select

We are using Angular 2 + TypeScript:

I have an Array of objects. Each object in the array include a property called 'StudentType'.

I need to run a C# LINQ style query that extract a list of StudentType in the array and number of array members with that particular type.

While I can do an old school loop and do this, I wonder if there is a better way of doing this, like what C# LINQ GroupBy offers.

Since we are using Angular 2, the use of JQuery is not allowed by the project leads.



Solution 1:[1]

I just made up some studentType values for testing but you can use Array.prototype.reduce() to iterate over each element of the input array and add or manipulate properties of an accumulator object.

let arr = [{
    studentType: 'freshman'
  },
  {
    studentType: 'senior'
  },
  {
    studentType: 'sophomore'
  },
  {
    studentType: 'freshman'
  },
  {
    studentType: 'junior'
  },
  {
    studentType: 'senior'
  },
  {
    studentType: 'sophomore'
  },
  {
    studentType: 'freshman'
  },
  {
    studentType: 'sophomore'
  },
  {
    studentType: 'freshman'
  }
];

let result = arr.reduce((prev, curr) => {
  isNaN(prev[curr.studentType]) ? prev[curr.studentType] = 1 : prev[curr.studentType]++;
  return prev;
}, {});

console.log(result);

Solution 2:[2]

I created a groupBy function in TypeScript.

export interface Group {
  key: any;
  items: any[];
}

export interface GroupBy {
  keys: string[];
  thenby?: GroupBy;
}

export const groupBy = (array: any[], grouping: GroupBy): Group[] => {
  const keys = grouping.keys;
  const groups = array.reduce((groups, item) => {
    const group = groups.find(g => keys.every(key => item[key] === g.key[key]));
    const data = Object.getOwnPropertyNames(item)
      .filter(prop => !keys.find(key => key === prop))
      .reduce((o, key) => ({ ...o, [key]: item[key] }), {});
    return group
      ? groups.map(g => (g === group ? { ...g, items: [...g.items, data] } : g))
      : [
          ...groups,
          {
            key: keys.reduce((o, key) => ({ ...o, [key]: item[key] }), {}),
            items: [data]
          }
        ];
  }, []);
  return grouping.thenby ? groups.map(g => ({ ...g, items: groupBy(g.items, grouping.thenby) })) : groups;
};

I made a demo of it on StackBlitz at

https://stackblitz.com/edit/typescript-ezydzv

Solution 3:[3]

You can try groupBy from the manipula package that implements all C# LINQ methods and preserves its syntax.

Solution 4:[4]

Here you go (two options):

function GroupBy1(arr, key) {
    var group = {};

    arr.forEach(val => group[val[key]] = (group[val[key]] || 0) + 1);

    return group;
}

function GroupBy2(arr, key) {
    return arr.reduce((group, val) => {
        group[val[key]] = (group[val[key]] || 0) + 1;

        return count;
    }, {});
}

var arr = [
    { studentType: 'freshman' },
    { studentType: 'senior' },
    { studentType: 'freshman' },
    { studentType: 'junior' },
    { studentType: 'sophmore' },
    { studentType: 'freshman' },
    { studentType: 'freshman' },
    { studentType: 'junior' },
    { studentType: 'senior' },
    { studentType: 'senior' },
    { studentType: 'freshman' },
    { studentType: 'sophmore' },
    { studentType: 'freshman' },
    { studentType: 'sophmore' },
    { studentType: 'senior' }
];

console.log(GroupBy1(arr, 'studentType'));
console.log(GroupBy2(arr, 'studentType'));

Solution 5:[5]

Actually, LINQ-Group-By returns something like a dictionary with the group-item as key, and the element-array as object in a list.

This can be done easily in JavaScript.
Here's the TypeScript-Source:

// https://stackoverflow.com/questions/20310369/declare-a-delegate-type-in-typescript
// type Predicate<T, TKey> = (item: T) => TKey;

interface Predicate<T, TKey> 
{
    (item: T): TKey;
}


function LinqGroupBy<TSource, TKey>(source: TSource[], keySelector: Predicate<TSource, TKey>)
    : { [key: string]: TSource[] }
{
    if (source == null)
        throw new Error("ArgumentNullException: Source");
    if (keySelector == null)
        throw new Error("ArgumentNullException: keySelector");

    let dict: { [key: string]: TSource[]} = {};

    for (let i = 0; i < source.length; ++i)
    {
        let key: string = String(keySelector(source[i]));

        if (!dict.hasOwnProperty(key))
        {
            dict[key] = [];
        }

        dict[key].push(source[i]);
    }

    return dict;
}

Which transpiles down to:

function LinqGroupBy(source, keySelector) 
{
    if (source == null)
        throw new Error("ArgumentNullException: Source");
    if (keySelector == null)
        throw new Error("ArgumentNullException: keySelector");
    var dict = {};
    for (var i = 0; i < source.length; ++i) 
    {
        var key = String(keySelector(source[i]));
        if (!dict.hasOwnProperty(key)) 
        {
            dict[key] = [];
        }
        dict[key].push(source[i]);
    }
    return dict;
}

which is a bit shaky due to the nature of objects in JavaScript, but it works in most cases.

Solution 6:[6]

old post, but sharing my own implementation, mainly inspired from @Stefan.

Instead of returning { [key: string]: TSource[] }, my version returns { [key: string]: { key: TKey; value: TSource[] } }

Was useful in my case when the key is not a simple string.

interface Predicate<T, TKey> {
  (item: T): TKey;
}

const linqGroupBy = <TSource, TKey>(
  source: TSource[],
  keySelector: Predicate<TSource, TKey>
): { [key: string]: { key: TKey; value: TSource[] } } => {
  if (source == null) throw new Error("ArgumentNullException: Source");
  if (keySelector == null) throw new Error("ArgumentNullException: keySelector");

  const dict: { [key: string]: { key: TKey; value: TSource[] } } = {};

  for (let i = 0; i < source.length; ++i) {
    const k = keySelector(source[i]);
    let key = String(JSON.stringify(k));

    if (!dict.hasOwnProperty(key)) {
        dict[key] = {
            key: k,
            value: [],
        };
    }

    dict[key].value.push(source[i]);
  }

  return dict;
};

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Will
Solution 2
Solution 3 razon
Solution 4
Solution 5
Solution 6 Chris