Posted in:

Every now and then I create a “lunchtime LINQ challenge” for the developers at my work to try out and improve their LINQ skills. It’s been a while since I posted one to this blog, but I thought I’d take the first challenge and attempt to solve them in F#. For comparison, here are my answers in C#. Why not try the original challenge yourself first (in either C# or F#) before reading on?

Problem 1: Numbering Players

1. Take the following string "Davis, Clyne, Fonte, Hooiveld, Shaw, Davis, Schneiderlin, Cork, Lallana, Rodriguez, Lambert" and give each player a shirt number, starting from 1, to create a string of the form: "1. Davis, 2. Clyne, 3. Fonte" etc

This one is a fairly easy challenge in F#, so long as you know about the Seq.mapi function, which is basically the same as map, but also gives you the index of each item. Without that, you’d probably use Seq.zip to zip a sequence of numbers with a sequence of players. One nice advantage F# has here over our C# solution is that the String.concat function can be placed at the end of our pipeline. Note that I’m using my LINQPad DumpAs helper function to display the output in all these examples.

names.Split(',')
    |> Seq.mapi (fun index item -> sprintf "%d.%s" (index + 1) item)
    |> String.concat ", "
    |> DumpAs "Q1"

Problem 2: Sort by Age

Take the following string "Jason Puncheon, 26/06/1986; Jos Hooiveld, 22/04/1983; Kelvin Davis, 29/09/1976; Luke Shaw, 12/07/1995; Gaston Ramirez, 02/12/1990; Adam Lallana, 10/05/1988" and turn it into an IEnumerable of players in order of age (bonus to show the age in the output)

This is a slightly tricky challenge to do in a single statement as the calculation to work out someone’s age given their date of birth is not a one-liner. So I wanted to make a getAge helper function first. But since F# is a functional language, passing just the date of birth to this function would mean we don’t have a pure function, since it’s output would rely on the current date. So I made a getAgePure function, which takes two date parameters, and would be easily unit testable, and then partially applied it with today’s date to make the single parameter getAge function for convenience.

let getAgePure (today:DateTime) (dob:DateTime) =
    let age = today.Year - dob.Year;
    if dob > today.AddYears(-age) then age - 1 else age
let getAge = getAgePure DateTime.Today

Now we are ready to solve the problem. I’ve got three maps in a row to manipulate the input string into a triple of name, date of birth and age. I wanted to keep hold of date of birth so the sort can put two people in the right order even if they have the same age. But once they’re in the right order (sorted with sortByDescending), we only need the name and age to form the output string.

let players = "Jason Puncheon, 26/06/1986; Jos Hooiveld, 22/04/1983; Kelvin Davis, 29/09/1976; Luke Shaw, 12/07/1995; Gaston Ramirez, 02/12/1990; Adam Lallana, 10/05/1988"
players.Split(';')
    |> Seq.map (fun s -> s.Split(','))
    |> Seq.map (fun p -> p.[0].Trim(), DateTime.Parse(p.[1].Trim()))
    |> Seq.map (fun (name, dob) -> name,dob,(getAge dob))
    |> Seq.sortByDescending (fun (name,dob,age) -> dob)
    |> Seq.map (fun (name,dob,age) -> sprintf "%s %d" name age)
    |> String.concat ", "
    |> DumpAs "Q2"

Problem 3: Album Duration

Take the following string "4:12,2:43,3:51,4:29,3:24,3:14,4:46,3:25,4:52,3:27" which represents the durations of songs in minutes and seconds, and calculate the total duration of the whole album

This problem is solved by splitting the string, parsing to TimeSpans and then summing them. In C# we had to use Aggregate since Sum can’t sum timespans, and in F#, Seq.reduce is the function we want.

"4:12,2:43,3:51,4:29,3:24,3:14,4:46,3:25,4:52,3:27"
    .Split(',')
    |> Seq.map (fun s -> TimeSpan.Parse("0:" + s))   
    |> Seq.reduce (fun t1 t2 -> t1 + t2)
    |> DumpAs "Q3"

Problem 4: Coordinates

Create an enumerable sequence of strings in the form "x,y" representing all the points on a 3x3 grid. e.g. output would be: 0,0 0,1 0,2 1,0 1,1 1,2 2,0 2,1 2,2

This problem was designed to show off how SelectMany and Enumerable.Range can be used, although were other ways of solving it. In C# the LINQ Query Expression Syntax was particularly well suited to expressing this problem cleanly.

In F#, we can use sequence expressions which are a nice alternative to Enumerable.Range, but the syntax does get a little messier as we need yield! to emit the items in the nested sequence. I suspect there may be a slightly cleaner way to code this in F#, but here’s what I came up with:

seq { for x in 0 .. 2 do
        yield! seq { for y in 0 .. 2 do yield x,y } }
    |> Seq.map (fun (x,y) -> sprintf "%d,%d" x y)
    |> String.concat ", "
    |> DumpAs "Q4"

Problem 5: Swim Length Durations

Take the following string "00:45,01:32,02:18,03:01,03:44,04:31,05:19,06:01,06:47,07:35" which represents the times (in minutes and seconds) at which a swimmer completed each of 10 lengths. Turn this into an enumerable of timespan objects containing the time taken to swim each length (e.g. first length was 45 seconds, second was 47 seconds etc).

To solve this problem you need to deal with each time in pairs (as well as prepending 0:00 onto the sequence). In C#, I recommended making a ZipWithSelf extension method, but in F#, the library gives us exactly what we need with Seq.pairwise. This emits a sequence of tuples, one for each pair of consecutive items in the original sequence.

This leaves us the simple task of subtracting the TimeSpans in each pair, and formatting them back to strings. I also decided to factor out the TimeSpan parsing into a little helper function just to make the whole thing a little more readable. This is another example of where the simplified F# syntax for creating one-liner functions like this encourages you to compose your code out of lots of little functions.

open System.Globalization
let splitTimes = "00:45,01:32,02:18,03:01,03:44,04:31,05:19,06:01,06:47,07:35"
let parseTimeSpan ts = TimeSpan.ParseExact (ts, "mm\\:ss", CultureInfo.CurrentCulture)

("00:00," + splitTimes)
    .Split(',')
    |> Seq.map parseTimeSpan
    |> Seq.pairwise
    |> Seq.map (fun (s,f) -> f-s)
    |> Seq.map (fun t -> t.ToString("mm\\:ss"))
    |> String.concat ", "
    |> DumpAs "Q5b"

Problem 6: Ranges

Take the following string "2,5,7-10,11,17-18" and turn it into an IEnumerable of integers: 2 5 7 8 9 10 11 17 18

The first part of this problem involves taking the original sequence and mapping it into a sequence of tuples, representing start and finish of ranges (some of which may contain only a single number). I do this in three map steps, as I think it makes for more readable code than trying to do it in one. A little trick of using Seq.last on each range deals with individual numbers and ranges without the need for an if expression.

Then we need Seq.collect (which is essentially the F# version of LINQ’s SelectMany), which expands those ranges, and here the F# sequence expression syntax gives us a nice way of expressing this. We can end by mapping simply to string rather than needing sprintf which again makes for slightly cleaner code.

let input = "2,5,7-10,11,17-18"
input
    .Split(',')
    |> Seq.map (fun r -> r.Split('-'))
    |> Seq.map (fun p -> p.[0], p |> Seq.last)
    |> Seq.map (fun (f,l) -> (int f, int l))
    |> Seq.collect (fun (f,l) -> seq { f .. l })
    |> Seq.map string 
    |> String.concat ", "
    |> DumpAs "Q6"

Over to You

Now I’m still fairly new to F#, so I don’t doubt that I’ve missed a few tricks along the way. Let me know how I could have done it better in the comments. And I hope to share some more of my Lunchtime LINQ Challenges in future posts.

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

Comments

Comment by Mark Heath

great, thanks for sharing. I like your approach. Some good F# tricks in there like using (+) as a function that I need to get into the habit of using.

Mark Heath