From numbers to letters

By: Dylan Thomas

Abstract: A recursive implementation of a numbers-to-letters function for the English language.

In the world of commercial application programming, the need for a function that can return the written equivalent of a number might be just around the corner at any given time. This function is often needed when implementing a checkbook program. For example, here in Guatemala all invoices have to provide the total amount of the bill, in text, at the bottom.

When I first found I needed such a function I started browsing through my old 3.5" 1.44 MB discs from school. What I found frightened me. The function was difficult to understand and very long. One thing I remembered was that my programming teacher was impressed because he had never seen a recursive implementation of this routine, so I started thinking of ways to simplify the code.

The code

I started by declaring arrays of constants that could form the basis of the patterns involved in naming numbers. First of course we have one, two,..fifteen. One could concatenate six and teen to get number 16 -- and do the same with 17, 18 and 19 -- but I found this only makes the code more complicated and less readable. So...

type
  TNumberStr = string[13];

const
  Numbers: array[1..19] of TNumberStr = ('one', 'two', 'three', 'four',
    'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve',
    'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen',
    'nineteen');
 
  Tenths: array[1..9] of TNumberStr = ('ten', 'twenty', 'thirty', 'forty',
    'fifty', 'sixty', 'seventy', 'eighty', 'ninety');

Those are almost all of the strings we need to construct our result. Next we need some constants to check that the parameter passed is between the valid range:

 ErrorString = 'not in valid range';
 Min = 1.00;
 Max = 4294967295.99;

We also need a wrapper function to validate the parameter passed, add optional formatting for the final string, and call the recursive function:

function NumToLetters(Number: Real): string;
begin
  if (Number >= Min) and (Number <= Max) then
    begin
      Result := RecurseNumber(Round(Int(Number)));
      {Added for cents in a currency value}
      if not(Frac(Number) = 0.00) then
        Result := Result +  ' and ' + IntToStr(Round(Frac(Number) * 100)) + '/100';
    end
  else
    raise ERangeError.CreateFmt('%g ' + ErrorString + ' %g..%g',
      [Number, Min, Max]);
end;{NumToLetters}

If you want to handle zero, negative numbers, or anything else you should do it at this level and not in the recursive function itself. Now the fun part.

function RecurseNumber(N: LongWord): string;
begin
  case N of
    1..19:
     Result := Numbers[N];
    20..99:
      Result := Tenths[N div 10] + ' ' + RecurseNumber(N mod 10)
    100..999:
      Result := Numbers[N div 100] + ' hundred ' + RecurseNumber(N mod 100);
    1000..999999:
      Result := RecurseNumber(N div 1000) + ' thousand ' +
        RecurseNumber(N mod 1000);
    1000000..999999999:
      Result := RecurseNumber(N div 1000000) + ' million '
        + RecurseNumber(N mod 1000000);
    1000000000..4294967295: 
      Result := RecurseNumber(N div 1000000000) + ' billion ' + 
        RecurseNumber(N mod 1000000000);
 end;{Case N of}
end;{RecurseNumber}

As you can see, the function doesnt use any string-handling routines. It relies only on operations like div, mod, and string concatenation. Also, the case statement can be expanded to widen the range of accepted numbers. Just remember to change the type in the function declaration accordingly.

I havent tested this function thoroughly and I havent done any benchmarking so this could be a case where, even if the code looks faster, that is not necessarily the case, much like the IntPower function where the recursive code might look nicer but is slower than the iterative code. If you find out, e-mail me at dylan@intelnet.net.gt

A unit containing this function and a demo program can be downloaded in Code Central ID: 15234.


Server Response from: ETNASC04