54

I have a DataTable which has a structure and data:

id |   inst   |   name
------------------------
 1 |  guitar  |  john
 2 |  guitar  |  george
 3 |  guitar  |  paul
 4 |  drums   |  ringo
 5 |  drums   |  pete

I can retrieve the records via:

IEnumerable <Beatle>...

class Beatle
{
  int id;
  string inst;
  string name;
}

I'd like to get the internal order of those who play the different instruments. In MSSQL I'd use

SELECT 
    *
    ,Row_Number() OVER (PARTITION BY inst ORDER BY id) AS rn
FROM Beatles

This query returns

id |   inst   |   name  | rn
-----------------------------
 1 |  guitar  |  john   | 1
 2 |  guitar  |  george | 2
 3 |  guitar  |  paul   | 3
 4 |  drums   |  ringo  | 1
 5 |  drums   |  pete   | 2

Question:
How can I do that in Linq?

Royi Namir
  • 144,742
  • 138
  • 468
  • 792

9 Answers9

35

B"H

I know this is old. But why isn't the solution simply?

var o = beatles.GroupBy(x => x.inst)
               .SelectMany(g =>
                   g.Select((j, i) => new { j.inst, j.name, rn = i + 1 })
               );
Gilad Green
  • 36,708
  • 7
  • 61
  • 95
Rabbi
  • 4,622
  • 9
  • 35
  • 45
35

Try this one liner:

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

Output:

Guitar John 1
Guitar George 2
Guitar Paul 3
drums Ringo 1
drums Pete 2
Stephen Turner
  • 7,125
  • 4
  • 51
  • 68
Jon Comtois
  • 1,824
  • 1
  • 22
  • 29
  • @c.sokun : the "t" is a variable holding the anonymous type which was generated in the `.Select` statment. So holding the count of the grouping. – Jon Comtois Dec 02 '14 at 15:17
  • @JonComtois I was breaking my head how come `t` is known after `selectmany` , then I realize that it's after the `select` and not after the `selectmany`. Edited for clarification. :-) – Royi Namir Oct 02 '15 at 08:00
  • 4
    I'm curious what kind of SQL this generates, if EF supports this. – Dai Oct 26 '15 at 10:02
  • 3
    Be careful on the `.GroupBy` since .NET string comparison is case sensitive, whereas SQL will not be. If you have `guitar` and `Guitar`, you'll get different results as when compared to SQL. See this SO post for options when grouping strings: http://stackoverflow.com/questions/16190180/case-insensitive-group-on-multiple-columns – ryancdotnet Apr 03 '17 at 23:25
  • Skip the Counting/Zipping and go with the other overload of Select that receives an index parameter (it starts at zero so you would need to add one to it.) – tuespetre Jun 10 '17 at 19:49
  • 25
    that's the most lines I've ever seen in a one liner – jtate Dec 13 '17 at 19:41
  • 2
    This works only on Linq to object but not on Linq to Entities. Zip can not be converted. – Istvan Heckl Jul 21 '21 at 13:51
6

Another idea is using a view, if possible.

leppie
  • 115,091
  • 17
  • 196
  • 297
4

Another solution for Linq to objects is:

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 { c.v.id, c.v.inst, c.v.name, rn = c.i + 1 })
            .ToList();

[ C# Demo ]

shA.t
  • 16,580
  • 5
  • 54
  • 111
3

As @The_Smallest points out, row number is not supported by LINQ. Here's how you can get what you're looking for, though:

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]++ } ) );
Omar
  • 16,329
  • 10
  • 48
  • 66
Ethan Brown
  • 26,892
  • 4
  • 80
  • 92
  • 1
    `y => new { y.inst, y.name, rn = rns[y.inst]++ }` is not expression, that's why it won't compile to sql query. Pity, but that query would be executed locally, no benefits of sql. – The Smallest Apr 02 '12 at 18:24
  • Well, you can drag it all into memory, where it will work. See update above. – Ethan Brown Apr 02 '12 at 18:26
  • 1
    Yes, as you say, no benefits of SQL. If you want to leverage SQL, you'll have to, as @The_Smallest points out below, create a stored procedure. – Ethan Brown Apr 02 '12 at 18:30
2

Some may find useful using in your code to get proper index

.Select((item, i) => new { Item = item, Index = i })
Gilad Green
  • 36,708
  • 7
  • 61
  • 95
Tomasz Maj
  • 1,541
  • 15
  • 15
2

Another solution to do the equivalent of RANK() OVER (PARTITION 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();
  }
Dgan
  • 10,077
  • 1
  • 29
  • 51
1

https://github.com/jurioli/Applied

var result = data.GroupBy(a => new { a.inst }).AsPartition()
.Over(p => p.RowNumber(), (a, value) => new { a.inst, a.name, RowNumber = value })
.ToList();
jurio
  • 21
  • 1
0

Based on @Jon Comtois answer you can use the following extension method if you need the specific row number gets filtered out;

    /// <summary>
    /// Groups and orders by the data partitioning and returns the list of data with provided rownumber
    /// It is the equivalent of SQL's ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)
    /// </summary>
    /// <typeparam name="TSource">Source type</typeparam>
    /// <typeparam name="TGroupingKey">Generic type for grouping property</typeparam>
    /// <typeparam name="TOrderKey">Generic type for ordering property</typeparam>
    /// <param name="source">Source list to be partitioned</param>
    /// <param name="groupingProperty">Grouping property</param>
    /// <param name="orderProperty">Ordering property</param>
    /// <param name="orderByAsc">Order direction</param>
    /// <param name="rowNumber">Rows to be filtered out finally</param>
    /// <returns>Partitioned list</returns>
    public static List<TSource> FilterByPartitioning<TSource, TGroupingKey, TOrderKey>(this List<TSource> source, Func<TSource, TGroupingKey> groupingProperty, Func<TSource, TOrderKey> orderProperty, bool orderByAsc = true, int rowNumber = 1)
    {
        var orderedData = orderByAsc ? source.OrderBy(orderProperty) : source.OrderByDescending(orderProperty);

        return orderedData.GroupBy(groupingProperty)
            .Select(g => new { g, count = g.Count() })
            .SelectMany(t => t.g.Select(b => b)
                .Zip(Enumerable.Range(1, t.count), (source, i) => new { source, row = i }))
            .Where(x => x.row == rowNumber)
            .Select(x => x.source).ToList();
    }

    //Usage
    var result = myList.FilterByPartitioning(group => group.PropertyToGroup, order => order.PropertyToOrder, orderByAsc: false, rowNumber: 1);
T.Y. Kucuk
  • 447
  • 8
  • 24