3 Ways to Convert Numbers to Words in Clojure: A Performance Comparison
Tags: clojure, programming, performance, beginners
Published: 2023-01-11 19:56:18

Hey, dev.to community!

Today I want to share three different ways of converting a number (in the range of [0, 999,999,999,999]) to its English word form. For example, 34567 would become "thirty-four thousand five hundred sixty-seven" and 1001001 would become "one million one thousand one".

## First Implementation: `num-to-word-1`

```
(:require [clojure.pprint :refer [cl-format]]
            [clojure.string :as str])

(defn num-to-word-1 [n]
  (if (<= 0 n 999999999999)
    (str/replace
     (cl-format nil "~R" n)
     "," "")
    (throw (IllegalArgumentException.))))
```
It uses a built-in function `cl-format` which is a part of `clojure.pprint` library. Although it is the most readable and easy to implement (out of the three implementations discussed in this article), it is also the slowest. 

## Second Implementation: `num-to-word-2`

```
(def ^:private ones {\0 "" \1 "one", \2 "two", \3 "three", \4 "four", \5 "five", \6 "six",
           \7 "seven", \8 "eight", \9 "nine"})

(def ^:private tens {\0 "",
           \1 {\0 "ten" \1 "eleven", \2 "twelve", \3 "thirteen", \4 "fourteen",
               \5 "fifteen", \6 "sixteen", \7 "seventeen", \8 "eighteen", \9 "nineteen"},
           \2 "twenty", \3 "thirty", \4 "forty", \5 "fifty",
           \6 "sixty", \7 "seventy", \8 "eighty", \9 "ninety"})

(def ^:private hundreds {\0 "" \1 "one hundred", \2 "two hundred", \3 "three hundred", \4 "four hundred",
               \5 "five hundred", \6 "six hundred", \7 "seven hundred", \8 "eight hundred", \9 "nine hundred"})

(defn- ones-tens [o t]
  (if (= \1 t)
    [((tens \1) o)]
    (->> [(tens t) (ones o)]
         (remove #(or (nil? %) (empty? %)))
         (interpose "-")
         (apply str)
         vector)))

(defn- parse-item [item]
  (let [label (keys item)
        [o t h] (first (vals item))]
    (if (= \0 o t h)
      '()
      (concat label (ones-tens o t) [(hundreds h)]))))

(defn num-to-word-2 [n]
  (cond
    (= 0 n) "zero"
    (<= 1 n 999999999999)
    (let [scale ["" "thousand" "million" "billion"]
          nil-empty? #(or (nil? %) (empty? %))]
      (->> n str reverse
           (partition-all 3)
           (map hash-map scale)
           (reduce #(into %1 (parse-item %2)) '())
           (remove nil-empty?)
           (interpose " ")
           (apply str)))
    :else (throw (IllegalArgumentException. "Number out of range"))))

```
This implementation uses three separate maps of `ones`, `tens`, and `hundreds`. The idea is to a number `n`, convert it into a string, then break it into groups of three `(partition-all 3)`, attach a scale to each group `(map hash-map scale)`, iterate over the groups using `reduce` converting each group into words using helper functions `parse-item` and `ones-tens`, then finally combine and convert the result back into a string to form the final output.

## Third Implementation: `num-to-word-3`

```
(:require [clojure.string :as str])

(def ^:private lookup
  {0 "zero" 1 "one" 2 "two" 3 "three" 4 "four" 
   5 "five" 6 "six" 7 "seven" 8 "eight" 9 "nine" 
   
   10 "ten" 11 "eleven" 12 "twelve" 13 "thirteen" 14 "fourteen"
   15 "fifteen" 16 "sixteen" 17 "seventeen" 18 "eighteen" 
   19 "nineteen"
   
   20 "twenty" 30 "thirty" 40 "forty"50 "fifty" 
   60 "sixty" 70 "seventy" 80 "eighty" 90 "ninety"})

(def ^:private scale
  [[1000000000 "billion"]
   [1000000    "million"]
   [1000       "thousand"]
   [100        "hundred"]])

(defn- to-word [n]
  (cond (contains? lookup n) (lookup n)
        (< n 100) (let [r (rem n 10)]
                    (str (to-word (- n r)) "-" (to-word r)))
        :else
        (let [[[limit label]] (drop-while (fn [[l]] (< n l)) scale)
              [qs r] ((juxt (comp to-word quot) rem) n limit)
              rs (when-not (zero? r) (to-word r))]
          (str/trimr (str/join " " [qs label rs])))))

(defn num-to-word-3 [n]
  (if (not (<= 0 n 999999999999))
    (throw (IllegalArgumentException.))
    (to-word n)))
```
This implementation uses a single `lookup` map for the numbers 0-19 and {20 30 40 50 60 70 80 90} and a 2D vector `scale`. The function makes use of recursion. If the number is in `lookup`, the corresponding string is returned or else the function matches the number against `scale` to determine a divisor, use this divisor to extract digits (`quot` and `rem`), and recalls itself for the extracted digits.

## Comparison

Given below is the time taken by each of the three functions to convert 100,000 random numbers into words.
```
(time (dotimes [_ 100000]
        (num-to-word-1 (rand-int 1000000000))))
;; "Elapsed time: 1448.9357 msecs"
;; nil

(time (dotimes [_ 100000]
        (num-to-word-2 (rand-int 1000000000))))
;; "Elapsed time: 889.3392 msecs"
;; nil

(time (dotimes [_ 100000]
        (num-to-word-3 (rand-int 1000000000))))
;; "Elapsed time: 374.0896 msecs"
;; nil
```

Keep exploring, experimenting and testing different options. Happy coding!