August 16, 2018
Exercism - Say
Given a number from
0
to999,999,999,999
, spell out that number in English.
Source: A variation on JavaRanch CattleDrive, exercise 4a.
There is already a function called cl-format
from Common Lisp that has an implementation in the pprint
library:
(ns say
(:require [cljs.test :refer-macros [deftest is run-tests]]
[cljs.pprint :refer [cl-format]]))
(cl-format nil "~R" 123456789012345678901234567890)
We can check its source and see how it works.
Obviously the program needs a representation of the english number spellings. We can use a vector for that:
(def english-cardinal-units
["zero" "one" "two" "three" "four" "five" "six" "seven" "eight" "nine"
"ten" "eleven" "twelve" "thirteen" "fourteen"
"fifteen" "sixteen" "seventeen" "eighteen" "nineteen"])
And then we can extract a value at a given index:
(english-cardinal-units 1)
Now for the tens:
(def english-cardinal-tens
["" "" "twenty" "thirty" "forty" "fifty" "sixty" "seventy" "eighty" "ninety"])
And then we're gonna need the scale numbers:
(def english-scale-numbers
["" "thousand" "million" "billion" "trillion" "quadrillion" "quintillion"
"sextillion" "septillion" "octillion" "nonillion" "decillion"
"undecillion" "duodecillion" "tredecillion" "quattuordecillion"
"quindecillion" "sexdecillion" "septendecillion"
"octodecillion" "novemdecillion" "vigintillion"])
(defn add-english-scales
"Take a sequence of parts, add scale numbers (e.g., million) and combine into a string
offset is a factor of 10^3 to multiply by"
[parts offset]
(let [cnt (count parts)]
(loop [acc []
pos (dec cnt)
this (first parts)
remainder (next parts)]
(if (nil? remainder)
(str (apply str (interpose " " acc))
(if (and (not (empty? this)) (not (empty? acc))) " ")
this
(if (and (not (empty? this)) (pos? (+ pos offset)))
(str " " (nth english-scale-numbers (+ pos offset)))))
(recur
(if (empty? this)
acc
(conj acc (str this " " (nth english-scale-numbers (+ pos offset)))))
(dec pos)
(first remainder)
(next remainder))))))
(defn format-simple-cardinal
"Convert a number less than 1000 to a cardinal english string"
[num]
(let [hundreds (quot num 100)
tens (rem num 100)]
(str
(if (pos? hundreds) (str (nth english-cardinal-units hundreds) " hundred"))
(if (and (pos? hundreds) (pos? tens)) " ")
(if (pos? tens)
(if (< tens 20)
(nth english-cardinal-units tens)
(let [ten-digit (quot tens 10)
unit-digit (rem tens 10)]
(str
(if (pos? ten-digit) (nth english-cardinal-tens ten-digit))
(if (and (pos? ten-digit) (pos? unit-digit)) "-")
(if (pos? unit-digit) (nth english-cardinal-units unit-digit)))))))))
(defn- consume [func initial-context]
(loop [context initial-context
acc []]
(let [[result new-context] (apply func [context])]
(if (not result)
[acc new-context]
(recur new-context (conj acc result))))))
(defn remainders
"Return the list of remainders (essentially the 'digits') of val in the given base"
[base val]
(reverse
(first
(consume #(if (pos? %)
[(rem % base) (quot % base)]
[nil nil])
val))))
(defn number [n]
(if (= 0 n)
"zero"
(let [abs-arg (if (neg? n) (- n) n)
parts (remainders 1000 abs-arg)]
(let [parts-strs (map format-simple-cardinal parts)
full-str (add-english-scales parts-strs 0)]
(str (if (neg? n) "minus ") full-str)))))
Test suite
(deftest zero-test
(is (= "zero" (say/number 0))))
(deftest one-test
(is (= "one" (say/number 1))))
(deftest fourteen-test
(is (= "fourteen" (say/number 14))))
(deftest twenty-test
(is (= "twenty" (say/number 20))))
(deftest twenty-two-test
(is (= "twenty-two" (say/number 22))))
(deftest one-hundred-test
(is (= "one hundred" (say/number 100))))
(deftest one-hundred-twenty-three-test
(is (= "one hundred twenty-three" (say/number 123))))
(deftest one-thousand-test
(is (= "one thousand" (say/number 1000))))
(deftest one-thousand-two-hundred-thirty-four-test
(is (= "one thousand two hundred thirty-four" (say/number 1234))))
(deftest one-million-test
(is (= "one million" (say/number 1000000))))
(deftest one-million-two-thousand-three-hundred-forty-five-test
(is (= "one million two thousand three hundred forty-five" (say/number 1002345))))
(deftest one-billion-test
(is (= "one billion" (say/number 1000000000))))
(deftest a-big-number-test
(is (= "nine hundred eighty-seven billion six hundred fifty-four million three hundred twenty-one thousand one hundred twenty-three" (say/number 987654321123))))
(run-tests)