Here is some .Net code to solve a real problem I had lately!
I had a case where I needed to convert a number into words for writing on a cheque (or a check if you prefer).
There are some algorithms on the Internet. Many have issues when come to the thousands. All of them had failed for my main requirement: being able to produce the words in French (in addition to English).
By chance, on the topic of writing a number in words, both English and French are somewhat similar in the building of the words except that the French language as a lot of little subtilties like adding s here and there and prefixing with un for numbers like 100 and 1000 (which are cent and mille).
Downloadable code
This month solution contains both VB and C# projects. The solution was created using Visual Studio 2017 RC but the code should work as well in older version of the.Net Framework.
A very simple user interface
As you can see in figure 1, only a data grid view control is shown on the screen. The reason is that the take away of this article is the class named Converter which does all the job.
Figure 1: The demo application in action
The Converter class
This is what you will want to copy to your own project!
This class takes care of all the conversion from a number to its words equivalent value in either English or French.
Really, the only method that needs to be public is the one named ConvertNumberToWords but because the demo solution also contains unit tests, all methods are made public.
You should never call any methods other than ConvertNumberToWords directly in your code. Just call this method passing your integer value you want to convert and the language and a string will be returned to you.
Option Strict On Public Enum Language English = 0 French = 1 End Enum Public Class Converter Public Shared Function ConvertNumberToWords(pValue As Integer, pLanguage As Language) As String Dim strReturn As String If pValue < 0 Then Throw New NotSupportedException("negative numbers not supported") ElseIf pValue = 0 Then strReturn = If(pLanguage = Language.English, "zero", "zéro") ElseIf pValue < 10 Then strReturn = ConvertDigitToWords(pValue, pLanguage) ElseIf pValue < 20 Then strReturn = ConvertTeensToWords(pValue, pLanguage) ElseIf pValue < 100 Then strReturn = ConvertHighTensToWords(pValue, pLanguage) ElseIf pValue < 1000 Then strReturn = ConvertBigNumberToWords(pValue, 100, "hundred", pLanguage) ElseIf pValue < 1000000 Then strReturn = ConvertBigNumberToWords(pValue, 1000, "thousand", pLanguage) ElseIf pValue < 1000000000 Then strReturn = ConvertBigNumberToWords(pValue, 1000000, "million", pLanguage) Else Throw New NotSupportedException("Number is too large!!!") End If If pLanguage = Language.French Then If strReturn.EndsWith("quatre-vingt") Then 'another French exception strReturn += "s" End If End If Return strReturn End Function Public Shared Function ConvertDigitToWords(pValue As Integer, pLanguage As Language) As String Select Case pValue Case 0 Return "" Case 1 Return If(pLanguage = Language.English, "one", "un") Case 2 Return If(pLanguage = Language.English, "two", "deux") Case 3 Return If(pLanguage = Language.English, "three", "trois") Case 4 Return If(pLanguage = Language.English, "four", "quatre") Case 5 Return If(pLanguage = Language.English, "five", "cinq") Case 6 Return "six" Case 7 Return If(pLanguage = Language.English, "seven", "sept") Case 8 Return If(pLanguage = Language.English, "eight", "huit") Case 9 Return If(pLanguage = Language.English, "nine", "neuf") Case Else Throw New IndexOutOfRangeException("{pValue} not a digit") End Select End Function 'assumes a number between 10 & 19 Public Shared Function ConvertTeensToWords(pValue As Integer, pLanguage As Language) As String Select Case pValue Case 10 Return If(pLanguage = Language.English, "ten", "dix") Case 11 Return If(pLanguage = Language.English, "eleven", "onze") Case 12 Return If(pLanguage = Language.English, "twelve", "douze") Case 13 Return If(pLanguage = Language.English, "thirteen", "treize") Case 14 Return If(pLanguage = Language.English, "fourteen", "quatorze") Case 15 Return If(pLanguage = Language.English, "fifteen", "quinze") Case 16 Return If(pLanguage = Language.English, "sixteen", "seize") Case 17 Return If(pLanguage = Language.English, "seventeen", "dix-sept") Case 18 Return If(pLanguage = Language.English, "eighteen", "dix-huit") Case 19 Return If(pLanguage = Language.English, "nineteen", "dix-neuf") Case Else Throw New IndexOutOfRangeException("{pValue} not a teen") End Select End Function 'assumes a number between 20 and 99 Public Shared Function ConvertHighTensToWords(pValue As Integer, pLanguage As Language) As String Dim tensDigit As Integer = CInt(Math.Floor(CDbl(pValue) / 10.0)) Dim tensStr As String Select Case tensDigit Case 2 tensStr = If(pLanguage = Language.English, "twenty", "vingt") Exit Select Case 3 tensStr = If(pLanguage = Language.English, "thirty", "trente") Exit Select Case 4 tensStr = If(pLanguage = Language.English, "forty", "quarante") Exit Select Case 5 tensStr = If(pLanguage = Language.English, "fifty", "cinquante") Exit Select Case 6 tensStr = If(pLanguage = Language.English, "sixty", "soixante") Exit Select Case 7 tensStr = If(pLanguage = Language.English, "seventy", "soixante-dix") Exit Select Case 8 tensStr = If(pLanguage = Language.English, "eighty", "quatre-vingt") Exit Select Case 9 tensStr = If(pLanguage = Language.English, "ninety", "quatre-vingt-dix") Exit Select Case Else Throw New IndexOutOfRangeException("{pValue} not in range 20-99") End Select If pValue Mod 10 = 0 Then Return tensStr 'French sometime has a prefix in front of 1 Dim strPrefix As String = String.Empty If pLanguage = Language.French AndAlso (tensDigit < 8) AndAlso (pValue - tensDigit * 10 = 1) Then strPrefix = "-et" End If Dim onesStr As String If pLanguage = Language.French AndAlso (tensDigit = 7 OrElse tensDigit = 9) Then tensStr = ConvertHighTensToWords(10 * (tensDigit - 1), pLanguage) onesStr = ConvertTeensToWords(10 + pValue - tensDigit * 10, pLanguage) Else onesStr = ConvertDigitToWords(pValue - tensDigit * 10, pLanguage) End If Return Convert.ToString((tensStr & strPrefix) + "-") & onesStr End Function ' Use this to convert any integer bigger than 99 Public Shared Function ConvertBigNumberToWords(pValue As Integer, baseNum As Integer, baseNumStr As String, pLanguage As Language) As String ' special case: use commas to separate portions of the number, unless we are in the hundreds Dim separator As String If pLanguage = Language.French Then separator = " " Else separator = If((baseNumStr <> "hundred"), ", ", " ") End If ' 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: Dim bigPart As Integer = CInt(Math.Floor(CDbl(pValue) / baseNum)) Dim bigPartStr As String If pLanguage = Language.French Then Dim baseNumStrFrench As String Select Case baseNumStr Case "hundred" baseNumStrFrench = "cent" Exit Select Case "thousand" baseNumStrFrench = "mille" Exit Select Case "million" baseNumStrFrench = "million" Exit Select Case "billion" baseNumStrFrench = "milliard" Exit Select Case Else baseNumStrFrench = "????" Exit Select End Select If bigPart = 1 AndAlso pValue < 1000000 Then bigPartStr = baseNumStrFrench Else bigPartStr = Convert.ToString(ConvertNumberToWords(bigPart, pLanguage) & Convert.ToString(" ")) & baseNumStrFrench End If Else bigPartStr = Convert.ToString(ConvertNumberToWords(bigPart, pLanguage) & Convert.ToString(" ")) & baseNumStr End If ' Step 2: check to see whether we're done: If pValue Mod baseNum = 0 Then If pLanguage = Language.French Then If bigPart > 1 Then 'in French, a s is required to cent/mille/million/milliard if there is a value in front but nothing after Return bigPartStr & Convert.ToString("s") Else Return bigPartStr End If Else Return bigPartStr End If End If ' Step 3: concatenate 1st part of string with recursively generated remainder: Dim restOfNumber As Integer = pValue - bigPart * baseNum Return Convert.ToString(bigPartStr & separator) & ConvertNumberToWords(restOfNumber, pLanguage) End Function End Class
using System; namespace DemoNumberToWords { public enum Language { English = 0, French = 1 } public class Converter { public static string ConvertNumberToWords(int pValue, Language pLanguage) { string strReturn; if (pValue < 0) throw new NotSupportedException("negative numbers not supported"); else if (pValue == 0) strReturn=pLanguage == Language.English ? "zero" : "zéro"; else if (pValue < 10) strReturn= ConvertDigitToWords(pValue, pLanguage); else if (pValue < 20) strReturn = ConvertTeensToWords(pValue, pLanguage); else if (pValue < 100) strReturn = ConvertHighTensToWords(pValue, pLanguage); else if (pValue < 1000) strReturn = ConvertBigNumberToWords(pValue, 100, "hundred", pLanguage); else if (pValue < 1000000) strReturn = ConvertBigNumberToWords(pValue, 1000, "thousand", pLanguage); else if (pValue < 1000000000) strReturn = ConvertBigNumberToWords(pValue, 1000000, "million", pLanguage); else throw new NotSupportedException("Number is too large!!!"); if (pLanguage == Language.French) { if (strReturn.EndsWith("quatre-vingt")) { //another French exception strReturn += "s"; } } return strReturn; } public static string ConvertDigitToWords(int pValue, Language pLanguage) { switch (pValue) { case 0: return ""; case 1: return pLanguage == Language.English ? "one" : "un"; case 2: return pLanguage == Language.English ? "two" : "deux"; case 3: return pLanguage == Language.English ? "three" : "trois"; case 4: return pLanguage == Language.English ? "four" : "quatre"; case 5: return pLanguage == Language.English ? "five" : "cinq"; case 6: return "six"; case 7: return pLanguage == Language.English ? "seven" : "sept"; case 8: return pLanguage == Language.English ? "eight" : "huit"; case 9: return pLanguage == Language.English ? "nine" : "neuf"; default: throw new IndexOutOfRangeException($"{pValue} not a digit"); } } //assumes a number between 10 & 19 public static string ConvertTeensToWords(int pValue, Language pLanguage) { switch (pValue) { case 10: return pLanguage == Language.English ? "ten" : "dix"; case 11: return pLanguage == Language.English ? "eleven" : "onze"; case 12: return pLanguage == Language.English ? "twelve" : "douze"; case 13: return pLanguage == Language.English ? "thirteen" : "treize"; case 14: return pLanguage == Language.English ? "fourteen" : "quatorze"; case 15: return pLanguage == Language.English ? "fifteen" : "quinze"; case 16: return pLanguage == Language.English ? "sixteen" : "seize"; case 17: return pLanguage == Language.English ? "seventeen" : "dix-sept"; case 18: return pLanguage == Language.English ? "eighteen" : "dix-huit"; case 19: return pLanguage == Language.English ? "nineteen" : "dix-neuf"; default: throw new IndexOutOfRangeException($"{pValue} not a teen"); } } //assumes a number between 20 and 99 public static string ConvertHighTensToWords(int pValue, Language pLanguage) { int tensDigit = (int)(Math.Floor((double)pValue / 10.0)); string tensStr; switch (tensDigit) { case 2: tensStr = pLanguage == Language.English ? "twenty" : "vingt"; break; case 3: tensStr = pLanguage == Language.English ? "thirty" : "trente"; break; case 4: tensStr = pLanguage == Language.English ? "forty" : "quarante"; break; case 5: tensStr = pLanguage == Language.English ? "fifty" : "cinquante"; break; case 6: tensStr = pLanguage == Language.English ? "sixty" : "soixante"; break; case 7: tensStr = pLanguage == Language.English ? "seventy" : "soixante-dix"; break; case 8: tensStr = pLanguage == Language.English ? "eighty" : "quatre-vingt"; break; case 9: tensStr = pLanguage == Language.English ? "ninety" : "quatre-vingt-dix"; break; default: throw new IndexOutOfRangeException($"{pValue} not in range 20-99"); } if (pValue % 10 == 0) return tensStr; //French sometime has a prefix in front of 1 string strPrefix = string.Empty; if (pLanguage == Language.French && (tensDigit < 8) && (pValue - tensDigit * 10 == 1)) strPrefix = "-et"; string onesStr; if (pLanguage == Language.French && (tensDigit == 7 || tensDigit == 9)) { tensStr = ConvertHighTensToWords(10 * (tensDigit - 1), pLanguage); onesStr = ConvertTeensToWords(10 + pValue - tensDigit * 10, pLanguage); } else onesStr = ConvertDigitToWords(pValue - tensDigit * 10, pLanguage); return tensStr + strPrefix + "-" + onesStr; } // Use this to convert any integer bigger than 99 public static string ConvertBigNumberToWords(int pValue, int baseNum, string baseNumStr, Language pLanguage) { // special case: use commas to separate portions of the number, unless we are in the hundreds string separator; if (pLanguage == Language.French) separator = " "; else 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)pValue / baseNum)); string bigPartStr; if (pLanguage == Language.French) { string baseNumStrFrench; switch (baseNumStr) { case "hundred": baseNumStrFrench = "cent"; break; case "thousand": baseNumStrFrench = "mille"; break; case "million": baseNumStrFrench = "million"; break; case "billion": baseNumStrFrench = "milliard"; break; default: baseNumStrFrench = "????"; break; } if (bigPart == 1 && pValue < 1000000) bigPartStr = baseNumStrFrench; else bigPartStr = ConvertNumberToWords(bigPart, pLanguage) + " " + baseNumStrFrench; } else bigPartStr = ConvertNumberToWords(bigPart, pLanguage) + " " + baseNumStr; // Step 2: check to see whether we're done: if (pValue % baseNum == 0) { if (pLanguage == Language.French) { if (bigPart > 1) { //in French, a s is required to cent/mille/million/milliard if there is a value in front but nothing after return bigPartStr + "s"; } else return bigPartStr; } else return bigPartStr; } // Step 3: concatenate 1st part of string with recursively generated remainder: int restOfNumber = pValue - bigPart * baseNum; return bigPartStr + separator + ConvertNumberToWords(restOfNumber, pLanguage); } } }
Building the test UI
To offer a visual test to my class, I have created a very simple which host a single control (a DataGridView) to show some converted values.
In the Shown event of the class, I create a list of Translation to hold the results that will be later shown in the data grid.
The Translation reads like this:
Public Class Translation Public Property Value As Integer Public Property English As String Public Property French As String End Class
public class Translation { public int Value { get; set; } public string English { get; set; } public string French { get; set; } }
The code then loops a number of times calling the ConvertNumberToWords method in both English and French. Once done, the result list is displayed. The whole code reads like this:
Private Sub Form1_Shown(sender As Object, e As EventArgs) Handles Me.Shown Dim results As New List(Of Translation) Dim values As Integer() = Enumerable.Range(0, 10001).ToArray() For Each v As Integer In values Dim t As New Translation() t.Value = v t.English = Converter.ConvertNumberToWords(v, Language.English) t.French = Converter.ConvertNumberToWords(v, Language.French) results.Add(t) Next dataGridView1.DataSource = results End Sub
private void Form1_Shown(object sender, EventArgs e) { List<Translation> results = new List<Translation>(); int[] values = Enumerable.Range(0, 10001).ToArray(); foreach (int v in values) { Translation t = new Translation(); t.Value = v; t.English = Converter.ConvertNumberToWords(v, Language.English); t.French= Converter.ConvertNumberToWords(v, Language.French); results.Add(t); } dataGridView1.DataSource = results; }
Unit testing
If you download the demo solution, you will also find unit test classes (one for each project). It was a lot easier to test using unit testing and ensuring that nothing else got broken after I made some modifications to my translation algorithm.
Conclusion
A useful class for you to bring to your project whenever you need to convert a number into words. Especially if you want the French value!