Click here to Skip to main content
15,867,686 members
Articles / Desktop Programming / WPF

Converting Numbers to Text in C#

Rate me:
Please Sign up or sign in to vote.
4.97/5 (16 votes)
9 Jan 2017CPOL4 min read 51.2K   1.9K   15   9
A simple program to translate numbers into their textual representation. (To turn 1013 into "one thousand, thirteen").

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:

screenshot

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.

XML
<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:

C#
public class MyDoubleConverter : IValueConverter
    {
        // Converts double to int
        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:

C#
public class MyIntToStringConverter : IValueConverter
    {
        // Converts int to textual representation
        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.

C#
    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));
            }
        }

        //assumes a number between 10 & 19
        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));
            }
        }

        //assumes a number between 20 and 99
        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: 

C#
        /// <summary>
        /// This is the primary conversion method which can convert any integer bigger than 99
        /// </summary>
        /// <param name="n">The numeric value of the integer to be translated ("textified")</param>
        /// <param name="baseNum">Represents the order of magnitude of the number (e.g., 100 or 1000 or 1e6, etc)</param>
        /// <param name="baseNumStr">The string representation of the base number (e.g. "hundred", "thousand", or "million", etc)</param>
        /// <returns>Textual representation of any integer</returns>
        private static string ConvertBigNumberToString(int n, int baseNum, string baseNumStr)
        {
            // special case: use commas to separate portions of the number, unless we are in the hundreds
            string separator = (baseNumStr != "hundred") ? ", " : " ";

            // Strategy: translate the first portion of the number, then recursively translate the remaining sections.
            // Step 1: strip off first portion, and convert it to string:
            int bigPart = (int)(Math.Floor((double)n / baseNum));
            string bigPartStr = ConvertNumberToString(bigPart) + " " + baseNumStr;
            // Step 2: check to see whether we're done:
            if (n % baseNum == 0) return bigPartStr;
            // Step 3: concatenate 1st part of string with recursively generated remainder:
            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:

C#
      //converts any number between 0 & INT_MAX (2,147,483,647)
        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");
            //if (n < 1e12)
            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)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
I graduated from Washington State University in Computer Science (undergrad + masters). I've spent over 10 years in software development for a National Laboratory, a startup engineering company, and a nuclear processing plant. This has provided me with a diverse set of experiences and skills. I have worked extensively with C++ (w/ MFC, STL, & Boost), C#, Python, and Qt. I'm also comfortable with VB.Net, and HTML/XML, as well as tools like Visual Studio, JIRA, and Subversion. I have a strong background in mathematics & graphics and have delved into graph & network theory, information visualization, data analytics, SCADA/HMIs, and artificial intelligence.

Also, I'm not quite as old as I look.

Comments and Discussions

 
PraiseRefreshing Pin
Roberta Mafessoni12-Jan-17 12:28
Roberta Mafessoni12-Jan-17 12:28 
QuestionCommunication is king Pin
Qwertie10-Jan-17 14:44
Qwertie10-Jan-17 14:44 
AnswerRe: Communication is king Pin
Richard MacCutchan10-Jun-17 21:50
mveRichard MacCutchan10-Jun-17 21:50 
QuestionTDD Pin
Сергій Ярошко10-Jan-17 6:20
professionalСергій Ярошко10-Jan-17 6:20 
QuestionLongest String Pin
Bassam Abdul-Baki10-Jan-17 4:53
professionalBassam Abdul-Baki10-Jan-17 4:53 
SuggestionSuggestion Pin
Jochen Arndt10-Jan-17 3:36
professionalJochen Arndt10-Jan-17 3:36 
You asked for suggestions in the lounge. So here is one.

Why not just use n / 10 instead of:
C#
int tensDigit = (int)( Math.Floor((double)n / 10.0));

BugLittle problem Pin
Сергій Ярошко9-Jan-17 8:15
professionalСергій Ярошко9-Jan-17 8:15 
GeneralRe: Little problem Pin
kdmote9-Jan-17 10:19
kdmote9-Jan-17 10:19 
QuestionConvertTeensToString Pin
ydude9-Jan-17 3:15
ydude9-Jan-17 3:15 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.