Eu tenho um DataTable
que tem essa estrutura e dados:
id | inst | nome ------------------------ 1 | guitarra | John 2 | guitarra | george 3 | guitarra | Paulo 4 | tambores | ringo 5 | tambores | pete
Eu posso recuperar os registros assim:
IEnumerable ... class Beatle { int id; string inst; string name; }
Eu gostaria de obter a ordem interna daqueles que tocam os diferentes instrumentos. No MSSQL eu usaria
SELECT * ,Row_Number() OVER (PARTITION BY inst ORDER BY id) AS rn FROM Beatles
Esta consulta retorna
id | inst | nome | rn ----------------------------- 1 | guitarra | john | 1 2 | guitarra | george | 2 3 | guitarra | paul | 3 4 | tambores | ringo | 1 5 | tambores | pete | 2
Como posso fazer isso no Linq ?
Editar (depois da resposta aceita)
Código de trabalho completo:
var beatles = (new[] { new { id=1 , inst = "guitar" , name="john" }, new { id=2 , inst = "guitar" , name="george" }, new { id=3 , inst = "guitar" , name="paul" }, new { id=4 , inst = "drums" , name="ringo" }, new { id=5 , inst = "drums" , name="pete" } }); var o = beatles.OrderBy(x => x.id).GroupBy(x => x.inst) .Select(g => new { g, count = g.Count() }) .SelectMany(t => tgSelect(b => b) .Zip(Enumerable.Range(1, t.count), (j, i) => new { j.inst, j.name, rn = i })); foreach (var i in o) { Console.WriteLine("{0} {1} {2}", i.inst, i.name, i.rn); }
B “H
Eu sei que isso é velho. Mas por que a solução não é simplesmente?
var o = beatles.GroupBy(x => x.inst) .SelectMany(g => g.Select((j, i) => new { j.inst, j.name, rn = i + 1 }) );
Experimente este forro:
var o = beatles .OrderBy( x => x.id ) .GroupBy( x => x.inst ) .Select( group => new { Group = group, Count = group.Count() } ) .SelectMany( groupWithCount => groupWithCount.Group.Select( b => b) .Zip( Enumerable.Range( 1, groupWithCount.Count ), ( j, i ) => new { j.inst, j.name, RowNumber = i } ) ); foreach (var i in o) { Console.WriteLine( "{0} {1} {2}", i.inst, i.name, i.RowNumber ); }
Saída:
Guitar John 1 Guitar George 2 Guitar Paul 3 drums Ringo 1 drums Pete 2
Outra ideia é usar uma visão, se possível.
Como @The_Smallest aponta, o número da linha não é suportado pelo LINQ. Veja como você pode obter o que está procurando:
var grouped = beatles.OrderBy( x => x.id ) .ToList() // required because SelectMany below doesn't evaluate to SQL .GroupBy( x => x.inst ); var rns = grouped.ToDictionary( x => x.Key, x => 1 ); var result = grouped .SelectMany( x => x.Select( y => new { inst = y.inst, name = y.name, rn = rns[y.inst]++ } ) );
Outra solução para o Linq para objects é:
var result = beatles .GroupBy(g => g.inst) // PARTITION BY ^^^^ .Select(c => c.OrderBy(o => o.id).Select((v, i) => new { i, v }).ToList()) // ORDER BY ^^ .SelectMany(c => c) .Select(c => new { cvid, cvinst, cvname, rn = ci + 1 }) .ToList();
[ C# Demo ]
Alguns podem achar útil usar em seu código para obter o índice adequado
.Select((item, i) => new { Item = item, Index = i })
Outra solução para fazer o equivalente de RANK () OVER (PARTIÇÃO BY “partitionBy” ORDER BY “orderBy” DESC):
DataTable Rank(DataTable dt, string partitionBy, string orderBy, int whichRank) { DataView dv = new DataView(dt); dv.Sort = partitionBy + ", " + orderBy + " DESC"; DataTable rankDt = dv.ToTable(); rankDt.Columns.Add("Rank"); int rank = 1; for (int i = 0; i < rankDt.Rows.Count - 1; i++) { rankDt.Rows[i]["Rank"] = rank; DataRow thisRow = rankDt.Rows[i]; DataRow nextRow = rankDt.Rows[i + 1]; if (thisRow[partitionBy].ToString() != nextRow[partitionBy].ToString()) rank = 1; else rank++; } DataView selectRankdv = new DataView(rankDt); selectRankdv.RowFilter = "rank = " + whichRank; return selectRankdv.ToTable(); }