LINQ (Language Integrated Query) is a collection of methods and language features that allow you to interact with collections of data. In our last session, we focused on LINQ to Objects which allows us to use method predicates to interact with those collections.
Let's setup our Card
class and FritzSet
collection object to work with again in this workbook
class Card {
public Card(string def) {
var values = def.Split('-');
Rank = values[0];
Suit = values[1];
}
public string Rank;
public int RankValue {
get {
var faceCards = new Dictionary<string,int> { {"J", 11}, {"Q", 12}, {"K", 13}, {"A", 14} };
return faceCards.ContainsKey(Rank) ? faceCards[Rank] : int.Parse(Rank);
}
}
public string Suit;
public override string ToString() {
return $"{Rank}-{Suit}";
}
private static bool IsLegalCardNotation(string notation) {
var segments = notation.Split('-');
if (segments.Length != 2) return false;
var validSuits = new [] {"c","d","h","s"};
if (!validSuits.Any(s => s == segments[1])) return false;
var validRanks = new [] {"A","2","3","4","5","6","7","8","9","10","J","Q","K"};
if (!validRanks.Any(r => r == segments[0])) return false;
return true;
}
public static implicit operator Card(string id) {
if (IsLegalCardNotation(id)) return new Card(id);
return null;
}
}
class FritzSet<T> : IEnumerable<T> {
private List<T> _Inner = new List<T>();
public IEnumerator<T> GetEnumerator()
{
return _Inner.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return _Inner.GetEnumerator();
}
public FritzSet<T> Add(T newItem) {
var insertAt = _Inner.Count == 0 ? 0 : new Random().Next(0,_Inner.Count+1);
_Inner.Insert(insertAt, newItem);
return this;
}
public FritzSet<T> Shuffle() {
_Inner = _Inner.OrderBy(_ => Guid.NewGuid()).ToList();
return this;
}
}
var TheDeck = new FritzSet<Card>();
TheDeck.Add("A-c").Add("A-d");TheDeck.Add("A-h");TheDeck.Add("A-s");TheDeck.Add("2-c");TheDeck.Add("2-d");TheDeck.Add("2-h");TheDeck.Add("2-s");TheDeck.Add("3-c");TheDeck.Add("3-d");TheDeck.Add("3-h");TheDeck.Add("3-s");TheDeck.Add("4-c");TheDeck.Add("4-d");TheDeck.Add("4-h");TheDeck.Add("4-s");
TheDeck.Add("5-c");TheDeck.Add("5-d");TheDeck.Add("5-h");TheDeck.Add("5-s");TheDeck.Add("6-c");TheDeck.Add("6-d");TheDeck.Add("6-h");TheDeck.Add("6-s");TheDeck.Add("7-c");TheDeck.Add("7-d");TheDeck.Add("7-h");TheDeck.Add("7-s");TheDeck.Add("8-c");TheDeck.Add("8-d");TheDeck.Add("8-h");TheDeck.Add("8-s");
TheDeck.Add("9-c");TheDeck.Add("9-d");TheDeck.Add("9-h");TheDeck.Add("9-s");TheDeck.Add("10-c");TheDeck.Add("10-d");TheDeck.Add("10-h");TheDeck.Add("10-s");TheDeck.Add("J-c");TheDeck.Add("J-d");TheDeck.Add("J-h");TheDeck.Add("J-s");
TheDeck.Add("Q-c");TheDeck.Add("Q-d");TheDeck.Add("Q-h");TheDeck.Add("Q-s");TheDeck.Add("K-c");TheDeck.Add("K-d");TheDeck.Add("K-h");TheDeck.Add("K-s");
// TheDeck
TheDeck.Shuffle().Shuffle().Shuffle().Shuffle().Shuffle();
//TheDeck
Card PriyanksCard = "Joker"; // Fix this
//display(PriyanksCard ?? "No card assigned");
In review, we can write a little bit of code to work with this collection to deal cards appropriately for a Texas Hold 'em poker game:
var ourDeck = TheDeck.Shuffle().Shuffle();
var hand1 = new List<Card>();
var hand2 = new List<Card>();
var hand3 = new List<Card>();
hand1.Add(ourDeck.Skip(1).First());
hand2.Add(ourDeck.Skip(2).First());
hand3.Add(ourDeck.Skip(3).First());
hand1.Add(ourDeck.Skip(4).First());
hand2.Add(ourDeck.Skip(5).First());
hand3.Add(ourDeck.Skip(6).First());
display("Hand 1");
display(hand1);
display("Hand 2");
display(hand2);
display("Hand 3");
display(hand3);
// Burn a card and deal the next 3 cards called 'the flop'
display("The Flop");
display(ourDeck.Skip(8).Take(3));
// Burn a card and take one card called 'the turn'
display("The Turn");
display(ourDeck.Skip(12).First());
// Burn a card and take the final card called 'the river'
display("The River");
display(ourDeck.Skip(14).First());
Hand 1
index | RankValue | Rank | Suit |
---|---|---|---|
0 | 6 | 6 | s |
1 | 4 | 4 | s |
Hand 2
index | RankValue | Rank | Suit |
---|---|---|---|
0 | 11 | J | s |
1 | 11 | J | h |
Hand 3
index | RankValue | Rank | Suit |
---|---|---|---|
0 | 10 | 10 | s |
1 | 11 | J | d |
The Flop
index | RankValue | Rank | Suit |
---|---|---|---|
0 | 4 | 4 | c |
1 | 14 | A | c |
2 | 9 | 9 | h |
The Turn
RankValue | Rank | Suit |
---|---|---|
8 | 8 | h |
The River
RankValue | Rank | Suit |
---|---|---|
7 | 7 | d |
You can build expressions in the middle of your C# code that LOOKS like SQL turned sideways. Query Expressions begin with a from
clause and there's also a mandatory select
clause to specify the values to return. By convention, many C# developers who use this syntax align the clauses to the right of the =
symbol. Let's dig into that syntax a bit more:
// The simplest query
var outValues = from card in TheDeck // the required collection we are querying
select card; // the values to be returned
outValues
index | RankValue | Rank | Suit |
---|---|---|---|
0 | 10 | 10 | s |
1 | 4 | 4 | d |
2 | 3 | 3 | s |
3 | 4 | 4 | c |
4 | 6 | 6 | s |
5 | 5 | 5 | s |
6 | 5 | 5 | d |
7 | 2 | 2 | d |
8 | 13 | K | c |
9 | 6 | 6 | c |
10 | 5 | 5 | c |
11 | 13 | K | s |
12 | 14 | A | h |
13 | 10 | 10 | h |
14 | 12 | Q | s |
15 | 2 | 2 | s |
16 | 10 | 10 | d |
17 | 12 | Q | d |
18 | 3 | 3 | d |
19 | 13 | K | h |
(32 more) |
That's a boring and non-productive query. You can start to make queries more interesting by adding a where clause with an appropriate test in a format similar to that you would find in an if
statement. You can also optionally add an orderby clause with an ALSO optional descending keyword. Tinker with the query in the next block to learn more about these clauses
var results = from card in TheDeck
where card.Suit == "h" // Return just the Hearts
orderby card.RankValue descending
select card;
results
index | RankValue | Rank | Suit |
---|---|---|---|
0 | 14 | A | h |
1 | 13 | K | h |
2 | 12 | Q | h |
3 | 11 | J | h |
4 | 10 | 10 | h |
5 | 9 | 9 | h |
6 | 8 | 8 | h |
7 | 7 | 7 | h |
8 | 6 | 6 | h |
9 | 5 | 5 | h |
10 | 4 | 4 | h |
11 | 3 | 3 | h |
12 | 2 | 2 | h |
Additionally, nothing is requiring you to return the object in the collection. You can return different properties and values by changing up the select
clause:
var results = from card in TheDeck
where card.Suit == "h" && card.RankValue > 10
select card.Rank;
results
index | value |
---|---|
0 | A |
1 | K |
2 | J |
3 | Q |
Just like SQL syntax, you can correlate two collections and work with the combined result. The Join keyword allows you to relate two collections based on a matching key value in each collection. There is a similar Join method in LINQ to Objects that delivers the same feature.
Joins are slightly more involved and can be confusing topic, and we've embedded the official sample from the docs here. This sample relates Person
records to their Pets
that they own. The Join
method receives each collection and uses two expression bodied members to select the key properties from each collection. Finally, it provides a projection method to create the resultant object.
I have annotated this sample and the Join
method to make it clearer
class Person
{
public string Name { get; set; }
}
class Pet
{
public string Name { get; set; }
public Person Owner { get; set; }
}
Person magnus = new Person { Name = "Hedlund, Magnus" };
Person terry = new Person { Name = "Adams, Terry" };
Person charlotte = new Person { Name = "Weiss, Charlotte" };
// Declare the set of 4 pets and their owners
Pet barley = new Pet { Name = "Barley", Owner = terry };
Pet boots = new Pet { Name = "Boots", Owner = terry };
Pet whiskers = new Pet { Name = "Whiskers", Owner = charlotte };
Pet daisy = new Pet { Name = "Daisy", Owner = magnus };
List<Person> people = new List<Person> { magnus, terry, charlotte };
List<Pet> pets = new List<Pet> { barley, boots, whiskers, daisy };
// Create a list of Person-Pet pairs where
// each element is an anonymous type that contains a
// Pet's name and the name of the Person that owns the Pet.
var query =
people.Join(pets, // Join the People and Pets collections
person => person, // We will match the Person object
pet => pet.Owner, // with the Owner property in the Pet record
(person, pet) => // The combined output of Person and Pet
// is an object with OwnerName and the Pet's Name
new { OwnerName = person.Name, Pet = pet.Name });
foreach (var obj in query)
{
display(string.Format("{0} - {1}",
obj.OwnerName,
obj.Pet));
}
Hedlund, Magnus - Daisy
Adams, Terry - Barley
Adams, Terry - Boots
Weiss, Charlotte - Whiskers
Data in your query can be grouped together using the group clause. The group
clause can be used in place of the select
clause or can be used with the select
clause to aggregate data in various groupings. Let's try using the group
keywords
var results = from card in TheDeck
group card by card.Suit;
display(results.GetType());
results
index | value |
---|---|
0 | [ 10-s, 3-s, 6-s, 5-s, K-s, Q-s, 2-s, 4-s, 7-s, J-s, 9-s, 8-s, A-s ] |
1 | [ 4-d, 5-d, 2-d, 10-d, Q-d, 3-d, J-d, 7-d, 9-d, K-d, A-d, 8-d, 6-d ] |
2 | [ 4-c, K-c, 6-c, 5-c, A-c, 9-c, 10-c, 3-c, 8-c, 7-c, Q-c, J-c, 2-c ] |
3 | [ A-h, 10-h, K-h, J-h, 4-h, 7-h, 2-h, Q-h, 5-h, 9-h, 8-h, 3-h, 6-h ] |
Interestingly, we are returned a collection with all of the cards grouped by their suits. If we also wanted to select the suit and create a grouped result we could expand our query like this:
var results = from card in TheDeck
group card by card.Suit into suit
select new {TheSuit=suit.Key, suit};
display(results.GetType());
results
index | TheSuit | suit |
---|---|---|
0 | d | [ 9-d, 5-d, 2-d, A-d, 6-d, 8-d, K-d, 4-d, Q-d, 10-d, 3-d, 7-d, J-d ] |
1 | c | [ Q-c, J-c, 6-c, 3-c, 8-c, A-c, 5-c, 9-c, K-c, 2-c, 7-c, 10-c, 4-c ] |
2 | h | [ J-h, 7-h, 5-h, 8-h, 2-h, Q-h, A-h, 3-h, 6-h, 4-h, 10-h, 9-h, K-h ] |
3 | s | [ 3-s, 6-s, 8-s, Q-s, A-s, 5-s, K-s, 4-s, J-s, 7-s, 9-s, 2-s, 10-s ] |
Now this is VERY INTERESTING we have created an Anonymous Type, a type on the fly that contains a string field for TheSuit
and a collection of Card
objects in a field called suit
. We'll get more into Anonymous Types next week, but you need to know that you can use the new
keyword with curly braces { }
to create a type and make it available in your code. Many C# veterans will recommend against exposing the anonymous type outside of the method it is created in and instead suggest creating a concrete type to return in that select
clause.
Our groupings can take some interesting calculations. Let's write a grouping for all of the face cards (and the Ace too):
var results = from card in TheDeck
group card by card.RankValue > 10 into facecards
select new {TheSuit=facecards.Key, facecards};
results
index | TheSuit | facecards |
---|---|---|
0 | False | [ 10-s, 4-d, 3-s, 4-c, 6-s, 5-s, 5-d, 2-d, 6-c, 5-c, 10-h, 2-s, 10-d, 3-d, 9-c, 4-h, 4-s, 7-s, 7-h, 10-c ... (16 more) ] |
1 | True | [ K-c, K-s, A-h, Q-s, Q-d, K-h, A-c, J-h, Q-h, J-s, Q-c, J-d, J-c, K-d, A-d, A-s ] |
That looks strange, but we have two groups: 1 group that are the numeric cards and a second group that are the face cards. Let's tinker with that method a little more:
var results = from card in TheDeck
where card.RankValue > 10
group card by card.Rank into facecards
select new {Face=facecards.Key, facecards};
results
index | Face | facecards |
---|---|---|
0 | K | [ K-c, K-s, K-h, K-d ] |
1 | A | [ A-h, A-c, A-d, A-s ] |
2 | Q | [ Q-s, Q-d, Q-h, Q-c ] |
3 | J | [ J-h, J-s, J-d, J-c ] |
Now this sets up for a simplified Sam the Bellhop classic card trick. Take a few minutes and enjoy magician and former Philadelphia Eagles player Jon Dorenbos performing this trick where he sorts and finds cards while telling the story of Sam the Bellhop.
#r "nuget:LinqToCsv"
using LINQtoCSV;
class MyDataRow {
[CsvColumn(Name = "Year", FieldIndex = 1)]
public int Year {get; set;}
[CsvColumn(Name = "Number of tropical storms", FieldIndex = 2)]
public byte TropicalStormCount { get; set;}
[CsvColumn(Name = "Number of hurricanes", FieldIndex = 3)]
public byte HurricaneCount { get; set;}
[CsvColumn(Name = "Number of major hurricanes", FieldIndex = 4)]
public byte MajorHurricaneCount { get; set;}
// Accumulated Cyclone Energy
[CsvColumn(Name = "ACE", FieldIndex = 5)]
public decimal ACE { get; set; }
[CsvColumn(Name = "Deaths", FieldIndex = 6)]
public int Deaths { get; set; }
[CsvColumn(Name="Strongest storm", FieldIndex = 7)]
public string StrongestStorm { get; set; }
[CsvColumn(Name = "Damage USD", FieldIndex = 8)]
public string DamageUSD { get; set; }
[CsvColumn(Name = "Retired names", FieldIndex = 9)]
public string RetiredNames { get; set; }
[CsvColumn(Name = "Notes", FieldIndex = 10)]
public string Notes { get; set; }
}
var inputFileDescription = new CsvFileDescription
{
SeparatorChar = ',',
FirstLineHasColumnNames = true
};
var context = new CsvContext();
var hurricanes = context.Read<MyDataRow>("data/atlantic_hurricanes.csv", inputFileDescription);
display(hurricanes.OrderByDescending(h => h.Year).Take(10).Select(h => new {h.Year, h.TropicalStormCount, h.HurricaneCount, h.StrongestStorm}));
index | Year | TropicalStormCount | HurricaneCount | StrongestStorm |
---|---|---|---|---|
0 | 2020 | 23 | 8 | Laura |
1 | 2019 | 18 | 6 | Dorian |
2 | 2018 | 15 | 8 | Michael |
3 | 2017 | 17 | 10 | Maria |
4 | 2016 | 15 | 7 | Matthew |
5 | 2015 | 11 | 4 | Joaquin |
6 | 2014 | 8 | 6 | Gonzalo |
7 | 2013 | 14 | 2 | Humberto |
8 | 2012 | 19 | 10 | Sandy |
9 | 2011 | 19 | 7 | Ophelia |
var results = from storm in hurricanes
orderby storm.DamageUSD descending
where storm.HurricaneCount >= 10
select new {storm.Year, storm.HurricaneCount, storm.ACE, storm.StrongestStorm, storm.DamageUSD};
results
index | Year | HurricaneCount | ACE | StrongestStorm | DamageUSD |
---|---|---|---|---|---|
0 | 2017 | 10 | 224.88 | Maria | ≥ $294.67 billion |
1 | 1995 | 11 | 227.10 | Opal | $9.3 billion |
2 | 2012 | 10 | 132.63 | Sandy | $72.32 billion |
3 | 2010 | 12 | 165.48 | Igor | $7.4 billion |
4 | 1950 | 11 | 211.28 | Dog | $37 million |
5 | 2005 | 15 | 250.13 | Wilma | $180.7 billion |
6 | 1998 | 10 | 181.77 | Mitch | $12.2 billion |
7 | 1969 | 12 | 165.74 | Camille | $1.7 billion |
Extension Methods are a way to extend the functionality of a type without modifying the existing type. You don't even need access to the original type's source code to add on a feature to the type. We've seen examples of extension methods in use with the various predicate methods in the LINQ to Objects discussion above. The FritzSet<T>
object did not have all of the query interaction methods writte on it, but they were available to manipulate the collection.
To create our own extension methods, you create them in a static class
as methods with a signature where the first argument is prefixed by this
to indicate the type being amended. That class must be at the top-level and not hosted inside of another class.
NOTE: This code does not work in .NET Interactive with Jupyter Notebooks due to how .NET Interactive compiles and hosts code. The code is formatted here and does run in .NET applications
static class CardExtensions {
public static string ToFormattedValue(this Card theCard) {
var outSuit = theCard.Suit == "c" ? "♣" : theCard.Suit == "d" ? "♦" : theCard.Suit == "h" ? "♥" : "♠";
return theCard.Rank + outSuit;
}
}
Card myCard = new Card("A-h");
myCard.ToFormattedValue();