5
\$\begingroup\$

Like many, I started out with procedural programming. Of course, when learning a functional language, old habits may die hard, so I wrote a fairly trivial little thing which takes an integer and returns a list of the english-language representation.

A few days later, I re-wrote it to try to take advantage of tail recursion in the main order-of-magnitude function; I freely admit that no attempt has been made to add tail-recursion to the calculation of numbers less than 1000, but since those have a fixed maximum depth of 3 (and a maximum of 5 calls in any case) I opted not to worry about it for now.

Any notes and criticisms, please lob them at me, that I may learn to do things in a less procedural way.

-module(titoa).
-export([itoa/1]).

itoa(0)                    -> "zero";
itoa(N) when is_float(N)   -> no_float_support;
itoa(N) when N < 0         -> "negative " ++ itoa(abs(N));
itoa(N) when is_integer(N) -> itoa_render(N);
itoa(_)                    -> severe_error.

itoa_render(N) when N >= 1100, N < 10000, N rem 100 == 0, N rem 1000 /= 0 ->
    itoa_render(N, ["", "hundred"], 100, []);
itoa_render(N) when N >= 1000 ->
    itoa_render(N,["",
        "thousand","million","billion","trillion","quadrillion","quintillion",
        "sextillion","septillion","octillion","nontillion","dectillion"
        ],1000, []
    );
itoa_render(N) when N >= 100, N rem 100 == 0 ->
    itoa_render(N div 100) ++ " hundred";
itoa_render(N) when N >= 100 ->
    Hun_diff = N rem 100,
    itoa_render(N - Hun_diff) ++ [32 | itoa_render(Hun_diff)];
itoa_render(N) when N > 19, N rem 10 == 0 ->
    lists:nth(N div 10 - 1, [
        "twenty","thirty","forty","fifty","sixty","seventy","eighty","ninety"
    ]);
itoa_render(N) when N > 19 ->
    Ten_diff = N rem 10,
    itoa_render(N - Ten_diff) ++ "-" ++ itoa_render(Ten_diff);
itoa_render(N) when N > 0 ->
    lists:nth(N, [
        "one","two","three","four","five","six","seven","eight","nine",
        "ten","eleven","twelve","thirteen","fourteen","fifteen","sixteen",
        "seventeen","eighteen","nineteen"
    ]);
itoa_render(_) -> []. % 0

itoa_render(0, _, _, After) -> After;
itoa_render(_, [], _, _) -> overflow;
itoa_render(N, [Magnitude | Remaining_Magnitudes], Factor, After) ->
    This_OOM = itoa_render(N rem Factor),
    This_Rep = if
        This_OOM  == [] -> [];
        Magnitude == [] -> This_OOM;
        true            -> This_OOM ++ [32 | Magnitude] ++ if
            After     == [] -> [];
            true            -> [32]
        end
    end,
    itoa_render(N div Factor, Remaining_Magnitudes, Factor, This_Rep ++ After).
\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

At first glance, you're trying to be a bit cute here and it's making your code more complex than necessary. For example, why not handle 11 and 12 as part of the teens and why not just type out the teen and decade names in full rather than having extra logic to append a not-quite common suffix ("teen" and "ty")?

For n < 1000 you have the following rules (let <n> mean "n in words", % mean modulo, and // mean integer division):

if 100 <= n then <n // 100> " hundred" ++ 
                 (if n % 100 == 0 then "" else " and " ++ <n % 100>)
if 20 <= n then  {"", "twenty", ..., "ninety"}[n // 10] ++ 
                 (if n % 10 == 0 then "" else " " ++ <n % 10>)
otherwise        {"zero", "one", "two", ..., "ten", "eleven", ..., "nineteen"}[n]

Now you only need to deal with the orders of magnitude ("thousand", "million", etc.).

\$\endgroup\$
5
  • 1
    \$\begingroup\$ I'll grant you the bit about the splitting of 12-19, and the "teen" and "ty". However, could you please help me understand why doing it with if clauses is more efficient than using Erlang's guard syntax, and furthermore, what about the tail recursion -- did I implement it correctly or will it not work? \$\endgroup\$
    – Dereleased
    Commented Oct 3, 2011 at 6:18
  • \$\begingroup\$ Just three quick points. First, there should be no efficiency difference between using if-then-elses or guards -- they're the same pig wearing different dresses. Second, where you write itoa_render(N - Hun_diff) ++ [32 | itoa_render(Hun_diff)];, you're clearly not being tail recursive since you have a recursive call in a constructor argument in a function argument. Third, you probably shouldn't worry about efficiency too much here since this algorithm is O(log n) anyway! My point was more about clarity. \$\endgroup\$
    – Rafe
    Commented Oct 3, 2011 at 22:40
  • \$\begingroup\$ And a valid point it is, clarity. I know the 0-999 calculators aren't tail recursive ("I freely admit that no attempt has been made to add tail-recursion to the calculation of numbers less than 1000"), but the concept of tail recursion is fairly new to me, so largely I just wanted to be sure that I had managed to get it going in that one place, or if I failed to grasp it and thus failed to implement it. I am going to rewrite it using explicit ifs and see if that increases clarity. \$\endgroup\$
    – Dereleased
    Commented Oct 3, 2011 at 23:27
  • \$\begingroup\$ Tail recursion typically looks something like this: f(x) = if isBaseCase(x) then a else f(g(x)). Anything else, such as f(x) = if isBaseCase(x) then a else h(f(g(x))) is not tail recursive. \$\endgroup\$
    – Rafe
    Commented Oct 4, 2011 at 1:19
  • \$\begingroup\$ I was going to post it, but I'm electing not to now; I must've done something wrong, because it seems larger and more convoluted. I suppose I have a fair bit more learning to do. \$\endgroup\$
    – Dereleased
    Commented Oct 4, 2011 at 1:56
1
\$\begingroup\$
  1. Defensive programming is not recommended in Erlang, which means you could/should remove the no_float_support and severe_error clause.
  2. Using lists:nth is generally bad practice since it has a O(n) complexity and this means using your list like an array. For fixed-size lists, use tuples instead. Here's an example:

    element(N div 10 - 1,
        {twenty","thirty","forty","fifty","sixty","seventy","eighty","ninety"}
    );
    
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.