Introduction


Converting a decimal number into its textual representation is nothing new and the Internet is full of examples on ways to do so.  The basic idea is to create some kind of look up table for numbers and their respective words (e.g. 0 = "zero", 1 = "one", 2 = "two", etc.) and then provide a method which can take a decimal number like 1234.56 and output a string containing "one thousand two hundred and thirty-four point five six".

However, many of these examples are overly complex and rely heavily on string manipulation which slows down their execution.  This article will present a simple solution which relies on math and the capabilities of objects in the .Net Framework to provide good performance with a relatively small amount of code.

Designing the Class


For this solution, we'll create a sealed class called "NumericStrings" which will expose shared members for generating the strings and configuring their formatting.  The class will be sealed by declaring it NotInheritable and making the constructor Protected.  Users will never create an instance of this class, rather, they will simply utilize its shared fields and methods.

This example will be limited to handling numbers up to Integer.Max (2,147,483,647) but larger numbers could be supported by expanding the class to use the Long data type.

We'll begin by providing some configuration options for the user, such as the strings to use for separators, and declare the maximum supported decimal value:

Public NotInheritable Class NumericStrings
    Public Const MAX_DECIMAL_VALUE As Decimal = 2147483647.2147483647D
 
    Public Shared DecimalSeparator As String = System.Globalization.CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator
    Public Shared GroupSeparator As String = System.Globalization.CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator
    Public Shared SpaceString As String = " "
    Public Shared AndString As String = "and"
    Public Shared DashString As String = "-"
    Public Shared DecimalString As String = "point"
    Public Shared NegativeString As String = "negative"
 
    Protected Sub New()
    End Sub
End Class

 Instead of using an array or dictionary to store the root numbers and their names, we can create an Enum which is already designed to neatly provide a series of string identifiers each with an associated numeric value:

Public Enum RootNumbers
    zero
    one
    two
    three
    four
    five
    six
    seven
    eight
    nine
    ten
    eleven
    twelve
    thirteen
    fourteen
    fifteen
    sixteen
    seventeen
    eighteen
    nineteen
    twenty
    thirty = 30
    forty = 40
    fifty = 50
    sixty = 60
    seventy = 70
    eighty = 80
    ninety = 90
    hundred = 100
    thousand = 1000
    million = 1000000
    billion = 1000000000
End Enum

This provides us with all of the root number words required to build a string for any number, up to the maximum supported decimal value.  If we wanted to declare this Enum as type Long, we could support larger numbers in the algorithm that builds the word string.   Having stored all of the number words in an Enum, we'll now want to create a small helper method which allows us to retrieve a number word based on an integer value:

Public Shared Function GetRootNumberWord(ByVal number As Integer) As String
    Return [Enum].GetName(GetType(RootNumbers), number)
End Function

With the Enum and helper method in place, we can write the basic algorithm to build a word string for a given integer value.  First we'll look at the algorithm in whole, then break it down and explain its parts:

Public Shared Function GetNumberWords(ByVal number As Integer) As String
    If number = 0 Then Return GetRootNumberWord(0)
 
    If number < 0 Then
        Return NegativeString & SpaceString & GetNumberWords(System.Math.Abs(number))
    End If
 
    Dim result As New System.Text.StringBuilder
    Dim digitIndex As Integer = 9
    While digitIndex > 1
        Dim digitValue As Integer = CInt(10 ^ digitIndex)
        If number \ digitValue > 0 Then
            result.Append(GetNumberWords(number \ digitValue))
            result.Append(SpaceString)
            result.Append(GetRootNumberWord(digitValue))
            result.Append(SpaceString)
            number = number Mod digitValue
        End If
 
        If digitIndex = 9 Then
            digitIndex = 6
        ElseIf digitIndex = 6 Then
            digitIndex = 3
        ElseIf digitIndex = 3 Then
            digitIndex = 2
        Else
            digitIndex = 0
        End If
    End While
 
    If number > 0 Then
        If result.Length > 0 Then
            result.Append(AndString)
            result.Append(SpaceString)
        End If
 
        If number < 20 Then
            result.Append(GetRootNumberWord(number))
        Else
            result.Append(GetRootNumberWord((number \ 10) * 10))
            Dim modTen As Integer = number Mod 10
            If modTen > 0 Then
                result.Append(DashString)
                result.Append(GetRootNumberWord(modTen))
            End If
        End If
    End If
 
    Return result.ToString
End Function

The method begins by checking the trivial case that the number provided is zero and simply returns the root number word for zero if it is.  The method then checks to see if the number is negative, and if so, it creates a string containing the text used for negative numbers and then recalls itself passing the absolute value of the number.  Once executing with a positive value for number, the method creates a new StringBuilder to hold the resulting string and a temporary "digitIndex" variable to track the digit of the supplied number currently being analyzed.

If number = 0 Then Return GetRootNumberWord(0)
 
If number < 0 Then
    Return NegativeString & SpaceString & GetNumberWords(System.Math.Abs(number))
End If
 
Dim result As New System.Text.StringBuilder
Dim digitIndex As Integer = 9

The value of digitIndex begins at 9, which represents the theoretical most significant digit for the supported number range.  This would be the tenth digit in a number with ten digits, such as the max integer value 2,147,483,647.  The algorithm then loops until the digitIndex is less than 1, indicating that the entire number has been analyzed.  On each iteration of the loop, the digitValue is calculated as 10^digitIndex.  So on the first pass (digitIndex = 9), digitValue will equal 1,000,000,000.  Now if the number is greater than or equal to the digitValue we know that we need to get the number words for the billions portion of the number.  To do that we can make a recursive call to GetNumberWords and just pass the billions portion of the number; if we were using number = 2,147,483,647 then 2 would be passed in the recursive call.  With the number word returned we can add a space separator, get the root number word for the current digitValue (billion), add another space separator, and finally remove the analyzed digits from the number (drop the billions portion).

While digitIndex > 1
    Dim digitValue As Integer = CInt(10 ^ digitIndex)
    If number \ digitValue > 0 Then
        result.Append(GetNumberWords(number \ digitValue))
        result.Append(SpaceString)
        result.Append(GetRootNumberWord(digitValue))
        result.Append(SpaceString)
        number = number Mod digitValue
    End If

The final step of the loop is to reduce the digit index by three.  We are processing each hundreds portion of the number all at once so we only need to start the loop every three digits (plus the final hundreds value).  So the second iteration will have a digitIndex = 6 and digitValue of 10^6 or 1,000,000 meaning we are analyzing the millions portion of the number.  Now our number is 147,483,647 so GetNumberWords is called with (number \ digitValue) = 147 and GetRootNumberWord will return "million" for digitValue.

    If digitIndex = 9 Then
        digitIndex = 6
    ElseIf digitIndex = 6 Then
        digitIndex = 3
    ElseIf digitIndex = 3 Then
        digitIndex = 2
    Else
        digitIndex = 0
    End If
End While

Once the loop is complete, the algorithm checks to see if number is still a value above zero.  If so, it means that there is still an "...and something" to calculate on number.  For example, when GetNumberWords(147) was called previously, number would equal 47 at this point and the string "one hundred" would already be in the result with the words "and forty-seven" being added in the following lines of code.

If the result has text already then we need to add the "and" and space separators.  Then if the remaining number is under 20 we can just append the root number word for it.  Otherwise we need to get the tens-digit word and then append a dash separator and the ones-digit word if applicable.  Simple math allows us to isolate the desired digit in the remaining number.

If number > 0 Then
    If result.Length > 0 Then
        result.Append(AndString)
        result.Append(SpaceString)
    End If
 
    If number < 20 Then
        result.Append(GetRootNumberWord(number))
    Else
        result.Append(GetRootNumberWord((number \ 10) * 10))
        Dim modTen As Integer = number Mod 10
        If modTen > 0 Then
            result.Append(DashString)
            result.Append(GetRootNumberWord(modTen))
        End If
    End If
End If

This completes the method and the resulting string is returned to the caller.

Return result.ToString

Converting Decimal Values

To handle decimal values we'll want to split the number at the decimal point and use the functionality described to generate the text of the significant portion of the number, but we'll need to create a different routine for generating the fractional portion of the decimal number.  For this we'll want a method to simply return the root word for each digit of the number:

Public Shared Function GetDigitWords(ByVal number As Integer) As String
    Dim result As New System.Text.StringBuilder
    Dim digits() As Char = number.ToString.ToCharArray
    For Each digit As Char In digits
        If result.Length > 0 Then
            result.Append(SpaceString)
        End If
        result.Append(GetRootNumberWord(Val(digit)))
    Next
    Return result.ToString
End Function

This method is straight-forward and simply loops each character of the string representation of the number, getting the root number word for each character converted back into an integer.  This process is fast enough for a series of up to ten digits that the conversion overhead is minimal and so the problem is not worth addressing mathematically. 

With that method in place, we can now create the GetNumberWords implementation for a decimal value which combines the results of the two previous methods into a single output string:

Public Shared Function GetNumberWords(ByVal number As Decimal) As String
    If number > MAX_DECIMAL_VALUE Then Return "Overflow"
    Dim result As New System.Text.StringBuilder
    Dim parts() As String = number.ToString.Split({DecimalSeparator}, StringSplitOptions.None)
    result.Append(GetNumberWords(CInt(parts(0))))
    If parts.Length > 1 Then
        result.Append(SpaceString)
        result.Append(DecimalString)
        result.Append(SpaceString)
        Dim mantissaString As String = parts(1).TrimEnd("0"c)
        If mantissaString.Length > 9 Then mantissaString = mantissaString.Substring(0, 9)
        result.Append(GetDigitWords(CInt(mantissaString)))
    End If
    Return result.ToString
End Function

The method first checks the trivial case of a decimal value which is too large to parse.  It then creates a StringBuilder to hold the result and splits the number into two strings at the decimal point.  Finally, it proceeds to get the number words for the significant portion of the string and then, if there is a fractional portion, it adds the appropriate separators and gets the digit words for the value.  Again the code takes advantage of a little string manipulation because it is fast enough when used sparingly.

Usage Example

The following example code builds a simple GUI for converting a decimal number into words using the NumericStrings class shown above:

Public Class Form1
    Friend LayoutPanel As New FlowLayoutPanel With {.Dock = DockStyle.Fill}
    Friend WithEvents Button1 As New Button With {.Text = "Convert"}
    Friend TextBox1 As New TextBox
    Friend Label1 As New Label With {.AutoSize = True}
 
    Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
        Controls.Add(LayoutPanel)
        LayoutPanel.Controls.Add(Button1)
        LayoutPanel.Controls.Add(TextBox1)
        LayoutPanel.SetFlowBreak(TextBox1, True)
        LayoutPanel.Controls.Add(Label1)
    End Sub
 
    Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
        Dim number As Decimal
        If Decimal.TryParse(TextBox1.Text, number) Then
            Label1.Text = NumericStrings.GetNumberWords(number)
        Else
            Label1.Text = "Invalid Number"
        End If
    End Sub
End Class

Summary

By creating an Enum to hold the root number words and using simple math to process the digits of a number we can create an efficient class for converting numbers to their textual string representations. Further improvements could be made by expanding the class to support Long Integer values and further reducing string manipulation, however, the average decimal value should take less than 2 ms to convert with the existing design.  The design might also be altered to support instance members, allowing more than one style of formatting at the same time.

Appendix A: Complete Code Sample



Option Strict On
 
Imports System.ComponentModel
Imports System.Runtime.CompilerServices
 
''' <summary>
''' Converts Integer or Decimal numbers into their textual String representations (converts numbers to words).
''' </summary>
''' <remarks></remarks>
Public NotInheritable Class NumericStrings
    Public Const MAX_DECIMAL_VALUE As Decimal = 2147483647.2147483647D
 
    Public Shared DecimalSeparator As String = System.Globalization.CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator
    Public Shared GroupSeparator As String = System.Globalization.CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator
    Public Shared SpaceString As String = " "
    Public Shared AndString As String = "and"
    Public Shared DashString As String = "-"
    Public Shared DecimalString As String = "point"
    Public Shared NegativeString As String = "negative"
 
    Protected Sub New()
    End Sub
 
    Public Enum RootNumbers
        zero
        one
        two
        three
        four
        five
        six
        seven
        eight
        nine
        ten
        eleven
        twelve
        thirteen
        fourteen
        fifteen
        sixteen
        seventeen
        eighteen
        nineteen
        twenty
        thirty = 30
        forty = 40
        fifty = 50
        sixty = 60
        seventy = 70
        eighty = 80
        ninety = 90
        hundred = 100
        thousand = 1000
        million = 1000000
        billion = 1000000000
    End Enum
 
    <EditorBrowsable(EditorBrowsableState.Advanced)>
    Public Shared Function GetRootNumberWord(ByVal number As RootNumbers) As String
        Return [Enum].GetName(GetType(RootNumbers), number)
    End Function
 
    <EditorBrowsable(EditorBrowsableState.Advanced)>
    Public Shared Function GetRootNumberWord(ByVal number As Integer) As String
        Return [Enum].GetName(GetType(RootNumbers), number)
    End Function
 
    Public Shared Function GetDigitWords(ByVal number As Integer) As String
        Dim result As New System.Text.StringBuilder
        Dim digits() As Char = number.ToString.ToCharArray
        For Each digit As Char In digits
            If result.Length > 0 Then
                result.Append(SpaceString)
            End If
            result.Append(GetRootNumberWord(Val(digit)))
        Next
        Return result.ToString
    End Function
 
    Public Shared Function GetNumberWords(ByVal number As Decimal) As String
        If number > MAX_DECIMAL_VALUE Then Return "Overflow"
        Dim result As New System.Text.StringBuilder
        Dim parts() As String = number.ToString.Split({DecimalSeparator}, StringSplitOptions.None)
        result.Append(GetNumberWords(CInt(parts(0))))
        If parts.Length > 1 Then
            result.Append(SpaceString)
            result.Append(DecimalString)
            result.Append(SpaceString)
            Dim mantissaString As String = parts(1).TrimEnd("0"c)
            If mantissaString.Length > 9 Then mantissaString = mantissaString.Substring(0, 9)
            result.Append(GetDigitWords(CInt(mantissaString)))
        End If
        Return result.ToString
    End Function
 
    Public Shared Function GetNumberWords(ByVal number As Integer) As String
        If number = 0 Then Return GetRootNumberWord(0)
 
        If number < 0 Then
            Return NegativeString & SpaceString & GetNumberWords(System.Math.Abs(number))
        End If
 
        Dim result As New System.Text.StringBuilder
        Dim digitIndex As Integer = 9
        While digitIndex > 1
            Dim digitValue As Integer = CInt(10 ^ digitIndex)
            If number \ digitValue > 0 Then
                result.Append(GetNumberWords(number \ digitValue))
                result.Append(SpaceString)
                result.Append(GetRootNumberWord(digitValue))
                result.Append(SpaceString)
                number = number Mod digitValue
            End If
 
            If digitIndex = 9 Then
                digitIndex = 6
            ElseIf digitIndex = 6 Then
                digitIndex = 3
            ElseIf digitIndex = 3 Then
                digitIndex = 2
            Else
                digitIndex = 0
            End If
        End While
 
        If number > 0 Then
            If result.Length > 0 Then
                result.Append(AndString)
                result.Append(SpaceString)
            End If
 
            If number < 20 Then
                result.Append(GetRootNumberWord(number))
            Else
                result.Append(GetRootNumberWord((number \ 10) * 10))
                Dim modTen As Integer = number Mod 10
                If modTen > 0 Then
                    result.Append(DashString)
                    result.Append(GetRootNumberWord(modTen))
                End If
            End If
        End If
 
        Return result.ToString
    End Function
End Class