The Supermarket Pricing Kata in Haskell
Posted by Tom Moertel Fri, 28 Apr 2006 20:30:00 GMT
At last night’s meeting of the Pittsburgh Coding Dojo, we worked on the Supermarket Pricing Kata. This particular kata was intended to be food for thought – a “shower kata” – but our goal was to do some coding, so we made the problem more concrete:
- Come up with a sensible way to represent common supermarket pricing rules such as “buy one, get one free,” “three for a dollar,” ”$0.34 per ounce,” and so on
- Implement a method to check out a shopping cart full of goods, applying all applicable pricing rules, and computing the total price for the cart’s contents
Most people paired up, but I worked alone because I wasn’t ready to code right away. (Laptop issues.) At the end of the meeting, nobody had a working solution. (I guess it was a shower kata for a reason.) I had a partial solution, but I didn’t like my internal representation of prices because it conflated goods and their pricing rules.
Over lunch today I came up with a more sensible representation and finished off my implementation. Now I’m happy with it.
The code
Here’s my solution. I stripped the comments to emphasize the code itself (a forest and trees thing). If you want to see the comments, see the unstripped source.
{-
My solution to "The Supermarket Pricing Kata"
http://blogs.pragprog.com/cgi-bin/pragdave.cgi/Practices/Kata/KataOne.rdoc
Tom Moertel <tom@moertel.com>
2006-04-27
-}
module SupermarketPricing where
import Control.Arrow ((&&&))
import Data.List (groupBy, sort)
import Test.HUnit
type Portion = Double
type Count = Portion
type Price = Double
type Name = String
data PricingRule
= Per Portion Price
| For Count Price Price
deriving (Eq, Ord, Read, Show)
data Good
= G { name :: Name, quantity :: !Portion, rule :: PricingRule }
deriving (Eq, Ord, Read, Show)
per nm y x p = G nm y (Per x p)
each nm p = G nm 1 (For 1 p p)
for nm n p = G nm 1 (For n p p)
bogo = flip bngo 1
btgo = flip bngo 2
bngo nm n p = G nm 1 (For n' np p)
where (n', np) = (n + 1, p * n)
checkout :: [Good] -> Price
checkout =
checkoutBy $ sum . map price
subtotal :: [Good] -> [((Portion, Name), Price)]
subtotal =
checkoutBy $ map ((quantity &&& name) &&& price)
checkoutBy :: ([Good] -> a) -> [Good] -> a
checkoutBy f =
f . map (foldl1 combine) . groupByName . sort
where
groupByName = groupBy (\g1 g2 -> name g1 == name g2)
price :: Good -> Price
price (G nm y (Per x p)) = y * p / x
price (G nm m (For n p p2)) = (m - r) * p / n + r * p2
where
r = fromIntegral $ round m `rem` round n
combine :: Good -> Good -> Good
combine g1@(G nm x rule) g2@(G nm2 x2 rule2)
| nm /= nm2 || rule /= rule2
= error $ "can't combine incompatible goods " ++ show [g1, g2]
| otherwise
= G nm (x + x2) rule
Read on for an explanation of the code and my unit tests.
Read more...
readers

