Hey, se você caiu aqui de paraquedas, esta é uma sequência de outro post
O método GroupBy
serve para agrupar elementos de uma coleção de acordo com um determinado valor (ou conjunto de valores) comum a eles e seu funcionamento é análogo à cláusula GROUP BY
dos bancos de dados relacionais.
O GroupBy
divide uma coleção em um grupo de coleções menores de acordo com o “critério de agrupamento” informado por parâmetro. De forma geral, os agrupamentos têm como propósito realizar uma ou mais agregações em cada grupo.
A primeira coisa que é preciso entender é como funciona a principal estrutura de retorno do método.
IEnumerable<IGrouping<TKey, TSource>>
Vendo a assinatura acima nota-se que o método irá retornar uma coleção de IGrouping's
. Mas o que é este IGrouping
?
IGrouping
é uma interface que herda de IEnumerable
, portanto ela é também um enumerável, uma coleção. Isso quer dizer que o método retorna uma coleção de coleções, ou melhor, uma coleção de grupos. A definição desta interface é a seguinte:
interface IGrouping<out TKey, out TElement>: IEnumerable<TElement>
{
TKey Key { get; }
}
Cada grupo vai conter todos os elementos que atendam ao mesmo critério de agrupamento e também uma propriedade Key
que carregará o valor (ou valores) em comum entre estes elementos.
Por exemplo: Ao agrupar uma lista de compras pelo mês da compra utilizando o GroupBy
, o retorno será uma nova coleção com todos os grupos de compras para cada mês diferente.
Para efeito de comparação, é possível pensar nesta estrutura como um dicionário onde os elementos sejam coleções (listas, arrays, entre outros).
IDictionary<TipoChave, IEnumerable<TipoElemento>>
É importante ter em mente que, diferentemente de um dicionário, os grupos não contêm uma propriedade Value
Code time
Primeiro, vamos usar uma classe Compra
e criar um array de compras com alguns dados.
class Compra
{
public string Cliente { get; set; }
public decimal Valor { get; set; }
public DateTime Data { get; set; }
public override string ToString() => $"{Cliente}, com valor de {Valor:n2} em {Data:dd/MM}";
}
IReadOnlyCollection<Compra> Compras => new []
{
new Compra { Valor = 100, Data = new DateTime(2019, 01, 19), Cliente = "Finn" },
new Compra { Valor = 200, Data = new DateTime(2019, 02, 05), Cliente = "Jake" },
new Compra { Valor = 1350, Data = new DateTime(2019, 02, 07), Cliente = "BMO" },
new Compra { Valor = 900, Data = new DateTime(2019, 01, 21), Cliente = "Finn" },
new Compra { Valor = 150, Data = new DateTime(2019, 02, 05), Cliente = "Jake" },
new Compra { Valor = 1500, Data = new DateTime(2019, 02, 09), Cliente = "Finn" },
new Compra { Valor = 300, Data = new DateTime(2019, 03, 10), Cliente = "Marceline" },
new Compra { Valor = 1200, Data = new DateTime(2019, 03, 15), Cliente = "Marceline" },
new Compra { Valor = 1900, Data = new DateTime(2019, 01, 10), Cliente = "Marceline" },
new Compra { Valor = 2500, Data = new DateTime(2019, 03, 18), Cliente = "Finn" }
};
Como dito anteriormente, é possível agrupar as compras pelo mês da compra. Veja o código abaixo.
IEnumerable<IGrouping<int, Compra>> grupos
= Compras.GroupBy(compra => compra.Pagamento.Month); // Como deve ser agrupado
foreach(var grupo in grupos) // Coleção de grupos
{
WriteLine($"Compras pagas no mês de {_dtFormat.GetMonthName(grupo.Key)}"); // Chave do agrupamento
foreach(var elemento in grupo) // Compras do grupo => IGrouping<int, Compra>
{
WriteLine($"\t{elemento.ToString()}");
}
}
Também é possível agrupar as compras pelo cliente.
IEnumerable<IGrouping<string, Compra>> grupos
= Compras.GroupBy(x => x.Cliente); // Como deve ser agrupado
foreach(var grupo in grupos) // Coleção de grupos
{
WriteLine($"Compras de {grupo.Key}");
foreach(var compra in grupo) // Compras do grupo => IGrouping<string, Compra>
{
WriteLine($"\t {compra.Valor:n2}");
}
}
No código acima, fica claro que o retorno do GroupBy
é uma coleção (IEnumerable
) de IGrouping
. Ou seja, cada elemento desta coleção é uma outra coleção formada pelo conjunto de todos os elementos da coleção original que atendem um mesmo critério de agrupamento, a propriedade Key
é justamente o valor deste critério de agrupamento.
A tabela abaixo ilustra como ficam os dados na variável grupos
para os dois códigos.
Agregações
Como dito no início, é bem comum se agrupar dados para aplicar funções de agregação nestes grupos. Com LINQ, é muito simples fazer estas agregações.
No código abaixo, é produzida uma nova coleção contendo algumas informações através das funções de agregação.
var agg = grupos.Select(gp => new
{
Cliente = gp.Key,
QuantidadeCompras = gp.Count(),
MelhorCompra = gp.Max(x => x.Valor),
PiorCompra = gp.Min(x => x.Valor),
TotalCompras = gp.Sum(x => x.Valor)
});
foreach(var elemento in agg)
{
WriteLine($"Cliente: {elemento.Cliente}");
WriteLine($"Quantidade de compras: {elemento.QuantidadeCompras}");
WriteLine($"Melhor Compra: {elemento.MelhorCompra}");
WriteLine($"Pior Compra: {elemento.PiorCompra}");
WriteLine($"Total em compras: {elemento.TotalCompras}");
}
É importante notar que gp
é também uma coleção, portanto é possível filtrá-la usando Where
, fazer projeções usando Select
, etc.
O código abaixo, filtra por clientes que tenham mais de uma compra e, na projeção, a propriedade PiorCompra
representa a pior compra (a com menor valor) acima de $ 150.
// Apenas clientes com mais de uma compra
var aggComFiltro = grupos.Where(gp => gp.Count() > 1).Select(gp => new
{
Cliente = gp.Key,
PiorCompra = gp.Where(c => c.Valor > 150).Min(x => x.Valor)
// Apenas compras acima de 150
});
foreach(var elemento in aggComFiltro)
{
WriteLine($"Cliente: {elemento.Cliente}");
WriteLine($"Pior compra (acima de 150): {elemento.PiorCompra}");
}
Da mesma forma, também é possível aninhar agrupamentos, produzindo resultados mais complexos.
Pra finalizar, veja o código abaixo. Nele a lista de compras é agrupada pelo mês da compra e posteriormente, nos grupos de cada mês, é feito um segundo agrupamento pelo cliente da compra. Além disso, para cada agrupamento (tanto por mês, quanto por cliente) são adicionadas informações extras como a quantidade, o total em compras e o maior valor dentre as compras.
var comprasPorMesECliente = Compras.GroupBy(compra => compra.Data.Month).Select(gp => new
{
Mes = _dtFormat.GetMonthName(gp.Key),
TotalComprasMes = gp.Sum(c => c.Valor),
MaiorCompraMes = gp.Max(c => c.Valor),
QtdComprasMes = gp.Count(),
ComprasPorCliente = gp.GroupBy(c => c.Cliente).Select(g => new
{
Cliente = g.Key,
Compras = g.ToArray(),
Total = g.Sum(c => c.Valor),
MaiorValor = g.Max(c => c.Valor),
Qtd = g.Count()
})
});
foreach(var grupo in comprasPorMesECliente)
{
WriteLine($"Mês: {grupo.Mes}");
WriteLine($"{grupo.QtdComprasMes} compras, com um total de {grupo.TotalComprasMes}.");
WriteLine($"A maior compra foi de {grupo.MaiorCompraMes:n2}");
foreach(var grupoInterno in grupo.ComprasPorCliente)
{
WriteLine($"\t{grupoInterno.Cliente} fez {grupoInterno.Qtd} compras, com um total de {grupoInterno.Total}");
WriteLine($"\tA maior compra foi de {grupoInterno.MaiorValor:n2}");
foreach(var elemento in grupoInterno.Compras)
{
WriteLine($"\t\t{elemento.Valor:n2} em {elemento.Data:dd/MM}");
}
}
}
O código dos exemplos pode ser executado e editado no Repl.it