Haskell String Calculator Kata

Written by Liam McLennan

The Kata

string calculatorRoy Osherove’s string calculator kata is a very simple kata designed (I believe) to demonstrate the basic TDD workflow.

The basic idea is to write a program (function?) that sums the integers in a string. “1,2,3” -> 6.

Setting Up a Haskell Package for the Kata

I like to start by initialising an isolated package for each new project.

I used hsenv to create an isolated environment (similar to rbenv etc). Then added a cabal configuration file:

name:                stringcalc
version:             0.1.0.0
author:              Liam
build-type:          Simple
cabal-version:       >=1.8

library
  exposed-modules:     StringCalc
  -- other-modules:       
  hs-source-dirs:      src/
  build-depends:       base ==4.6.*

test-suite tests
  type:               exitcode-stdio-1.0
  hs-source-dirs:     test/
  main-is:            Spec.hs
  build-depends:      base ==4.6.*
                      , hspec
                      , stringcalc

This says that my project is library exporting one module, StringCalc, and that it has a test project in Spec.hs.

To get this to build / run I need to create the library src/StringCalc.hs:

module StringCalc where

and the test file test/Spec.hs (these tests use the hspec test library):

import Test.Hspec
import Test.QuickCheck
import Control.Exception (evaluate)
import StringCalc

main :: IO ()
main = hspec $ do

  describe "Example Spec" $ do
    it "should pass" $ do
      (0 :: Int) `shouldBe` (0 :: Int)

then build with cabal:

cabal configure --enable-tests
cabal install --enable-tests --only-dependencies
cabal build
cabal test

Implementing the Kata

Step 1: “”

The first thing to do is to get the program to handle the empty string input.

Start with the test:

main :: IO ()
main = hspec $ do

  describe "StringCalc" $ do
    it "should sum the empty string" $ do
      sumString "" `shouldBe` (0 :: Int)

and then the implementation:

module StringCalc (sumString) where

sumString :: String -> Int
sumString "" = 0

next deal with adding a single number:

describe "add single number" $ do
      it "should add positive number" $ do
        sumString "1" `shouldBe` (1 :: Int)

      it "should add negative number" $ do
        sumString "-2" `shouldBe` (-2 :: Int)
sumString :: String -> Int
sumString "" = 0
sumString s = sum $ map rInt (split s)
  where rInt :: String -> Int
        rInt s = read s

The next step is, “Allow the Add method to handle an unknown amount of numbers”:

describe "add two numbers" $ do
      describe "add 3 and 9" $ do
        it "should give 12" $ do
          sumString "3,9" `shouldBe` (12 :: Int)
module StringCalc (sumString) where

sumString :: String -> Int
sumString "" = 0
sumString s = sum $ map rInt (split s)
  where rInt :: String -> Int
        rInt s = read s

split :: String -> [String]
split "" = []
split (',':ss) = split ss
split s = [takeWhile isNotComma s :: String] ++ (split $ dropWhile isNotComma s)
  where isNotComma :: Char -> Bool
        isNotComma c = c /= ','

The sumString function now needs to be changed to use commas or newlines as the separator:

describe "with newline separators" $ do
  let input = "1\n2\n3"

  it "should give 6" $ do
    sumString input `shouldBe` (6::Int)
sumString :: String -> Int
sumString "" = 0
sumString s = sum $ map rInt (split s)
  where rInt :: String -> Int
        rInt s = read s

split :: String -> [String]
split "" = []
split (',':ss) = split ss
split ('\n':ss) = split ss
split s = [takeWhile isNotSeparator s :: String] ++ (split $ dropWhile isNotSeparator s)
  where isNotSeparator :: Char -> Bool
        isNotSeparator c = c /= ',' && c /= '\n'

The final requirement is to support different delimeters. Here it is, with some refactoring:

module StringCalc (sumString) where

import Data.List
import Data.List.Split

hasExplicitDelimeter :: String -> Bool
hasExplicitDelimeter ('/':'/':s:'\n':ss) = True
hasExplicitDelimeter _                   = False

trimmed :: String -> String
trimmed input | hasExplicitDelimeter input = drop 4 input
              | otherwise                  = input

sumString :: String -> Int
sumString "" = 0
sumString s = sum . map rInt $ splitOn [guessDelimeter s] (trimmed s)

  where rInt :: String -> Int
        rInt s = read s

        guessDelimeter :: String -> Char
        guessDelimeter input  | hasExplicitDelimeter input     = input !! 2
                              | "\n" `isInfixOf` trimmed input = '\n'
                              | otherwise                      = ','

UPDATE: Michael Feathers performs the string calculator kata and arrives at a similar solution, except that his is point free