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. |] 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.