How do I implement the Groupby method in Typescript?

Asked

Viewed 1,709 times

5

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 return of the function is

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

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

2 answers

4

I don’t know if there is a Typescript way for this, but to solve your problem a Javascript solution would meet.

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

Exit:

[{"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 library Linq.js has a method GroupBy:

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

Exit:

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

1


I implemented it as follows:

I have a class

class Linq<T>

And in it 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);
}

groupby test pass

The implementation of AsLinq and GetHashCode sane:

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 remarks:

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

  • The means used to obtain the HashCode is not totally safe for objects, can cause a circular reference error.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.