I have a transfer with products that unfortunately has to get matched by product name. The biggest issue here is I might get duplicate products on account of roman numbers.
Borrowed a lot from System.Linq
on this one. String
implements IEnumerable<char>
, so I figured that was appropriate since we are treating it as an enumerable object anyways. Tested it against a bunch of random numbers, including 1, 3, 4, 8, 83, 99, 404, 555, 846, 927, 1999, 2420.
public static IDictionary<char, int> CharValues
{
get
{
return new Dictionary<char, int>
{{'I', 1}, {'V', 5}, {'X', 10}, {'L', 50}, {'C', 100}, {'D', 500}, {'M', 1000}};
}
}
public static int RomanNumeralToInteger(IEnumerable<char> romanNumerals)
{
int retVal = 0;
//go backwards
for (int i = romanNumerals.Count() - 1; i >= 0; i--)
{
//get current character
char c = romanNumerals.ElementAt(i);
//error checking
if (!CharValues.ContainsKey(c)) throw new InvalidRomanNumeralCharacterException(c);
//determine if we are adding or subtracting
bool op = romanNumerals.Skip(i).Any(rn => CharValues[rn] > CharValues[c]);
//then do so
retVal = op ? retVal - CharValues[c] : retVal + CharValues[c];
}
return retVal;
}
This is my solution:
/// <summary>
/// Converts a Roman number string into a Arabic number
/// </summary>
/// <param name="romanNumber">the Roman number string</param>
/// <returns>the Arabic number (0 if the given string is not convertible to a Roman number)</returns>
public static int ToArabicNumber(string romanNumber)
{
string[] replaceRom = { "CM", "CD", "XC", "XL", "IX", "IV" };
string[] replaceNum = { "DCCCC", "CCCC", "LXXXX", "XXXX", "VIIII", "IIII" };
string[] roman = { "M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I" };
int[] arabic = { 1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1 };
return Enumerable.Range(0, replaceRom.Length)
.Aggregate
(
romanNumber,
(agg, cur) => agg.Replace(replaceRom[cur], replaceNum[cur]),
agg => agg.ToArray()
)
.Aggregate
(
0,
(agg, cur) =>
{
int idx = Array.IndexOf(roman, cur.ToString());
return idx < 0 ? 0 : agg + arabic[idx];
},
agg => agg
);
}
/// <summary>
/// Converts a Arabic number into a Roman number string
/// </summary>
/// <param name="arabicNumber">the Arabic number</param>
/// <returns>the Roman number string</returns>
public static string ToRomanNumber(int arabicNumber)
{
string[] roman = { "M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I" };
int[] arabic = { 1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1 };
return Enumerable.Range(0, arabic.Length)
.Aggregate
(
Tuple.Create(arabicNumber, string.Empty),
(agg, cur) =>
{
int remainder = agg.Item1 % arabic[cur];
string concat = agg.Item2 + string.Concat(Enumerable.Range(0, agg.Item1 / arabic[cur]).Select(num => roman[cur]));
return Tuple.Create(remainder, concat);
},
agg => agg.Item2
);
}
Here's the Explanation how the methods work:
ToArabicNumber
First aggregation step is to Replace the Roman Number special cases (e.g.: IV -> IIII). Second Aggregate step simply sums up the equivalent Arabic number of the Roman letter (e.g. V -> 5)
ToRomanNumber:
I start the aggregation with the given Arabic number. For each step the number will be divided by the equivalent number of the Roman letter. The remainder of this division is then the input for the next step. The division Result will be translated to the Equivalent Roman Number character which will be appended to the result string.
I landed here searching for a small implementation of a Roman Numerals parser but wasn't satisfied by the provided answers in terms of size and elegance. I leave my final, recursive implementation here, to help others searching a small implementation.
XIIX
)./[mdclxvi]*/i
) roman numerals.// returns the value for a roman literal
private static int romanValue(int index)
{
int basefactor = ((index % 2) * 4 + 1); // either 1 or 5...
// ...multiplied with the exponentation of 10, if the literal is `x` or higher
return index > 1 ? (int) (basefactor * System.Math.Pow(10.0, index / 2)) : basefactor;
}
public static int FromRoman(string roman)
{
roman = roman.ToLower();
string literals = "mdclxvi";
int value = 0, index = 0;
foreach (char literal in literals)
{
value = romanValue(literals.Length - literals.IndexOf(literal) - 1);
index = roman.IndexOf(literal);
if (index > -1)
return FromRoman(roman.Substring(index + 1)) + (index > 0 ? value - FromRoman(roman.Substring(0, index)) : value);
}
return 0;
}
Try it using this .Netfiddle: https://dotnetfiddle.net/veaNk3
This algorithm calculates the value of a Roman Numeral by taking the highest value from the Roman Numeral and adding/subtracting recursively the value of the remaining left/right parts of the literal.
ii X iiv # Pick the greatest value in the literal `iixiiv` (symbolized by uppercase)
Then recursively reevaluate and subtract the lefthand-side and add the righthand-side:
(iiv) + x - (ii) # Subtract the lefthand-side, add the righthand-side
(V - (ii)) + x - ((I) + i) # Pick the greatest values, again
(v - ((I) + i)) + x - ((i) + i) # Pick the greatest value of the last numeral compound
Finally the numerals are substituted by their integer values:
(5 - ((1) + 1)) + 10 - ((1) + 1)
(5 - (2)) + 10 - (2)
3 + 10 - 2
= 11
This is my solution
public int SimplerConverter(string number)
{
number = number.ToUpper();
var result = 0;
foreach (var letter in number)
{
result += ConvertLetterToNumber(letter);
}
if (number.Contains("IV")|| number.Contains("IX"))
result -= 2;
if (number.Contains("XL")|| number.Contains("XC"))
result -= 20;
if (number.Contains("CD")|| number.Contains("CM"))
result -= 200;
return result;
}
private int ConvertLetterToNumber(char letter)
{
switch (letter)
{
case 'M':
{
return 1000;
}
case 'D':
{
return 500;
}
case 'C':
{
return 100;
}
case 'L':
{
return 50;
}
case 'X':
{
return 10;
}
case 'V':
{
return 5;
}
case 'I':
{
return 1;
}
default:
{
throw new ArgumentException("Ivalid charakter");
}
}
}
Solution with fulfilling the "subtractive notation" semantics checks
None of the current solutions completely fulfills the entire set of rules for the "subtractive notation". "IIII" -> is not possible. Each of the solutions results a 4. Also the strings: "CCCC", "VV", "IC", "IM" are invalid.
A good online-converter to check the semantics is https://www.romannumerals.org/converter So, if you really want doing a completely semantics-check, it's much more complex.
My approach was to first write the unit tests with the semantic checks. Then to write the code. Then to reduce the loops with some linq expressions.
Maybe there is a smarter solution, but I think the following code fullfills the rules to convert a roman numerals string.
After the code section, there is a section with my unit tests.
public class RomanNumerals
{
private List<Tuple<char, ushort, char?[]>> _validNumerals = new List<Tuple<char, ushort, char?[]>>()
{
new Tuple<char, ushort, char?[]>('I', 1, new char? [] {'V', 'X'}),
new Tuple<char, ushort, char?[]>('V', 5, null),
new Tuple<char, ushort, char?[]>('X', 10, new char?[] {'L', 'C'}),
new Tuple<char, ushort, char?[]>('L', 50, null),
new Tuple<char, ushort, char?[]>('C', 100, new char? [] {'D', 'M'}),
new Tuple<char, ushort, char?[]>('D', 500, null),
new Tuple<char, ushort, char?[]>('M', 1000, new char? [] {null, null})
};
public int TranslateRomanNumeral(string input)
{
var inputList = input?.ToUpper().ToList();
if (inputList == null || inputList.Any(x => _validNumerals.Select(t => t.Item1).Contains(x) == false))
{
throw new ArgumentException();
}
char? valForSubtraction = null;
int result = 0;
bool noAdding = false;
int equalSum = 0;
for (int i = 0; i < inputList.Count; i++)
{
var currentNumeral = _validNumerals.FirstOrDefault(s => s.Item1 == inputList[i]);
var nextNumeral = i < inputList.Count - 1 ? _validNumerals.FirstOrDefault(s => s.Item1 == inputList[i + 1]) : null;
bool currentIsDecimalPower = currentNumeral?.Item3?.Any() ?? false;
if (nextNumeral != null)
{
// Syntax and Semantics checks
if ((currentNumeral.Item2 < nextNumeral.Item2) && (currentIsDecimalPower == false || currentNumeral.Item3.Any(s => s == nextNumeral.Item1) == false) ||
(currentNumeral.Item2 == nextNumeral.Item2) && (currentIsDecimalPower == false || nextNumeral.Item1 == valForSubtraction) ||
(currentIsDecimalPower && result > 0 && ((nextNumeral.Item2 -currentNumeral.Item2) > result )) ||
(currentNumeral.Item2 > nextNumeral.Item2) && (nextNumeral.Item1 == valForSubtraction)
)
{
throw new ArgumentException();
}
if (currentNumeral.Item2 == nextNumeral.Item2)
{
equalSum += equalSum == 0 ? currentNumeral.Item2 + nextNumeral.Item2 : nextNumeral.Item2;
int? smallest = null;
var list = _validNumerals.Where(p => _validNumerals.FirstOrDefault(s => s.Item1 == currentNumeral.Item1).Item3.Any(s2 => s2 != null && s2 == p.Item1)).ToList();
if (list.Any())
{
smallest = list.Select(s3 => s3.Item2).ToList().Min();
}
// Another Semantics check
if (currentNumeral.Item3 != null && equalSum >= (smallest - currentNumeral.Item2))
{
throw new ArgumentException();
}
result += noAdding ? 0 : currentNumeral.Item2 + nextNumeral.Item2;
noAdding = !noAdding;
valForSubtraction = null;
}
else
if (currentNumeral.Item2 < nextNumeral.Item2)
{
equalSum = 0;
result += nextNumeral.Item2 - currentNumeral.Item2;
valForSubtraction = currentNumeral.Item1;
noAdding = true;
}
else
if (currentNumeral.Item2 > nextNumeral.Item2)
{
equalSum = 0;
result += noAdding ? 0 : currentNumeral.Item2;
noAdding = false;
valForSubtraction = null;
}
}
else
{
result += noAdding ? 0 : currentNumeral.Item2;
}
}
return result;
}
}
Here are the UNIT tests
[TestFixture]
public class RomanNumeralsTests
{
[Test]
public void TranslateRomanNumeral_WhenArgumentIsNull_RaiseArgumentNullException()
{
var romanNumerals = new RomanNumerals();
Assert.Throws<ArgumentException>(() => romanNumerals.TranslateRomanNumeral(null));
}
[TestCase("A")]
[TestCase("-")]
[TestCase("BXA")]
[TestCase("MMXK")]
public void TranslateRomanNumeral_WhenInvalidNumeralSyntax_RaiseException(string input)
{
var romanNumerals = new RomanNumerals();
Assert.Throws<ArgumentException>(() => romanNumerals.TranslateRomanNumeral(input));
}
[TestCase("IIII")]
[TestCase("CCCC")]
[TestCase("VV")]
[TestCase("IC")]
[TestCase("IM")]
[TestCase("XM")]
[TestCase("IL")]
[TestCase("MCDXCXI")]
[TestCase("MCDDXC")]
public void TranslateRomanNumeral_WhenInvalidNumeralSemantics_RaiseException(string input)
{
var romanNumerals = new RomanNumerals();
Assert.Throws<ArgumentException>(() => romanNumerals.TranslateRomanNumeral(input));
}
[TestCase("I", 1)]
[TestCase("II", 2)]
[TestCase("III", 3)]
[TestCase("IV", 4)]
[TestCase("XLII", 42)]
[TestCase("MMXIII", 2013)]
[TestCase("MXI", 1011)]
[TestCase("MCDXCIX", 1499)]
[TestCase("MMXXII", 2022)]
[TestCase("V", 5)]
[TestCase("VI", 6)]
[TestCase("CX", 110)]
[TestCase("CCCLXXV", 375)]
[TestCase("MD", 1500)]
[TestCase("MDLXXV", 1575)]
[TestCase("MDCL", 1650)]
[TestCase("MDCCXXV", 1725)]
[TestCase("MDCCC", 1800)]
[TestCase("MDCCCLXXV", 1875)]
[TestCase("MCML", 1950)]
[TestCase("MMXXV", 2025)]
[TestCase("MMC", 2100)]
[TestCase("MMCLXXV", 2175)]
[TestCase("MMCCL", 2250)]
[TestCase("MMCCCXXV", 2325)]
[TestCase("MMCD", 2400)]
[TestCase("MMCDLXXV", 2475)]
[TestCase("MMDL", 2550)]
[TestCase("MMMMMMMM", 8000)]
[TestCase("MMMMMMMMIV", 8004)]
public void TranslateRomanNumeral_WhenValidNumeral_Translate(string input, int output)
{
var romanNumerals = new RomanNumerals();
var result = romanNumerals.TranslateRomanNumeral(input);
Assert.That(result.Equals(output));
}
}
A more simple and readable C# implementation that:
Code:
private static Dictionary<char, int> _romanMap = new Dictionary<char, int>
{
{'I', 1}, {'V', 5}, {'X', 10}, {'L', 50}, {'C', 100}, {'D', 500}, {'M', 1000}
};
public static int ConvertRomanToNumber(string text)
{
int totalValue = 0, prevValue = 0;
foreach (var c in text)
{
if (!_romanMap.ContainsKey(c))
return 0;
var crtValue = _romanMap[c];
totalValue += crtValue;
if (prevValue != 0 && prevValue < crtValue)
{
if (prevValue == 1 && (crtValue == 5 || crtValue == 10)
|| prevValue == 10 && (crtValue == 50 || crtValue == 100)
|| prevValue == 100 && (crtValue == 500 || crtValue == 1000))
totalValue -= 2 * prevValue;
else
return 0;
}
prevValue = crtValue;
}
return totalValue;
}