I finally took the time to code some F# again. This time I tried out Roy Osherove's String Calculator kata.
This was the first time I did development on a Mac using Mono. The experience was surprisingly pain-free. The biggest problem I had was MonoDevelop hanging when debugging. I was afraid that I would have to setup external libraries by hand but much to my surprise getting NuGet working was really easy. This blog post helped me get started.
The tests are pretty simple. A new thing for me was testing for exceptions, took a while to find out how to do it. Example can be seen on line 22.
[<TestFixture>]
type ``Given adding strings`` () =
[<Test>]
member x.``Adding empty strings is zero``()=
Add "" |> should equal 0
[<Test>]
member x.``Single number returns itself``()=
Add "1" |> should equal 1
[<Test>]
member x.``two numbers are summed``()=
Add "1,2" |> should equal 3
[<Test>]
member x.``can sum any amount of numbers``()=
Add "1,2,3" |> should equal 6
Add "1,2,3,4" |> should equal 10
[<Test>]
member x.``numbers can be delimited with newline``()=
Add "1,2\n3" |> should equal 6
[<Test>]
member x.``numbers can be delimited with given delimiter``()=
Add "//;\n1;2" |> should equal 3
[<Test; ExpectedException(typeof<System.Exception>, ExpectedMessage="Negatives not allowed: -1")>]
member x.``negative numbers throw exception``()=
(Add "-1,2") |> ignore
[<Test; ExpectedException(typeof<System.Exception>, ExpectedMessage="Negatives not allowed: -1,-3")>]
member x.``all negatives listed on error message``()=
(Add "-1,2,-3") |> ignore
[<Test>]
member x.``ignore over 1000``()=
Add "1000,2" |> should equal 2
[<Test>]
member x.``delimiter can have any length``()=
Add "//[***]\n1***2***3" |> should equal 6
[<Test>]
member x.``can have several delimiters``()=
Add "//[*][%]\n1*2%3" |> should equal 6
[<Test>]
member x.``can have several delimiters of any length``()=
Add "//[***][%]\n1***2%3" |> should equal 6
Everything went fast and smooth until the part where delimiter can have different lengths. Here is where I started to pay the price of skipping refactoring in earlier stages and had to resort to some serious hacking to move on. This was the first version with all the tests passing.
let rec sumList(list, acc) =
match list with
| [] -> acc
| hd :: tl -> sumList(tl, System.Int32.Parse(hd) + acc)
let (|Delimiter|_|) (str: string) =
if str.StartsWith("//") then Some(str)
else None
let sumUsingDelimiter(x:string, delimiter:string) =
let split =
let temp = delimiter.ToCharArray()
List.ofArray(x.Split(temp))
|> List.map(fun x -> List.ofArray(x.Split[|'\n'|]))
|> List.collect(fun x -> x)
|> List.filter(fun x -> System.Int32.Parse(x) < 1000)
let negatives = split |> List.filter (fun str -> str.[0] = '-')
match negatives.Length with
| 0 -> sumList(split, 0)
| _ -> failwith ("Negatives not allowed: " + (negatives |> String.concat ","))
let parseDelimiter(x:string) =
if x.[2] = '[' then
let pattern="^\/\/(?<delimiters>.*?)\\n(?<valuesString>.*)"
let regexMatch = Regex.Match(x, pattern)
let delimiterPart = regexMatch.Groups.["delimiters"].Value // "
let delimiters = delimiterPart.Replace("][",",").Replace("]", "").Replace("[", "").Split[|','|]
let valuesString = regexMatch.Groups.["valuesString"].Value //"
let deli = List.ofArray delimiters
List.fold (fun (acc:string) item -> acc.Replace(item, ",")) valuesString deli
else
let delimeter = x.Substring(2,1)
let rest = x.Substring(4)
rest.Replace(delimeter, ",")
let AddString(x:string) =
match x with
| "" -> 0
| Delimiter x -> sumUsingDelimiter(parseDelimiter(x), ",")
| _ -> sumUsingDelimiter(x, ",")
It is filled with bad naming and functions that are too large.
I did some renaming and extracting stuff to descriptive functions. I got most of the nastiness contained in descriptive helper functions. This helped make the basic flow more readable.
let rec sumList(list, acc) =
match list with
| [] -> acc
| hd :: tl -> sumList(tl, hd + acc)
let (|Empty|HasDelimiters|Normal|) (str : string) =
if str="" then Empty else
if str.StartsWith("//") then HasDelimiters
else Normal
let split(str:string) =
List.ofArray(str.Split(','))
|> List.map(fun x -> List.ofArray(x.Split[|'\n'|]))
|> List.collect(fun x -> x)
let parseNumbers numbers = numbers |> List.map(fun x -> System.Int32.Parse(x))
let filterOverThousand numbers = numbers |> List.filter(fun x -> x < 1000)
let failForNegatives numbers =
let negatives = numbers |> List.filter (fun number -> number < 0)
if negatives.Length > 0 then
failwith ("Negatives not allowed: " + (negatives |> List.map(fun x -> x.ToString()) |> String.concat(",") ))
else numbers
let sumDelimited(delimited:string) =
let numbers =
delimited
|> split
|> parseNumbers
|> filterOverThousand
|> failForNegatives
sumList(numbers, 0)
let gatherDelimiters (x:string) =
x.Replace("][",",").Replace("]", "").Replace("[", "").Split[|','|] |> List.ofArray
let separateDelimitersAndValues (input : string) =
let pattern="^\/\/(?<delimiters>.*?)\\n(?<valuesString>.*)"
let regexMatch = Regex.Match(input, pattern)
let delimiters = gatherDelimiters regexMatch.Groups.["delimiters"].Value //"
let valuesString = regexMatch.Groups.["valuesString"].Value //"
(delimiters,valuesString)
let unifyDelimiters(input:string) =
if input.[2] = '[' then
let delimiters, values = separateDelimitersAndValues input
List.fold (fun (acc:string) item -> acc.Replace(item, ",")) values delimiters
else
let delimeter = input.Substring(2,1)
let rest = input.Substring(4)
rest.Replace(delimeter, ",")
let Add(input:string) =
match input with
| Empty -> 0
| HasDelimiters -> unifyDelimiters input |> sumDelimited
| _ -> sumDelimited input
New language feature I learned about doing this kata was Active Patterns. It can be seen on line 6. It can be used for clearer pattern matching like seen on the Add-function starting at line 54.
Software professional with a passion for quality. Likes TDD and working in agile teams. Has worked with wide-range of technologies, backend and frontend, from C++ to Javascript. Currently very interested in functional programming.