Our community of experts have been thoroughly vetted for their expertise and industry experience. Experts with Gold status have received one of our highest-level Expert Awards, which recognize experts for their valuable contributions.
More often than not, we developers are confronted with a need: a need to make some kind of magic happen via code. Whether it is for a client, for the boss, or for our own personal projects, the need must be satisfied. Most of the time, the Framework satisfies such needs out of the box; sometimes, however, we must craft our own magical creation in order to satiate our need. To my knowledge, one such feature that is lacking from the Framework prior to removing the shrink wrap is that of converting a numeric value to a word representation. The uses for such a device are varied, and probably slightly niche, yet the need exists. In this article, I am going to demonstrate one approach to creating logic which will fulfill this need.
The code snippets in this article will be in C#, but I am including both a C# and VB.NET version of this code as attachments. You can find links to these attachments at the bottom of the article. My goal is that the article be clear enough for the novice of either language to follow along. Do not be shy about opening up the VB version if you have trouble following the code.
To make sure everyone is on the same page, let's demonstrate what I mean by "numbers to words". Let's say you have the following numeric value:
23,456,789
That is, twenty-three million four hundred fifty-six thousand seven hundred eighty-nine. So what does the conversion to words look like? Well, it is two sentences back! The need in question is taking the familiar Arabic numeral representation of a number and converting it to a series of words which evokes the same concept of quantity. Let me take a look at how the process could broken up.
Before I get into the guts of the logic, ask yourself this: what makes up a number? In other words, what are the parts that make a number--both the high-level and low-level parts. For this project, I looked at numbers in this manner: a number is set of digits, grouped by threes, going from right-to-left. You can see this partly in the number I mentioned earlier: the entire number is a set of digits, the digits are grouped into threes (delimited by commas), and the groups are packed from right-to-left since the left-hand side of the number only has two digits. Each group has common features that it shares with the other groups; each also has unique features. The common features include a spot for hundreds, tens, and ones positions. The unique feature each group has is that its multiplier increases by a factor of 1000 as you move from group to group, going from right-to-left. Taking all of this into account, developing a system of turning numbers into words should not be that difficult. Now that I have visualized the parts, let me conceptualize the code to manipulate those parts.
Getting the Text of a Number
Let me start off with a function called GetNumberText. I will use this function to return our newly created string. You'll most likely want to check for non-numbers before you get too far into the function call-wise, but for now, assume I have a valid number.
Note:I am using a regular expression here to remove the non-digit characters from the initial string. I am comfortable with regular expressions, so this is trivial for me. You can change this to some other logic if you prefer to avoid regular expressions, but the logic to evaluate the string depends on a string consisting of only digits, so ensure your string is composed as such.
In this case, the "\D" means anyhting that is NOT a digit--here, case is important ("\d" means any digit) and there are two backslashes due to C#'s handling of backslashes within strings. Since I am doing a Replace call, anything that matches the pattern will be replaced with the replacement value--in this case, an empty string. The net effect is that all non-digit characters are removed in the resulting string.
Earlier I suggested that a number is a set of groups (subsets). Our first step is to break the number into its respective groups. For example, I could generate an array of groups in this manner:
In the above, I am determining the number of groups using a combination of integer division and a modulus operation. The integer division gives the number of full groups (groups will be considered to always be of length 3, except possibly the first--left-most--group). The modulus operation tells whether or not a partial group exists. I piggy-back onto this operation and get the text for the partial group also. Once the number of groups is determined, I loop through the string and create each group as a separate slot in the returned array. The first slot in the array will hold the partial value if it exists; otherwise, it will be a full group. Once the array has been filled, I return back to GetNumberText.
Back within GetNumberText, I check to see how many groups were created, and based on that value, I make the appropriate calls to create each group's text:
if (parts.Length > 4) throw new System.OverflowException("Recieved value is too large.");
if (parts.Length > 3) GetGroup(parts[i++], BILLIONS, result);
if (parts.Length > 2) GetGroup(parts[i++], MILLIONS, result);
if (parts.Length > 1) GetGroup(parts[i++], THOUSAND, result);
GetGroup(parts[i], string.Empty, result);
As you can see, the higher the group count goes, the more "-illions" I process. If I wanted to add even higher values, then hopefully all I should have to do is change the overflow condition, and add a new if and constant for each -illion I wan to be able to process. All that is left for the GetNumberText function is to return the calculated value. "Where does that aforementioned 'magic' happen," you say? That, my friend, occurs inside the GetGroup method. Let us examine that method.
private static void GetGroup(string value, string denomination, StringBuilder result)
{
char[] reversed = Reverse(value);
if (value.Length > 2 && value[0] != ZERO)
{
result.Append(GetDigit(reversed[2]));
result.Append(HUNDREDS);
}
if (reversed.Length > 1)
{
if (reversed[1] > ONE)
{
result.Append(GetTens(reversed[1]));
if (reversed[0] != ZERO)
{
result.Append('-');
}
}
else if (reversed[1] == ONE && reversed[0] == ZERO)
{
result.Append(GetTens(reversed[1]));
}
else if (reversed[1] != ZERO)
{
result.Append(GetTeens(reversed[0]));
}
}
if (reversed.Length > 1)
{
if (reversed[0] > ZERO && reversed[1] != ONE)
{
result.Append(GetDigit(reversed[0]));
}
}
else
{
result.Append(GetDigit(reversed[0]));
}
result.Append(denomination);
}
In the GetGroup method, I start by reversing the order of the digits. This is only to make the subsequent if logic a bit simpler. I essentially check the length of the current group, and based on that length, I append the appropriate identifier. You will notice that the if for the hundreds is a bit shorter than the subsequent checks for the tens or the ones. This is because identifiers for the hundreds place are straightforward; the identifiers for tens and ones are a bit more complicated. If I have something in the tens position, I have to check whether or not I am dealing with one of the "-ty" values (e.g. twenty, thirty, etc.) or if I am dealing with a "-teen" value (e.g. thirteen, fourteen, etc.). Even though they do not end in "teen", I include eleven and twelve as "teen" values. Because of the irregularities which occur in the teens, namely eleven and twelve, our logic becomes a bit more involved.
To check the hundreds position, I need to ensure that I have three digits and that the value in that position is not zero. It is fine if the value is zero, but I do not want to append an identifier if it is. There is not a concept of "zero hundred" that I am familiar with.
Here is how our tens check (i.e. the second if block) breaks down. I start by checking that I have at least two digits in the incoming value. If so, I first look for a tens value that is greater than one. This tells me that I have one of the "-ty" values. I get request the word equivalent of this value, then I check whether or not the ones value is a zero. If it is, then our word is done; if it is not, then I add a hyphen to separate the tens and subsequent ones value. Now, if our tens value is not greater than one, I need to check whether it is exactly one--because then I have a teen value. If the value of the ones position is zero, then the value is ten exactly, and I get the word equivalent of it; otherwise, I get the word equivalent of a teen value. For this teen, I pass the value of the ones column, since I already know that the tens column is a one. Reaching the end of this block, however, does not indicate that I am finished, even if I acquired a teen value.
For the ones check, our third if, I need to confirm a couple of things:
1) Is the incoming value to this function a length greater than one
2) If the length is greater than one, is the value of the tens column not equal to one or is the value of the ones column greater than zero
The reasoning for the above logic is as follows. If the length of the incoming value is greater than one, I have a value for the tens column. As such, I need to check what kind of value, a "-ty" or a "-teen" value, was generated. If I generated a "-ty" value, then I have to make sure that I didnt' generated one of the multiples of ten (e.g. twenty, thirty, forty, etc.). If I did, then I won't be appending any words to the end of the value. There is no such value as "twenty-zero". If instead I generated a teen, I don't want to append a word either. Again, there is no such value as "nineteen-nine". If I can confirm, via the inner if that neither of these is the case, then I get the value of the ones column and append it to the result. That handles our ones column where I also have a tens column. For values where I only have a ones column (i.e. the incoming value is of length one), I simply call the function to return the word equivalent of the incoming value. This is handled by the else block.
As I mentioned earlier, the GetGroup method does much of the work in this process. The other functions it calls, GetDigit, GetTens, and GetTeens, are really just dumb functions that receive an incoming character and return its equivalent word, based on the context in which I choose a function and call it. Having these functions, however, gives me flexibility and code reuse, so I have not complaints to their dumbness.
Getting the Ordinal Text of a Number
I originally began thinking about this overall concept in response to a question I participated in. The question was actually requesting to return the ordinal text of a number. For those of you interested in this particular utility, the good news is that I did the majority of the work above. The only thing which needs to be adjusted is the suffix of the numbers generated by previously described logic. Before I cover that, let's refresh everyone's concept of what an ordinal number is.
At one point or another, we have all participated in some form of contest. Whether it be a spelling bee, a programming competition, or a scholastic examination. When you have the concept of a contest, you implicitly have the concept of a position in which each individual completes said contest. This is what an ordinal is: it describes the position of some thing in a given context. In the context of a competition, you have the first-place winner, the second-place winner, etc. The same holds true for other forms of contest. For a given number, to get its ordinal value a suffix is appended to the name. In some cases the name of the base number must be modified to some variant of the original (e.g. "twelve" becomes "twelfth"). Here is what the code to do so could look like.
As mentioned above, most of the hard work has been covered by the discussion above. And as I just informed you, to get an ordinal, I simply need to append a suffix, or I need to modify the base word appropriately for those special occurrences. For the purposes of ordinality, the suffixes involved will be "st", "nd", "rd", and "th". Most numbers belong to a group which uses the "th" suffix. Here is the function I concocted to adjust the phrase generated by GetNumberText to include the suffix:
public static string GetOrdinalText(string value)
{
if (value.Length == 0) return value;
StringBuilder result = new StringBuilder(GetNumberText(value));
if (value.Length == 1)
{
value = "x" + value;
}
if (result[result.Length - 1] == 'y')
{
result.Length--;
result.AppendFormat("ie{0}", GetSuffix(value[value.Length - 1]));
}
else if (value[value.Length - 1] == ONE && value[value.Length - 2] != ONE)
{
result.Length -= 3;
result.AppendFormat("Fir{0}", GetSuffix(value[value.Length - 1]));
}
else if (value[value.Length - 1] == TWO && value[value.Length - 2] != ONE)
{
result.Length -= 3;
result.AppendFormat("Seco{0}", GetSuffix(value[value.Length - 1]));
}
else if (value[value.Length - 1] == THREE && value[value.Length - 2] != ONE)
{
result.Length -= 5;
result.AppendFormat("Thi{0}", GetSuffix(value[value.Length - 1]));
}
else if (result[result.Length - 1] == 'e' || result[result.Length - 1] == 't')
{
result.Length--;
if (result[result.Length - 1] == 'v')
{
result[result.Length - 1] = 'f';
}
if (value[value.Length - 1] == TWO)
{
result.Append(GetSuffix(value[value.Length - 1], true));
}
else
{
result.Append(GetSuffix(value[value.Length - 1]));
}
}
else
{
if (value[value.Length - 2] == ONE)
{
result.Append(GetSuffix(value[value.Length - 1], true));
}
else
{
result.Append(GetSuffix(value[value.Length - 1]));
}
}
return result.ToString().Trim();
}
Firstly, I create the word equivalent of the number by making a call to the earlier described GetNumberText method. This is the base for our modifications. I will only be modifying the last word in the phrase, so a StringBuilder should grant an simple interface to do so. You'll notice I have included a small bit to append an "x" to the value. The "x" is really arbitrary, and the reason I included it was for times when a single value is being evaluated. Making the value be of length two rather than leaving it length one allowed me to escape a more complicated if block later. I will describe where this came into play soon. After this small adjustment, I start examining the last word in our phrase.
The first check comes in the form of looking for a trailing "y" on our word string. If I find such a character, then I have one of the multiples of ten. Multiples of ten use the "th" suffix, and they change their "y" to an "ie". Hence the first if block. If I do not find a "y" in the trailing position, I move on to the next condition.
The subsequent else if block checks the original numeric value for a trailing "1" that is not preceded by a "1". In other words, I am looking for a value that is not "11" in the right-most positions of the original numeric value. Here, and the following two else if blocks, is where the appending of the arbitrary value mentioned earlier comes into play. The first part of each of the three else if blocks will succeed no matter what, since I am guaranteed to have at least one character. If, however, I only have one character (sans append), then the second part of each else if would throw an index-out-of-bounds exception. So I sacrifice a call to append to save handling an exception. If both conditions match, then I shorten the length of the string by the length of the word that is at the end of the phrase--in this case, "One". I then append the equivalent ordinal phrase to the result--in this case "First". Notice that I am still calling the GetSuffix method in the else if body. I certainly could append the value "First" rather than append "Fir" followed by the call to GetSuffix, but I wanted to the logic similar throughout. This also grants me that if the suffixes ever change, I only need to change the GetSuffix method internally, and change the "Fir" if the base of the ordinal value changed. The two else if blocks following the first operate in the same manner, but evaluating two and three, respectively.
Since I mentioned it above, what's the deal with GetSuffix anyway? Well, it's another dumb function that just returns a suffix based on the incoming digit value. Here's what it looks like:
private static string GetSuffix(char value, bool treatAsTeen)
{
const string TH = "th";
switch (value)
{
case ONE:
return treatAsTeen ? TH : "st";
case TWO:
return treatAsTeen ? TH : "nd";
case THREE:
return treatAsTeen ? TH : "rd";
case FOUR:
case FIVE:
case SIX:
case SEVEN:
case EIGHT:
case NINE:
case ZERO:
return TH;
default:
return string.Empty;
}
}
private static string GetSuffix(char value)
{
return GetSuffix(value, false);
}
Yes, it is an overloaded function, and I will explain shortly why I overloaded it. As you can see, in the overload that does most of the work, I have a second parameter of type bool. The "treatAsTeen" variable is used to indicate when an incoming "1", "2", or "3" should be considered to represent eleven, twelve, or thirteen, respectively. The reason is that one has a different suffix than eleven does. The same is true for the other 4 numbers. Again, I tolerate this function's dumbness because it simplifies our goal. Now, back to parsing...
The else if block preceding the else block is used to check for words that end in either "e" or "t". The reason I do so is that these letters are dropped in the ordinal equivalent's representation. I also have an internal if block to check for a "v". For words that fit this characteristic, the "v" is changed to an "f". A word like "five" becomes "fifth" in the ordinal. The remaining if block checks for a ones value of "2" in the original numeric value. If I find a "2", then I must be working with the word "twelve", since the "e" check brought me into this else if. Since I am working with "twelve", I need to call the overloaded version of GetSuffix, telling it that I am passing it a "teen" word. This ensures I get the appropriate suffix for twelve. If I do not find a "2" in the ones position, then I can simply call GetSuffix with one parameter. This leaves me with only one case yet to examine.
In the final else block, I check whether or not the tens position is a one. Remember, I appended an arbitrary value if the original value was of length one, so I should not get an out-of-bounds exception here even if I do have but one number to process. If the value is one, then I must be working with a teen, and so I need to call the teen version of the GetSuffix method. Otherwise, I can simply call the single-parameter version and be done.
Summary
So there you have it: a few simple steps to creating a number-to-text converter. It was not that painful, was it? Again, this may not be something you could use every day, but if you ever need to simulate the legal lines of checks, spell out the positions of winners in your contests, or have some other creative use for displaying textual numbers, you now have a utility to do so. You could even modify the code to work in different languages, although you may need to adjust the way suffixes are applied. If you were able to use the code described here in your project, feel free to drop me a comment denoting your experience. I always enjoy knowing when my offerings were able to help someone out of a tight spot. You can even comment if you see an area of improvement in the code, although my focus was not strictly performance. For now, I say, "thanks for reading!"
Our community of experts have been thoroughly vetted for their expertise and industry experience. Experts with Gold status have received one of our highest-level Expert Awards, which recognize experts for their valuable contributions.
Our community of experts have been thoroughly vetted for their expertise and industry experience. Experts with Gold status have received one of our highest-level Expert Awards, which recognize experts for their valuable contributions.
Our community of experts have been thoroughly vetted for their expertise and industry experience. Experts with Gold status have received one of our highest-level Expert Awards, which recognize experts for their valuable contributions.
My thanks goes out to Qlemo, one of EE's illustrious PEs, who helped remind me that I shouldn't try to write code when I am sleep deprived. His suggestions helped clean up some of the logic I had originally posted. Vielen Dank!!!
Have a question about something in this article?
You can receive help directly from the article author.
Sign up for a free trial to get started.
Comments (2)
Commented:
Author
Commented: