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)
.Selec开发者_运维知识库t((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.
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)
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.
精彩评论