Posted in:

It’s about time I shared some answers for the Lunchtime LINQ Challenge I set a last week. Thanks to everyone who sent in their answers. I had about a dozen different entries from my colleagues at work and readers of this blog, and it was great to see how many different approaches there were.

So in this post I’ll explain what I think are the nicest solutions to these in C# which bring together my favourite ideas from all the submitted answers.

Problem 1 – Motor Sport Scores

This one is nice and easy. Turn each score into an integer, order the sequence, skip the first three, and then sum. One nice optimisation for succinctness is to pass in the int.Parse method name rather than needing to use a full lambda expression.

"10,5,0,8,10,1,4,0,10,1"
    .Split(',')
    .Select(int.Parse)
    .OrderBy (n => n)
    .Skip(3)
    .Sum()

Problem 2 – Bishop Moves

This was the nastiest to solve in a single C# statement. There were three main strategies you could employ. First, enumerate all 64 positions on the chess board and check if the bishop could move there. Second, enumerate all 8 rows, and check up to two possible positions on that row. And third, start from the starting point and head out in each of four diagonal directions until you reach the edge of the board.

Obviously the other part of the challenge was how to work out if the Bishop could move to a square. Most people realised that you can use Math.Abs see if the number of rows and columns a particular square is from the starting point is the same, in which case it is on a diagonal.

Of course, even then we’re not done. We need to decide on a structure to use to pass coordinates through our LINQ expression – most people went for a string (e.g. “c6”), or an anonymous object.

So here’s one possible solution using an anonymous object:

Enumerable.Range('a', 8)
    .SelectMany(x => Enumerable.Range('1', 8), (r, c) => new { Row = (char)r, Column = (char)c })
    .Where(x => x.Row != 'c')
    .Where(x => Math.Abs(x.Row - 'c') == Math.Abs(x.Column - '6'))

There is still one slightly unsatisfying thing about this solution, and that is that the starting column of ‘c’ appears twice – once to filter out the starting row, and again to decide on whether we can move there.

Of course there are lots of ways that could be eliminated, one of which involves expanding the anonymous object to include the distance from the starting point. But this is an example of where the alternative LINQ query syntax is perhaps more readable, as the let keyword allows us to avoid constructing cumbersome anonymous objects. So here’s a solution that only uses the starting position once:

from row in Enumerable.Range('a', 8)
from col in Enumerable.Range('1', 8)
let dx = Math.Abs(row - 'c')
let dy = Math.Abs(col - '6')
where dx == dy
where dx != 0
select String.Format("{0}{1}",(char)row,(char)col)

Problem 3 – Sampling

This one turned out to be easier than I had originally anticipated, since there is a helpful overload of the LINQ Where extension method that gives us the index of each element. This made it trivially easy to use some modulo arithmetic to pick out every 5th element.

"0,6,12,18,24,30,36,42,48,53,58,63,68,72,77,80,84,87,90,92,95,96,98,99,99,100,99,99,98,96,95,92,90,87,84,80,77,72,68,63,58,53,48,42,36,30,24,18,12,6,0,-6,-12,-18,-24,-30,-36,-42,-48,-53,-58,-63,-68,-72,-77,-80,-84,-87,-90,-92,-95,-96,-98,-99,-99,-100,-99,-99,-98,-96,-95,-92,-90,-87,-84,-80,-77,-72,-68,-63,-58,-53,-48,-42,-36,-30,-24,-18,-12,-6"
    .Split(',')
    .Where((_, i) => (i + 1) % 5 == 0))

By the way, MoreLINQ has a TakeEvery extension method which does almost exactly what we want, except that it starts with the first element in the system, so it needs to be combined with a Skip(4) to get the same result

Problem 4 – Vote Winning Margin

This one was easy once you realised that you just needed to turn Yes’s into 1’s and No’s into –1’s and sum the lot. I did so using a call to Aggregate, and others did so with a call to Select and then Sum. But the simplest solution is to remember you can pass a lambda into Sum:

"Yes,Yes,No,Yes,No,Yes,No,No,No,Yes,Yes,Yes,Yes,No,Yes,No,No,Yes,Yes"
    .Split(',')
    .Sum(x => x == "Yes" ? 1 : -1)

Problem 5 – Counting Pets

This one is a classic case for GroupBy, with the only complication being the need to manipulate the key so all animals that aren’t dogs or cats get grouped together. Then you can select the total of each group:

"Dog,Cat,Rabbit,Dog,Dog,Lizard,Cat,Cat,Dog,Rabbit,Guinea Pig,Dog"
    .Split(',')
    .GroupBy (x => (x != "Dog" && x != "Cat") ? "Other" : x)
    .Select(g => new { Pet = g.Key, Count = g.Count() })

Of course, you could argue that this solution requires you to keep an in-memory Dictionary of the entire sequence, so if you were counting millions of pets, it would be better to just keep a running total. One person came up with a way to do that using Aggregate:

"Dog,Cat,Rabbit,Dog,Dog,Lizard,Cat,Cat,Dog,Rabbit,Guinea Pig,Dog"
    .Split(',')
    .Select(a => new { Dog = a == "Dog" ? 1 : 0, Cat = a == "Cat" ? 1 : 0, Other = (a != "Dog" && a != "Cat") ? 1 : 0 })
    .Aggregate((t, n) => new { Dog = t.Dog + n.Dog, Cat = t.Cat + n.Cat, Other = t.Other + n.Other })

Probably creating a new LINQ extension called something like CountBy would be a more reusable way to achieve the same result.

Problem 6 – Run Length Decoding

This final one could be solved fairly simply by using Regex.Matches to split the input string, and then using the string constructor that can repeat a single character n times to form the expanded version. You also need to know that you can turn the output of Regex.Matches into an IEnumerable<Match> by using LINQ’s handy Cast extension method, which helps you out with old methods that still return a non-generic enumerable.

String.Join("",
Regex.Matches("A5B10CD3",@"[A-Z]\d*")
    .Cast<Match>()
    .Select(m => m.Value)
    .Select(m => new string(m[0], m.Length == 1 ? 1 : Int32.Parse(m.Substring(1)))))

There was a lot of variety to the other solutions people submitted to this problem, especially in constructing a string with repeated characters, but also in the choice of regex syntax. And two people even managed to solve it without resorting to regex at all.

Anyway, hope you found that interesting and maybe you picked up a couple of LINQ tips along the way. Let me know in the comments if you think these can be solved even more elegantly. I’m going to share my answers in F# soon.

Want to learn more about LINQ? Be sure to check out my Pluralsight course LINQ Best Practices.

Comments

Comment by Mark Jones

A few nice tips in there as always - wish I'd known/remembered a few of these methods when I started the challenge.
Thanks for spending the time doing this.

Mark Jones
Comment by Sehnsucht

I'm curious about the non regex solution for Problem 6 ; during my experiment I managed to find one but I was mainly focused on answering in F# :D


"A5B10CD3"
.Aggregate (
new {result = "", letter = '\0' /*dummy value*/, lonely = false, amount = 0},
(acc, c) => char.IsLetter (c)
? new {result = acc.result + new string (acc.letter, acc.lonely ? 1 : acc.amount), letter = c, lonely = true, amount = 0}
: new {acc.result, acc.letter, lonely = false, amount = acc.amount * 10 + (int) c - '0'},
acc => acc.result + new string (acc.letter, acc.lonely ? 1 : acc.amount));

Sehnsucht
Comment by Mark Heath

yes, I think is does get a bit convoluted. here's one of the answers:


var testData6 = "A5B10CD3";
String.Concat(testData6.AsEnumerable()
.Zip(Enumerable.Range(1,testData6.Length),
(A,B) => new { Char = A, Index = B })
.Where(data => Char.IsLetter(data.Char))
.Select (data =>
{
int repetitions;
if (int.TryParse(new string(testData6.Skip(data.Index).TakeWhile(item => Char.IsDigit(item)).ToArray()), out repetitions) == false) repetitions = 1;
return new string(data.Char, repetitions);
}))
.Dump("PROBLEM 6");

Mark Heath
Comment by Mark Heath

and here's another:

new []{"A5B10CD3"}.Select(s => string.Join("", 
s.Split("ABCDEFGHIJKLMNOPQRSTUVWXYZ".ToCharArray()).Skip(1)
.Select(i => i =="" ? 1 : int.Parse(i))
.Zip(string.Join("", s.Split("0123456789".ToCharArray())),
(a, b) => string.Join("", Enumerable.Range(0, a).Select(z => b)))))
.Dump();

Mark Heath
Comment by Sehnsucht

That's some nice solutions too, I'd probably never thought of them but that's what's interesting ;)

Sehnsucht