Following up on my initial post about the Bank OCR kata, this post walks through the code required to implement user story 2, which is about calculating a checksum.

The code I previously described already contains a function called ParseToDigits which returns a sequence of digits, so it seems reasonable to express the validation function as based on a sequence of digits as input.

The core unit test #

To ensure that the behavior of the new IsValid function would be correct, I wrote this Parameterized Test:

[<Theory>]
[<InlineData(1, 2, 3, 4, 5, 6, 7, 8, 9, true)>]
[<InlineData(3, 4, 5, 8, 8, 2, 8, 6, 5, true)>]
[<InlineData(3, 4, 5, 8, 8, 2, 8, 1, 4, true)>]
[<InlineData(7, 1, 5, 8, 8, 2, 8, 6, 4, true)>]
[<InlineData(7, 4, 5, 8, 8, 2, 8, 6, 5, false)>]
[<InlineData(7, 4, 5, 8, 8, 2, 8, 6, 9, false)>]
[<InlineData(7, 4, 5, 8, 8, 2, 8, 6, 8, false)>]
[<InlineData(1, 2, 3, 4, 5, 6, 7, 8, 8, false)>]
[<InlineData(1, 3, 3, 4, 5, 6, 7, 8, 8, false)>]
[<InlineData(1, 3, 3, 4, 5, 6, 7, 8, 0, false)>]
let IsValidReturnsCorrectResult d9 d8 d7 d6 d5 d4 d3 d2 d1 expected =
    seq { yield! [d9; d8; d7; d6; d5; d4; d3; d2; d1] }
    |> IsValid
    |> should equal expected

As was the case for the previous tests, this test utilizes xUnit.net's data theories feature to succinctly express a parameterized test, as well as FsUnit for the assertion DSL.

Once again I'd like to point out how the use of pipes enables me to very compactly follow the Arrange Act Assert (AAA) test pattern.

Implementation #

The IsValid function is very simple:

let IsValid digits =
    match digits |> Seq.toArray with
    | [| d9; d8; d7; d6; d5; d4; d3; d2; d1 |] ->
        (d1 + 2 * d2 + 3 * d3 + 4 * d4 + 5 * d5 + 6 * d6 + 7 * d7 + 8 * d8 + 9 * d9) % 11 = 0
    | _ -> false

The input sequence is converted to an array by piping it into the Seq.toArray function. The resulting array is then matched against an expected shape.

If the array contains exactly 9 elements, each element is decomposed into a named variable (d9, d8, etc.) and passed to the correct checksum formula. The formula only returns true if the calculated result is zero; otherwise it returns false.

If the input doesn't match the expected pattern, the return value is false. The following tests prove this:

[<Fact>]
let IsValidOfTooShortSequenceReturnsFalse() =
    seq { yield! [3; 4; 5; 8; 8; 2; 8; 6] }
    |> IsValid
    |> should equal false
 
[<Fact>]
let IsValidOfTooLongSequenceReturnsFalse() =
    seq { yield! [3; 4; 5; 8; 8; 2; 8; 6; 5; 7] }
    |> IsValid
    |> should equal false

In a future post I will walk you through user stories 3 and 4.


Comments

Why are you using `seq { yield! [d9; d8; d7; d6; d5; d4; d3; d2; d1] }` instead of just `[d9; d8; d7; d6; d5; d4; d3; d2; d1]`? It seems to me it's just a complicated way to write List.toSeq, which isn't even necessary here.
2012-06-01 23:45 UTC
That's a TDD artifact more than an actual requirement. I wrote the test first, and wanted it to specify that the IsValid function should be able to take any sequence, and not only a list.

Because of F# powerful type inference, that's what the function ends up doing anyway, but with TDD, it's the test that specifies the shape of the API, not the other way around.

However, I could also have written [d9; d8; d7; d6; d5; d4; d3; d2; d1] |> List.toSeq... There's no particular reason why I chose a sequence expression over that...
2012-06-02 10:13 UTC


Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or somewhere else with a permalink. Ping me with the link, and I may respond.

Published

Friday, 01 June 2012 05:54:27 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Friday, 01 June 2012 05:54:27 UTC