Javascript Equivalent to C# LINQ GroupBy

Viewed 3628

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.

6 Answers

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

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

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'));

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.

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;
};
Related