2

String Calculator Kata in F# - happy end

 3 years ago
source link: https://blog.ciechowski.net/string-calculator-kata-in-f-happy-end
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

String Calculator Kata in F# - happy end

Our calculator works pretty well so far. We were also able to use a few functional techniques in the process. Railway programming and composition is one of them.

This is a second part of a series:

Today we are going to focus on different types of delimiters:

  • a delimiter can be of any length
  • user can pass multiple delimiters of any size as an input

We are going to write a bit more code to implement these cases. Furthermore, we will do some domain modeling.

As always, in the beginning, there is a test:

[<Fact>]
let ``Delimiter can be of any length`` =
    let result = Add "//[***]\n1***2***3"
    Assert.Equal(Ok 6, result)

The first thing we will do is introduce a new type called DelimiterType

type DelimiterType =
    | Default
    | OneCharacterLong
    | MoreThanOneCharacterLong

We model our delimiter in a more granular way and explicitly define all available options with this type. Which, in turn, improves our domain.

With the introduction of a different kind of separators, we have to refine our algorithm. Instead of only deciding if a custom delimiter was passed, we now have to recognize its type. We do that with a new function:

let findDelimiterType (numbers: Input) =
    match numbers.StartsWith "//" with
    | false -> Default
    | true ->
        match numbers.Contains "[" && numbers.Contains "]" with
        | false -> OneCharacterLong
        | true -> MoreThanOneCharacterLong

I'm not satisfied with this approach. This double pattern matching seems clumsy. But I don't know what to do with it, and it works. Refactoring remarks are more than welcome.

It also forces us to add a new case in both extractNumbers and extractDelimiter:

let extractNumbers (numbers: Input, delimiterType: DelimiterType) =
    match delimiterType with
    | Default -> numbers
    | OneCharacterLong -> numbers.[4..numbers.Length]
    | MoreThanOneCharacterLong ->
        let delimiterEnd = numbers.IndexOf "]" + 2
        numbers.[delimiterEnd..numbers.Length]

let extractDelimiter (numbers: Input, delimiterType: DelimiterType) =
    let defaultDelimiters = [| ","; "\n" |]

    match delimiterType with
    | Default -> defaultDelimiters
    | OneCharacterLong ->
        let customDelimiter = [| string numbers.[2] |]

        Array.concat [| defaultDelimiters
                        customDelimiter |]
    | MoreThanOneCharacterLong ->
        let delimiterStart = numbers.IndexOf "[" + 1
        let delimiterEnd = numbers.IndexOf "]" - 1

        let delimiter =
            [| numbers.[delimiterStart..delimiterEnd] |]

        Array.concat [| defaultDelimiters
                        delimiter |]

One more change in parseInput function, where we extract delimiter type:

let parseInput (input: Input): InputWithDelimiters =
    let delimiterType = findDelimiterType input
    let numbers = extractNumbers (input, delimiterType)
    let delimiters = extractDelimiter (input, delimiterType)

    (numbers, delimiters)

And all our tests pass!

There is only one more requirement left. It's passing multiple separators of any length at once. Translate this into a test:

[<Theory>]
[<InlineData("//[*][%]\n1*2%3", 6)>]
[<InlineData("//[!!][..]\n1!!2..3", 6)>]
let ``Multiple delimiters can be passed at once`` (input, expected) =
    let result = Add input
    Assert.Equal(Ok expected, result)

Similar to the previous step, we have to change extracting numbers and delimiters functions. Luckily, we don't have to change a lot of code. Working code looks in extracting delimiter looks like so:

| MoreThanOneCharacterLong ->
    let delimitersStart = numbers.IndexOf "["
    let delimitersEnd = numbers.IndexOf "\n" - 1
    let delimiters = numbers.[delimitersStart..delimitersEnd]

    let customDelimiters =
        delimiters.Split([| "["; "]" |], 
                          StringSplitOptions.RemoveEmptyEntries)

    Array.concat [| defaultDelimiters
                    customDelimiters |]

and extracting numbers:

 MoreThanOneCharacterLong ->
    let delimiterEnd = numbers.LastIndexOf "]" + 2
    numbers.[delimiterEnd..numbers.Length]

And that's all! String calculator kata in F# is done! Full code is on my github.

I have to say I've enjoyed it a lot. There were times when I was stuck even though this kata looks relatively simple, right? F# also seems like an interesting language. Not only because of a different paradigm but also for being succinct. I've also loved modeling with types. For a C# developer, it's just so easy.

The best part? It only made me more hungry! Expect more F# code from me. If you have any ideas about the next steps send me a tweet.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK