Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
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"
End If
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)
For i = 0 To mantissaString.Length - 1
If mantissaString(i) = "0"c Then
result.Append(GetDigitWords(0))
result.Append(" "c)
Else
Exit For
End If
Next
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.
Updated GetNumberWords Method
As pointed out by Cadius in the comments, there was a bug in the GetNumberWords method which did not take leading zeros in the decimal portion of a number into account. This bug has been corrected with an update to the article.
Appendix A: Complete Code Sample
''' <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"
End If
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)
For i = 0 To mantissaString.Length - 1
If mantissaString(i) = "0"c Then
result.Append(GetDigitWords(0))
result.Append(" "c)
Else
Exit For
End If
Next
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