Payman
Payman

Reputation: 448

How to return neighbouring items of an item in a LINQ query

Consider the following collection

var players = new[]{
     new {Id = 1, Name = "A", Score = 70},
     new {Id = 2, Name = "B", Score = 50},
     new {Id = 3, Name = "C", Score = 100},
     new {Id = 4, Name = "D", Score = 90}
 };

If I wanted to return the position of specific player (say, Player with ID = 1) in above list ordered by score I could write a query like this:

var result = players.OrderByDescending(p => p.Score)
             .Select((p, i) => new {player = p, Position = i})
             .Where(x => x.player.Id == 1)
             .First();
int position = result.Position;
var player = result.player;

Now how can I take this further and return the neighbouring items in addition to the actual player? Neighbouring items are the previous and next player and their respective positions when we order the list by score.

Here is the expected result of the query

var expectedResult = new[]{
    new {Id = 2, Name = "B", Score = 50},   //Previous player
    new {Id = 1, Name = "A", Score = 70},
    new {Id = 4, Name = "D", Score = 90}    //Next Player 
};

Could the above result be achieved by a single LINQ expression? Any help would be appreciated.

Upvotes: 0

Views: 459

Answers (2)

mqp
mqp

Reputation: 71945

I'd write something like this:

public static IEnumerable<IList<T>> GetOverlappingChunks<T>(
    this IEnumerable<T> sequence, int chunkSize)
{
    List<T> chunk = new List<T>(chunkSize);

    foreach (var elt in sequence)
    {
        chunk.Add(elt);

        if (chunk.Count > chunkSize)
            chunk.RemoveAt(0);

        if (chunk.Count == chunkSize)
            yield return chunk.ToArray();
    }
}

// ...

var result = players.OrderByDescending(p => p.Score)
             .GetOverlappingChunks(3)
             .Where(x => x[1].Id == 1);

(Too bad C# doesn't have a built-in deque type.)

If you need to handle cases where there are less than three players in the list, then you'd need to tweak GetOverlappingChunks and the check slightly.

Upvotes: 1

Yann Schwartz
Yann Schwartz

Reputation: 5994

You can use the Zip operator defined in .NET 4.0, or use the Scan operator defined in the Rx extensions:

With zip:

var result = players.OrderByDescending(p => p.Score)
    .Select((p, i) => new {Player = p, Position = i})
    .ToList(); //forces evaluation

result.Zip(result.Skip(1), (i,j) => new {First= i, Second=j})
      .Zip(result.Skip(2), (i,j) => new {First = i.First, Second = i.Second, Third=j})
      .First(o => o.Second.player.Id == 1);

But this won't give you neighbors for the first and last player. If you want them too, you have to massage your collections (since all three ienumerable must have the same number of items)

Upvotes: 4

Related Questions