www.howardism.org
Babblings of an aging geek in love with the Absurd, his family, and his own hubris.... oh, and Lisp.

Coin Kata

Introduction

Many people have talked about the Coin Change Kata. The local PDX Clojerks had a swarm where you rewrote this same program repeatedly to make it better.

Martin Ordesky had the following Scala example of change counting in his online class, Functional Programming in Scala:

def countChange(money: Int, coins: List[Int]): Int = {
  if (money == 0)  // Is this a special case?
    0
  else
    changeAcc(money, coins.reverse)
}

def changeAcc(money: Int, coins: List[Int]): Int = {
  val availableCoins = coins.filter(_ <= money)

  if (money == 0)
    1 // Good path
  else if (money < 0)
    0
  else if (availableCoins.length == 0)
    0 // Bad path
  else
    changeAcc(money - coins.head, coins) + changeAcc(money, coins.tail)
}

Where you could call it with some of these test cases:

// example from instructions
assert(countChange(4,List(1,2)) === 3)

// sorted CHF
assert(countChange(300,List(5,10,20,50,100,200,500)) === 1022)

// no pennies
assert(countChange(301,List(5,10,20,50,100,200,500)) === 0)

// unsorted CHF
assert(countChange(300,List(500,5,50,100,20,200,10)) === 1022)

// no coins
assert(countChange(300,List()) === 0)

// no money
assert(countChange(0,List(500,5,50,100,20,200,10)) === 0)

Coin Kata Tests

While not always listed first, the tests were written first, and are stored in a test source code file next to coin-kata.clj.

Currently, they do not take advantage of any testing framework, and just make a bunch of assertions.

Port of Scala Version

The main functional interface for this is the count-change function which just calls an internal function, change-acc, which follows two coin change algorithms.

For instance, if we want change from 4¢, and we had only 1¢ and 2¢ coins, we could follow both paths shown in the following state diagram:

count-change.png

In Path 1, we lower the amount of money by the largest coin and try to find change from the remainder (with all available coins).

In Path 2, we try to find change from the same amount, but without the largest coin in the list.

Count Change Function

The count-change function has two parts, the first (at the end) sorts the coins from largest to smallest, and calls the inner function, change-acc.

After covering the end cases, tHe inner function simply counts the possibilities based on each of the two paths, and returns the sum.

(defn count-change
  "Returns the number of ways to make change for an amount with a list of coins."
  [money coins]

  (letfn [(change-acc [money coins]
            (let [available-coins (filter (fn [c] (<= c money)) coins)]
              (cond
               (= money 0) 1   ;; Happy Ending!
               (< money 0) 0
               (= (count available-coins) 0) 0
               :else
                 (+ (change-acc (- money (first coins)) coins)
                    (change-acc money (rest coins))))))]

    (if (= money 0)
      0
      (change-acc money (reverse (sort coins))))))

Sanity case, taken from Ordesky’s lecture, if you have coins for a 1¢ and a 2¢ value, you could make change for 4¢ in 3 different ways:

  • 1, 1, 1, 1
  • 1, 1, 2
  • 2, 2
(assert 3 (count-change 4 '(1 2)))

The base case, where we want change for nothing must be 0:

(assert 0 (count-change 0 '(1 5)))

The other base case is having an empty list of coins, which also much return 0:

(assert 0 (count-change 5 '()))

Here are the rest of the tests ported from the Scala work:

;; sorted CHF
(assert 1022 (count-change 300 '(5,10,20,50,100,200,500)))

;; no pennies
(assert 0 (count-change 301 '(5,10,20,50,100,200,500)))

;; unsorted CHF
(assert 1022 (count-change 300 '(500,5,50,100,20,200,10)))

While this does count the number of ways to make change, it doesn’t tell you what coins to use, and that is the focus of the Kata.

Initial Approach

Given a list of 1¢ and 2¢ coins, we expect to need both coins to make change for 3¢:

(assert '(2 1) (get-change 3 (list 1 2)))

Given the standard US coins, we expect 68¢ to need just about every coin as well:

(assert '(25 25 10 5 1 1 1) (get-change 68 '(1 5 10 25)))

This naive approach begins by sorting coins from highest to lowest, and then we use the top coin, and make change with the money left.

(defn get-change
  "Return a list of coins needed to make change for an amount with a list of coins."
  [money coins]

  (letfn [(get-change-acc [money coins]
            (let [available-coins (filter (fn [c] (<= c money)) coins)]
              (cond
               (= money 0) '()   ;; Happy Ending!
               (= (count available-coins) 0) '()
               :else
                 (let [biggest-coin (first available-coins)]
                   (cons biggest-coin
                         (get-change-acc (- money biggest-coin)
                                         available-coins))))))]

    (if (= money 0)
      '(0)
      (get-change-acc money (reverse (sort coins))))))

Why is this naive? Because it doesn’t produce the least amount of coins. Granted, this works out for US coins, but what if we wanted change for 80¢, but with had the following: 25¢, 20¢, and 1¢:

(get-change 80 '(1 20 25))

It thinks we need 8 coins, when we could have made change with 4 20¢ coins.

Better Approach

The next idea is to simply run the get-change function with subset collections of the coin list to see which is the smallest approach.

(defn get-better-change
  "Returns a smaller number of coins to make change"
  [money coins]
  (letfn [(get-all-change [money coins]
            (if (> (count coins) 0)
              (cons (get-change money coins)
                    (get-all-change money (rest coins)))))
          (smallest-list [list-a list-b]
            (if (< (count list-a) (count list-b))
              list-a
              list-b))]
    (reduce smallest-list (get-all-change money (reverse (sort coins))))))

This function now deals correctly with 80¢ change when our coins are 1¢, 20¢ and 25¢.

(assert '(20 20 20 20) (get-better-change 80 '(1 20 25)))

We should create a series of base-cases and sanity tests for our new function.

;; No money
(assert '() (get-better-change 0 '(1 5 10 25)))

;; No coins
(assert '() (get-better-change 100 '()))

;; No pennies
(assert '() (get-better-change 4 '(10 5)))

Technical Artifacts

This code (and the documentation) was created using org-mode and its Babel project and can both tangle the source code out to Clojure files as well as weave (generate) documents easier for human eyes.

This section is only if you’ve loaded the org file in your Emacs session. To build the source on a new system, first place the cursor over any of these properties, and hit: C-c C-c

Generate the human documentation by typing: C-c C-e b