How It Works:
Inspired by r/dailyprogrammer, the application uses regular expressions to group 1-3 digits at a time from right to left, each captured group is then converted to text.
Program Class
using System;
namespace CheckWriter
{
internal class Program
{
private static readonly decimal[] testNumbers =
{
333.88m,
742388.15m,
919616.12m,
12.11m,
2.0m
};
private static void Main(string[] args)
{
var writer = new CheckWriter();
foreach (var number in testNumbers)
{
Console.WriteLine(writer.BuildNumberTextOf(number));
}
}
}
}
CheckWriter Class
using System;
using System.Text;
using System.Text.RegularExpressions;
namespace CheckWriter
{
public class CheckWriter
{
// TODO: Can be modified to accept generated
// pattern and groups for even larger numbers.
private static readonly string[] groups =
{
"trillion",
"billion",
"million",
"thousand",
"hundred",
"cents"
};
private static readonly string pattern =
@"^(?<trillion>\d{1,3})?(?<billion>\d{1,3})?(?<million>\d{1,3})?(?<thousand>\d{1,3})?(?<hundred>\d{1,3})(\.(?<cents>\d{1,2}))?$";
private static readonly RegexOptions options = RegexOptions.RightToLeft;
private Regex regex;
public CheckWriter()
{
regex = new Regex(pattern, options);
}
public string BuildNumberTextOf(decimal value)
{
var match = regex.Match(Convert.ToString(value));
var sb = new StringBuilder();
foreach (var group in groups)
{
var capturedValue = match.Groups[group].Value.TrimStart(new char[] { '0' });
// Dollars wont be appended to result if the
// hundred group contains all zeros.
if (capturedValue == "" && group == "hundred")
{
sb.Append("dollars ");
continue;
}
else if (capturedValue == "")
{
continue;
}
else
{
var words = HundredsToWords(Convert.ToInt32(capturedValue));
// Make sure "hundred" doesnt double up since
// "hundred" is also appended in the HundredsToWords
// function.
if (group != "hundred")
{
var cents = group == "cents" ? "and " : "";
sb.Append($"{cents}{words} {group} ");
}
else
{
sb.Append($"{words} dollars ");
}
}
}
return sb.ToString().TrimEnd();
}
private string HundredsToWords(int number)
{
if (number > 999) throw new ArgumentOutOfRangeException("Passed value too large.");
var sb = new StringBuilder();
if ((number / 100) > 0)
{
sb.Append(HundredsToWords(number / 100) + " hundred ");
number %= 100;
}
if (number > 0)
{
var units = new[] { "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen" };
var tens = new[] { "zero", "ten", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety" };
if (number < 20)
{
sb.Append(units[number]);
}
else
{
sb.Append(tens[number / 10]);
if ((number % 10) > 0)
{
sb.Append("-" + units[number % 10]);
}
}
}
return sb.ToString();
}
}
}
Specific Question:
This implementation gets the job done for check amounts, but I didn't want to limit the class to smaller numbers. This design seems to allow for extremely large numbers to be converted to text as long as you can generate the required regex and use a BigNumber datatype (which this implementation clearly doesn't utilize.) Is this good design for the problem of converting extremely large numbers to text? I haven't seen an implementation like this for converting numbers to text, maybe for good reason?
Advice:
Any feedback or suggestions about any parts of the code is appreciated.