Translating numbers into words with Elixir

By Eric Lathrop on

I had a bout of insomnia last night, and I imagined a little programming exercise to write a function that would take an integer and translate it to the english words for that number. I felt that Elixir's pattern matching and macros would make the resulting code small and beautiful.

Here's what I wanted to achieve:

iex> IntegerToEnglish.integer_to_english(12)
"twelve"
iex> IntegerToEnglish.integer_to_english(-3034)
"negative three thousand and thirty four"
iex> IntegerToEnglish.integer_to_english(3823404)
"three million, eight hundred twenty three thousand, four hundred four"

This is the code I came up with:

defmodule IntegerToEnglish do
  use IntegerToEnglish.Macros

  def integer_to_english(i) when i < 0, do: "negative " <> integer_to_english(i * -1)
  def integer_to_english(0), do: "zero"
  def integer_to_english(1), do: "one"
  def integer_to_english(2), do: "two"
  def integer_to_english(3), do: "three"
  def integer_to_english(4), do: "four"
  def integer_to_english(5), do: "five"
  def integer_to_english(6), do: "six"
  def integer_to_english(7), do: "seven"
  def integer_to_english(8), do: "eight"
  def integer_to_english(9), do: "nine"
  def integer_to_english(10), do: "ten"
  def integer_to_english(11), do: "eleven"
  def integer_to_english(12), do: "twelve"
  def integer_to_english(13), do: "thirteen"
  def integer_to_english(14), do: "fourteen"
  def integer_to_english(15), do: "fifteen"
  def integer_to_english(16), do: "sixteen"
  def integer_to_english(17), do: "seventeen"
  def integer_to_english(18), do: "eighteen"
  def integer_to_english(19), do: "nineteen"

  generate_clause("twenty", 20, 30)
  generate_clause("thirty", 30, 40)
  generate_clause("fourty", 40, 50)
  generate_clause("fifty", 50, 60)
  generate_clause("sixty", 60, 70)
  generate_clause("seventy", 70, 80)
  generate_clause("eighty", 80, 90)
  generate_clause("ninety", 90, 100)
  generate_clause("hundred", 100, 1000)
  generate_clause("thousand", 1000, 1_000_000)
  generate_clause("million", 1_000_000, 1_000_000_000)
  generate_clause("billion", 1_000_000_000, 1_000_000_000_000)
end

defmodule IntegerToEnglish.Macros do
  defmacro __using__(_opts) do
    quote do
      import IntegerToEnglish.Macros

      defp prefix(i, dividend) when i >= 100 do
        IntegerToEnglish.integer_to_english(dividend) <> " "
      end

      defp prefix(_i, _dividend), do: ""

      defp remainder(_i, 0), do: ""

      defp remainder(i, rem) when i >= 1000 and rem < 100,
        do: " and " <> IntegerToEnglish.integer_to_english(rem)

      defp remainder(i, rem) when i >= 1000, do: ", " <> IntegerToEnglish.integer_to_english(rem)
      defp remainder(_i, rem), do: " " <> IntegerToEnglish.integer_to_english(rem)
    end
  end

  defmacro generate_clause(name, min, max) do
    quote do
      def integer_to_english(i) when is_integer(i) and i >= unquote(min) and i < unquote(max) do
        dividend = Integer.floor_div(i, unquote(min))
        rem = i - dividend * unquote(min)

        prefix(i, dividend) <>
          unquote(name) <> remainder(i, rem)
      end
    end
  end
end

I used the __using__ macro to define private helper functions for the generate_clause macro because macros need to be defined in a separate file from the file that uses them, and if I used regular functions they would have to be public for generate_clause to access them.

I published the code on Hex and GitLab, including tests.

I think the code turned out fairly straightforward and readable, and I wonder how it might look in other languages.