DEV Community

Harsh Srivastava
Harsh Srivastava

Posted on

3 Ways to Convert Numbers to Words in Clojure: A Performance Comparison

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.))))
Enter fullscreen mode Exit fullscreen mode

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"))))

Enter fullscreen mode Exit fullscreen mode

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)))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)