How can I implement the GroupBy method in TypeScript?

4

I have an array:

interface IFoo { IDCidade: number; Nome: string }

var arr =
[
    { IDCidade: 10, Nome: "Foo" },
    { IDCidade: 10, Nome: "Bar" },

    { IDCidade: 20, Nome: "Foo" },
    { IDCidade: 20, Nome: "Bar" }
];

I want to group the elements so that the function return is

var groups =
[
    { Key: 10, Elements: { ... } },
    { Key: 20, Elements: { ... } },
];

How can I create this function and how would I define the return type?

    
asked by anonymous 14.12.2013 / 22:58

2 answers

1

Implemented as follows:

I have a class

class Linq<T>

And I have the field a that contains the array. And I have the GroupBy method:

// keySelector: extrai o valor chave do elemento para agrupar
// elementSelector: extrai o valor do elemento que será colocado na lista agrupada
// comparer: define se os objetos são iguais e devem ser agrupados no mesmo local
GroupBy<TKey, TElement>(keySelector: (e: T) => TKey
    , elementSelector?: (e: T) => TElement
    , comparer?: IEqualityComparer<TKey>): Linq<any>
{
    // seta os valores padrões para os parâmetros
    elementSelector = elementSelector || o => o;
    comparer = comparer || { Equals: (a, b) => a == b, GetHashCode: (e) => e.GetHashCode() };

    // copiar o campo da classe para a função local melhora a performance
    var a = this.a;

    // key: irá guardar a chave extraida
    // hashKey: irá guardar o hash da chave
    // reHashKey: irá guardar o hash da chave regerado caso os
    // elementos forem diferentes, isto produz resultados semelhantes ao
    // groupby do C#
    var key: TKey, hashKey: number, reHashKey: number;
    var hashs = {};
    for (var i = 0, n = a.length; i < n; ++i)
    {
        reHashKey = undefined;

        key = keySelector(a[i]);
        hashKey = comparer.GetHashCode(key);

        if (typeof hashs[hashKey] !== "undefined")
            reHashKey = comparer.Equals(key, <TKey>hashs[hashKey].Key) ? hashKey : hashKey + i;

        if (typeof reHashKey !== "undefined" && reHashKey !== hashKey)
            hashKey = reHashKey;

        // na minha "hashTable" eu guardo Key e Elements que compõe um grupo
        // caso já exista algo, adiciona os novos elementos
        // caso não exista, cria o objeto
        hashs[hashKey] = hashs[hashKey] || { Key: key, Elements: [] };
        hashs[hashKey].Elements.push(elementSelector(a[i]));
    }

    // isto converte o "array associativo" para um array de índices
    var keys = Object.keys(hashs);
    var ret: IGrouping<TKey, T>[] = [];
    for (var i = 0, n = keys.length; i < n; ++i)
    {
        ret.push(hashs[keys[i]]);
    }

    // retorna uma nova instancia da minha classe com o novo array
    return new Linq<any>(ret);
}

A test would be

GroupBy(): void
{
    var actual = [{ Key: 1, Value: 2 }, { Key: 1, Value: 3 }]
        .AsLinq()
        .GroupBy(o => o.Key);

    Assert.AreEqual(1, actual.Count());
    Assert.AreEqual(2, actual.ElementAt(0).Elements.length);
}

TheimplementationofAsLinqandGetHashCodeare:

interface Array { AsLinq<T>(): Linq<T>; } Array.prototype.AsLinq = function<T>(): TSLinq.Linq<T> { return new Linq<T>(this); } interface Object { GetHashCode(): number; } Object.prototype.GetHashCode = function () { var s: string = this instanceof Object ? JSON.stringify(this) : this.toString(); var hash = 0; if (s.length === 0) return hash; for (var i = 0; i < s.length; ++i) { hash = ((hash << 5) - hash) + s.charCodeAt(i); } return hash; }; Number.prototype.GetHashCode = function () { return this.valueOf(); };

Some comments:

  • TypeScript does not support nested types (yet?), so it is not possible to return a type: Linq<IGrouping<TKey, TElement>> .

  • The medium used to get HashCode is not completely safe for objects, it can cause a circular reference error.

18.12.2013 / 07:51
4

I do not know if there is a proper TypeScript form for this, but to solve your problem a JavaScript solution would suit.

JavaScript pure

var arr =
[
    { IDCidade: 10, Nome: "Foo" },
    { IDCidade: 10, Nome: "Bar" },

    { IDCidade: 20, Nome: "Foo" },
    { IDCidade: 20, Nome: "Bar" }
];

function groupBy(colecao, propriedade) {
    var agrupado = [];
    colecao.forEach(function (i) {
        var foiAgrupado = false;
        agrupado.forEach(function (j) {
            if (j.Key == i[propriedade]) {
                j.Elements.push(i);
                foiAgrupado = true;
            }
        });
        if (!foiAgrupado) agrupado.push({ Key: i[propriedade], Elements: [ i ] });
    });
    return agrupado;
}

var groups = groupBy(arr, 'IDCidade');

console.log(JSON.stringify(groups));

Output:

[{"Key":10,"Elements":[{"IDCidade":10,"Nome":"Foo"},{"IDCidade":10,"Nome":"Bar"}]},
 {"Key":20,"Elements":[{"IDCidade":20,"Nome":"Foo"},{"IDCidade":20,"Nome":"Bar"}]}]

Using linq.js

The linq.js library has a GroupBy method:

var arr =
[
    { IDCidade: 10, Nome: "Foo" },
    { IDCidade: 10, Nome: "Bar" },

    { IDCidade: 20, Nome: "Foo" },
    { IDCidade: 20, Nome: "Bar" }
];

var groups = Enumerable.From(arr).GroupBy("e => e.IDCidade", null, "e, grupo => { Key: e, Elements: grupo.ToArray() }").ToJSON();

console.log(groups);

Output:

[{"Key":10,"Elements":[{"IDCidade":10,"Nome":"Foo"},{"IDCidade":10,"Nome":"Bar"}]},
 {"Key":20,"Elements":[{"IDCidade":20,"Nome":"Foo"},{"IDCidade":20,"Nome":"Bar"}]}]
    
15.12.2013 / 04:01