Introduction
On a recent snowy afternoon I decided to give myself a little coding challenge. How hard would it be to "teach the computer" how to translate numbers (integers) into words? My ultimate goal was to create a simple WPF control with a scroll bar bound to a label that can range across all positive integers and a second label that would convert those numbers to their textual representation. I was pleased with the result and thought it would make a good article for my first submission to CodeProject.
Background
I wanted to use this excercise as an opportunity to dust off my skills in TDD, WPF and algorithm design. I built the algorithm from the ground up, using TDD as my guide. Once the converter was complete, I added a graphical front end using WPF & xaml. I separated all three modules into independent projects/assemblies to help preserve separation of concerns.
Using the code
When you build and run the code you will see this simple interface:
As I mentioned above, the code is separated into a number of independent modules:
1. The GUI (WPF/XAML)
The xaml for this project is rather basic.
<Window x:Class="GUI.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:GUI.Converters"
Title="Number Converter" Height="169" Width="657" WindowStartupLocation="CenterScreen">
<Window.Resources>
<c:MyDoubleConverter x:Key="myDoubleConverter"/>
<c:MyIntToStringConverter x:Key="myIntToStringConverter"/>
</Window.Resources>
<StackPanel>
<Label Content="Slide the ScrollBar back and forth and watch the numbers change:"/>
<ScrollBar Orientation="Horizontal" Height="40" Name="mySB" Maximum="2147483647" LargeChange="100" SmallChange="1"/>
<Label x:Name="labSbNumeric" Content="{Binding Path=Value,
Converter={StaticResource myDoubleConverter}, ElementName=mySB}"/>
<Label x:Name="labSbString" Content="{Binding Path=Content,
Converter={StaticResource myIntToStringConverter}, ElementName=labSbNumeric}"/>
</StackPanel>
</Window>
The StackPanel consists of a few small controls. First I added a simple horizontal scrollbar to use as the number-selecting control. Then, using basic databinding, I bound the first label to the value of the scrollbar, and a second label to the content of the first. Now, due to the fact that the default value-type of the scrollbar is double
, I needed to use a wpf-style converter to translate it into integer. In order to do so, I added a custom window resource object to point to the converter which was defined in a separate file (in the GUI.Converters
namespace). I used the same convention to convert the integers into text.
2. The Converters
Adhering to the convention of MVVM to avoid using the code-behind file, I placed the converter code in a file of its own. If this had been a larger scale project, I would have placed them in the ViewModel module, but for such a minimalistic application, I didn't bother.
The integer converter is trivial, being little more than basic boilerplate:
public class MyDoubleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
double v = (double)value;
return (int)v;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value;
}
}
Similarly, the int-to-text converter is fairly straight-forward as well, because all the computation is done in a separate library:
public class MyIntToStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
int v = (int)value;
return Converter.ConvertNumberToString(v);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value;
}
}
3. The Main Conversion Library
A separate library houses all the brains of the conversion functions. The library contains a static class (called Converter
) which, in turn, consists of a single public static method called ConvertNumberToString(int)
, along with several private helper methods.
The helper methods include three trivial hard-code mappings (which serve, essentially, as the "base cases" for the subsequent recursion.
private static string ConvertDigitToString(int i)
{
switch (i)
{
case 0: return "";
case 1: return "one";
case 2: return "two";
case 3: return "three";
case 4: return "four";
case 5: return "five";
case 6: return "six";
case 7: return "seven";
case 8: return "eight";
case 9: return "nine";
default:
throw new IndexOutOfRangeException(String.Format("{0} not a digit",i));
}
}
private static string ConvertTeensToString(int n)
{
switch (n)
{
case 10: return "ten";
case 11: return "eleven";
case 12: return "twelve";
case 13: return "thirteen";
case 14: return "fourteen";
case 15: return "fiveteen";
case 16: return "sixteen";
case 17: return "seventeen";
case 18: return "eighteen";
case 19: return "nineteen";
default:
throw new IndexOutOfRangeException(String.Format("{0} not a teen", n));
}
}
private static string ConvertHighTensToString(int n)
{
int tensDigit = (int)( Math.Floor((double)n / 10.0));
string tensStr;
switch (tensDigit)
{
case 2: tensStr = "twenty"; break;
case 3: tensStr = "thirty"; break;
case 4: tensStr = "forty"; break;
case 5: tensStr = "fifty"; break;
case 6: tensStr = "sixty"; break;
case 7: tensStr = "seventy"; break;
case 8: tensStr = "eighty"; break;
case 9: tensStr = "ninety"; break;
default:
throw new IndexOutOfRangeException(String.Format("{0} not in range 20-99", n));
}
if (n % 10 == 0) return tensStr;
string onesStr = ConvertDigitToString(n - tensDigit * 10);
return tensStr + "-" + onesStr;
}
Things get slightly more interesting with the larger numbers. For this, I created a single method that takes three arguments:
private static string ConvertBigNumberToString(int n, int baseNum, string baseNumStr)
{
string separator = (baseNumStr != "hundred") ? ", " : " ";
int bigPart = (int)(Math.Floor((double)n / baseNum));
string bigPartStr = ConvertNumberToString(bigPart) + " " + baseNumStr;
if (n % baseNum == 0) return bigPartStr;
int restOfNumber = n - bigPart * baseNum;
return bigPartStr + separator + ConvertNumberToString(restOfNumber);
}
The first argument is the number itself. The second argument (baseNum
) specifies how "big" the number is (whether it is in the 100s, 1000s, or 100000s, etc). The final argument (baseNumStr
) specifies the textual version of that order of magnitude (e.g. "hundred", "thousand", etc). This is clearly the trickiest part of the entire program. The inline comments explain the logic of each step. Perhaps the best way to understand it is to see how a number progresses through each step, with the following example:
Number to convert: 2056 (baseNum= 1000; baseNumStr="thousand")
After step 1:
bigPart =
2 056/1000 = 2
bigPartStr
= "2 thousand"
In step 3:
restOfNumber
= 2056 - (2*1000) = 56 <-- this is the "remainder" that is recursively converted to a string.
Finally, we are ready to look at the main public conversion method, which is essentially just a trivial mapping function that calls the other utility/helper methods as appropriate:
public static string ConvertNumberToString(int n)
{
if (n < 0)
throw new NotSupportedException("negative numbers not supported");
if (n == 0)
return "zero";
if (n < 10)
return ConvertDigitToString(n);
if (n < 20)
return ConvertTeensToString(n);
if (n < 100)
return ConvertHighTensToString(n);
if (n < 1000)
return ConvertBigNumberToString(n, (int)1e2, "hundred");
if (n < 1e6)
return ConvertBigNumberToString(n, (int)1e3, "thousand");
if (n < 1e9)
return ConvertBigNumberToString(n, (int)1e6, "million");
return ConvertBigNumberToString(n, (int)1e9, "billion");
}
So that's it. Pretty simple, really. But it was a great little exercise and a fun project for a snowy afternoon.
Points of Interest
The TDD stategy proved to be very helpful. I started by creating a test that validated the conversion of single digits, then proceeded to create tests for increasingly challenging conversions (teens, then high tens, then hundreds, thousands, etc). At each stage it became fairly obvious how to harness the code from the previous stage to build upon.
The final version of the primary conversion function (ConvertBigNumberToString
()) turned out fairly terse. It didn't originate that way. In fact, I started out with multiple methods (one for "hundreds", one for "thousands", one for "millions", and so on. You can still see some evidence of this a number of the (now commented-out) unit tests. Eventually I saw the pattern that was common to all of them and this enabled me to condense the alorithm into a single recursive method. I like the brevity of it, but I admit that the resulting code is somewhat difficult to understand.
Conclusion
I have always wanted to post an article to CodeProject, so I am happy to finally do so. I welcome the input of any of you veterans as to how I might improve the article (or the associated code).
Attached files:
- NumberConverter.exe.zip -- download, unzip and run
- NumberConverterToText.zip -- source code and project files (including unit tests)