August 16, 2018

Exercism - Say

Given a number from 0 to 999,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))) " ")
             (if (and (not (empty? this)) (pos? (+ pos offset)))
               (str " " (nth english-scale-numbers (+ pos offset)))))
          (if (empty? this)
            (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"
  (let [hundreds (quot num 100)
        tens (rem num 100)]
      (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)]
              (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]
      (consume #(if (pos? %)
                  [(rem % base) (quot % base)]
                  [nil nil])

(defn number [n]
  (if (= 0 n)
    (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))))

Tags: coding exercises KLIPSE Exercism Clojure