Write a program that accepts input from the user in the form of a number. You can choose which of the following variants to make from there:
Variant 1
The program should convert the number to words, making sure to include the “and” where it's appropriate and hyphens for the tens. For example an input of \(42069\) should output forty-two thousand and sixty-nine (note the hyphens and the usage of “and”).
Bonus challenge
Modify the program to accept decimal numbers. Remember that numbers after a decimal point are read differently.
Variant 2
The program should convert the number to roman numerals, making sure to incorporate the subtractive principle. For example an input of \(293\) should output \(\text{CCXCIII}\) (note how \(90\) is represented as \(\text{XC}\), meaning "\(10\) away from \(100\)"). You can assume that no input contains fractions, it's always integer.
Bonus challenge
Modify the program so that it performs the reverse; convert roman numerals back to an integer.
Solution
Variant 1
As with any problem, the solution here is to break it down into smaller problems. First, try to solve it by writing just a single-digit number as words. This could easily be done with an array of the words from 1-9, where the index of the array is the digit - 1.
You don't need the word “zero” for 0, as the only time zero appears in the output is if the input number as a whole is equal to 0.
Then, solve for a two-digit number. The edge cases to look out for are 11-19, which in English have distinct names that do not follow the pattern of other two-digit numbers from 20 onwards.
Lastly, solve for a three-digit number. If you can solve for a three-digit number, you can solve for any number. English groups numbers into groups of three, delimited by the words for each power of \(1000\).
For example, the number \(123,123,123,123\) is written as “one hundred and twenty-three billion one hundred and twenty-three million one hundred and twenty-three thousand one hundred and twenty-three”. Each group of three digits is read the same, it's only the delimiter that changes.
The solution I settled on was:
string NumberToWords(long value)
{
if (value == 0)
{
return "zero";
}
var buffer = new StringBuilder();
var words = new List<string>();
if (value < 0)
{
words.Add("negative");
value = -value;
}
string[] magnitudes = ["", "thousand", "million", "billion", "trillion", "quadrillion", "quintillion"];
string[] teens = ["eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen"];
string[] units = ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"];
string[] tens = ["twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"];
// 7 possible magnitudes with long
for (int i = 6; i >= 0; i--)
{
int group = (int)Math.Ceiling(value / Math.Pow(1000, i) % 1000) - 1;
if (group == 0)
{
continue;
}
if (group >= 100)
{
words.Add(units[group / 100 - 1]);
words.Add("hundred");
if (group % 100 > 0)
{
words.Add("and");
}
group %= 100;
}
else if (i == 0 && group is > 0 and < 100 && value >= 1000)
{
words.Add("and");
}
switch (group)
{
case > 10 and < 20:
words.Add(teens[group - 11]);
break;
case >= 20:
buffer.Append(tens[group / 10 - 2]);
if (group % 10 > 0)
{
buffer.Append('-');
buffer.Append(units[group % 10 - 1]);
}
words.Add(buffer.ToString());
buffer.Clear();
break;
case > 0:
words.Add(units[group - 1]);
break;
}
words.Add(magnitudes[i]);
}
return string.Join(" ", words);
}
To allow decimal inputs, you simply need to treat each digit separately, as every digit after the decimal point is read in isolation. For example \(\pi\), or \(3.14159265 \dots\), is read as "three point one four one five nine two six five ...".
Variant 2
Roman numerals forbid any of the same character appearing 4 or more times. This is why each \(5 \cdot 10^k\) has a unique symbol.
For example, \(1\), \(2\), and \(3\) just start off as a tally \(\text{I}\), \(\text{II}\), \(\text{III}\). However because \(\text{IIII}\) is illegal notation, \(4\) is actually notated as \(5 - 1\), or \(\text{IV}\). \(5\) is then \(\text{V}\), and \(6\) is \(\text{VI}\).
The roman numeral symbols are:
- \(1 = \text{I}\)
- \(5 = \text{V}\)
- \(10 = \text{X}\)
- \(50 = \text{L}\)
- \(100 = \text{C}\)
- \(500 = \text{D}\)
- \(1000 = \text{M}\)
This also means it's impossible to represent any value greater than \(3999\), as there exists no symbol for \(5000\). If there were one (for example let's just call it \(\text{N}\)) then \(4000\) could be notated as \(\text{MN}\) (\(5000 - 1000\)).
Given this information, we can create a mapping of values to roman numeral strings:
int[] values = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
string[] symbols = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"];
Returning a roman numeral string then just becomes a matter of iterating through the array and appending each symbol until the value is smaller than the current symbol - repeating until every unit is accounted for:
string ToRomanNumerals(int n)
{
var result = new StringBuilder();
for (int i = 0; i < values.Length; i++)
{
while (n >= values[i])
{
n -= values[i];
result.Append(symbols[i]);
}
}
return result.ToString();
}
To do the reverse, where a Roman numeral input string is converted to integers, you can simply iterate over the input string and add to a cumulative total the value that the Roman numeral character represents - unless the next character is greater. For example given the input string VI, the first character V represents \(5\), and I represents \(1\). Since \(1 < 5\) these are simply added to give \(5 + 1 = 6\). However given the input string IV, the first character I represents \(1\) and the second character V represents \(5\). \(5 > 1\), and so you subtract the first from the second to give \(5 - 1 = 4\).