0 Comments

As promised, here are some F# solutions to my second lunchtime LINQ challenge. I’ve still got a way to go with getting familiar with all the features of F#, so thanks especially to Sehnsucht whose answers pointed me in the direction of some better ways to solve these.

Problem 1 – Motorsport Scores

For the first one, I pretty much went with the same approach as in C#: convert to int, sort, skip 3, then sum:

let scores = "10,5,0,8,10,1,4,0,10,1"
scores.Split(',')
    |> Seq.map int
    |> Seq.sort
    |> Seq.skip 3
    |> Seq.sum
    |> DumpAs "Q1"

Problem 2 – Bishop Moves

My first attempt at this one used Seq.collect, which is the F# equivalent of LINQ’s SelectMany.

seq { 'A' ..'H' }
    |> Seq.collect (fun a -> seq { 1 .. 8 } |> Seq.map (fun x -> sprintf "%c%d" a x))
    |> Seq.filter (fun p -> abs (int p.[0] - int 'C') = abs (int p.[1] - int '6'))
    |> Seq.filter (fun p -> p <> "C6")
    |> String.concat ","
    |> DumpAs "Q2"

But in the C# solution we saw how the LINQ Query Expression Syntax makes for a cleaner way of expressing the problem that doesn’t repeat the starting position. And it turns out that we can create a very similar solution in F#:

seq {
  for r in 1 .. 8 do
  for c in 'a' .. 'h' do
  let dx = abs (r - 6)
  let dy = abs (int c - int 'c') 
  if dx <> 0 && dx = dy
  then yield sprintf "%c%d" c r
}
|> DumpAs "Q2"

Problem 3 – Sampling

This was one where F# surprisingly lacks the method that made it so easy in C#. F~ has a mapi function which gives an index of each item, but there is no corresponding filteri function (feature request here) . So I ended up using a mapi to create a sequence of tuples, which I could then filter and map back to a string.

let samples = "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"
samples.Split(',')
    |> Seq.mapi (fun a b -> a,(int b))
    |> Seq.filter (fun x -> (fst x + 1) % 5 = 0)
    |> Seq.map (snd >> (sprintf "%d"))
    |> String.concat ","
    |> DumpAs "Q3"

Sehnsucht provided an interesting alternative approach, making use of Seq.chunkBySize:

"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(',')
    |> Seq.chunkBySize 5
    |> Seq.map (Array.item 4)
    |> DumpAs "Q3"

Problem 4 – Vote Winning Margin

This one was quite easy, as the sumBy function lets us use the same technique as with C# and turn yes into +1 and no into –1 and sum:

"Yes,Yes,No,Yes,No,Yes,No,No,No,Yes,Yes,Yes,Yes,No,Yes,No,No,Yes,Yes"
    .Split(',')
    |> Seq.sumBy (fun x -> if x = "Yes" then 1 else -1)
    |> DumpAs "Q4"

But again Sehnsucht pointed me in the direction of an alternative solution, and that is to use the function keyword (not the same as the fun keyword) which allows you to use pattern matching in the function definition. What I hadn’t realised was that you are not forced to match all possible inputs, and so you can do something like this:

"Yes,Yes,No,Yes,No,Yes,No,No,No,Yes,Yes,Yes,Yes,No,Yes,No,No,Yes,Yes"
    .Split(',')
    |> Seq.sumBy (function "Yes" -> 1 | "No" -> -1)
    |> DumpAs "Q4"  

Problem 5 – Counting Pets

In C# we used GroupBy, but noted that a better solution would be if there was a method called countBy. And guess what, F# has one! And we can use the function keyword we just learned about to specify that we want to count dogs and cats separately but batch all other pets together. And I also learned another new F# function, the ‘destructuring’ operator ||>, letting me pass a tuple to sprintf without needing to use fst and snd explicitly.

"Dog,Cat,Rabbit,Dog,Dog,Lizard,Cat,Cat,Dog,Rabbit,Guinea Pig,Dog"
    .Split(',')
    |> Seq.countBy (function x when x = "Dog" || x = "Cat" -> x | _ -> "Other")
    |> Seq.map (fun x -> x ||> sprintf "%s:%d")
    |> String.concat ","
    |> DumpAs "Q5b"

I have a feeling the map could be cleaned up even further with the composition operator >> but I couldn’t come up with the right syntax.

Problem 6 – Run Length Decoding

Finally, for this problem I took a very similar approach to C#, taking advantage of Regex.Matches and Cast<Match> to get started.

Regex.Matches("A5B10CD3",@"[A-Z]\d*")
    .Cast<Match>()
    |> Seq.map (fun m -> m.Value)
    |> Seq.map (fun m -> new string(m.[0], if m.Length = 1 then 1 else int (m.Substring(1))))
    |> String.concat ""
    |> DumpAs "Q6"

Of course, one of the things that F# makes really easy is to create little helper functions that you can use to simplify your code, in the same way that you might declare intermediate variables in C# to make your code more readable.

So for example we might create a repeats function to decide how many times a letter should be repeated, a repeat function to construct the string with that many repeats, and a matches function to simplify performing the regex. It doesn’t make the solution shorter, but I think this style of coding does aid readability:

let repeats (s:string) =
    match s.Substring(1) with | "" -> 1 | n -> int n

let repeat c n = new string(c, n)

let matches input regex = 
    Regex.Matches(input,regex).Cast<Match>() 
        |> Seq.map (fun m -> m.Value)
    
matches "A5B10CD3" @"[A-Z]\d*"
    |> Seq.map (fun m -> repeat m.[0] (repeats m))
    |> String.concat ""
    |> DumpAs "Q6"

As always, I’d love to hear from any F# gurus in the comments if there are some cool tricks and techniques I’ve missed.

Want to learn more about LINQ? Be sure to check out my Pluralsight course More Effective LINQ.
Vote on HN
comments powered by Disqus