exupero's blog

Calculator words

I gave my kids a couple of cheap, four-function calculators a while ago, and since I entertained myself in middle school spelling words with upside-down digits, I thought I'd create a series of operations to produce a word as a kind of mathematical shaggy dog story.

Digits map to upside-down letters like this:

(def digit->letter
  {0 \o
   1 \i
   2 \z
   3 \e
   4 \h
   5 \s
   6 \g
   7 \l
   8 \b})

I've omitted '9', which turns into an upper case 'G', since lower case 'g' is already provided by '6'.

To find words made up of these letters, we can construct a simple regex:

(def regex
  (let [letters (apply str (vals digit->letter))]
    (re-pattern (format "^[%s]{4,8}$" letters))))

I chose words between four and eight letters long because three-letter words aren't that impressive and the calculators show at most eight digits.

We can use this regex on /usr/share/dict/words or an equivalent system dictionary:

(-> (sh "cat" "/usr/share/dict/words")
  (->> (filter #(re-matches regex %)))
  (->> (sort-by count >))
  (->> (take 7)))

Here's a few of the more fun words:

(We can also pluralize, given that i, e, and s are all available.)

To create a list of calculator operations that construct a word, we first need to invert the mapping of numbers to letters:

(def letter->digit
  (clojure.set/map-invert digit->letter))
{\b 8, \e 3, \g 6, \h 4, \i 1, \l 7, \o 0, \s 5, \z 2}

For the digits to be in the right order when the calculator is turned upside-down, they need to occur in reverse order of their respective letters. Also, if the word ends in a zero we have to add a decimal for the zero to show at the start of the number:

(defn ->number [s]
  (let [digits (apply str (map letter->digit (reverse s)))]
    (if (= \0 (first digits))
      (BigDecimal. (str "0." (subs digits 1)))
      (BigDecimal. digits))))

Our calculator function will take a list of numbers or symbols like this:

[3 + 7 / 5]

The given sequence won't be used as a mathematical expression using infix notation. Rather it'll be the order of buttons to push on the calculator. In particular, it won't respect order of operations, meaning the above sequence will produce 2 instead of 4.4.

A calculator function will handle three cases: 1) storing a number, 2) storing an operation, and 3) evaluating the stored operation on a stored number and an incoming number:

(defn calculator [values]
      (fn [{:keys [number op]} v]
          (and number op (number? v))
          , {:number (eval (list op number v))
             :op nil}
          (and (nil? number) (number? v))
          , {:number (if (instance? BigDecimal v)
                       (BigDecimal. v))
             :op op}
          (symbol? v)
          , {:number number
             :op v}))
      {:number nil :op nil}

Breaking a number down into a series of calculations is up to personal taste, but after some fiddling I'm fairly satisfied with this:

(defn decompose
  ([x] (decompose x 1))
  ([x o]
     (< 0 x 1)
     , (let [p (long (Math/pow 10 (.scale x)))
             r (/ (int (* x p)) p)]
         (into (decompose (numerator r) o) ['/ (denominator r)]))
     (<= 1 x 20)
     , [x]
     (some #(zero? (mod x %)) (range 20 1 -1))
     , (let [n (some #(when (zero? (mod x %)) %) (range 20 1 -1))]
         (into (decompose (int (/ x n)) o) ['* n]))
     , (into (decompose (- x o) (+ o 2)) ['+ o]))))

decompose is a recursive function that returns numbers between 1 and 20 and tries to break down other numbers into multiple operations. For numbers larger than 20, it pulls out the largest factor between 1 and 20, then breaks down the result. For fractional numbers, it uses Clojure's ratios to create a (possibly) simplified numerator and denominator, going on to decompose the numerator. For all other numbers, it subtracts increasing odd numbers and recurses on the result. You could also throw in occasional subtraction and division.

For the word "hello", encoded as 0.7734, decompose produces the following operations:

(decompose (->number "hello"))
[20 + 3 * 4 * 14 + 1 * 3 / 5000]

which we can check with calculator:

(calculator (decompose (->number "hello")))

The trailing M denotes a Java BigDecimal, which I've used in the code above to avoid floating point imprecision. Displaying in the style of a calculator LCD:

Or, as it's meant to be read, upside-down:

A longer word like "eggshell" generates more operations:

(decompose (->number "eggshell"))
[5 * 10 * 13 + 11 * 8 + 9 * 2 + 7 * 3 * 16 + 5 * 4 * 19 + 3 * 2 + 1]