Roman numerals to integers

前端 未结 14 888
不思量自难忘°
不思量自难忘° 2020-12-05 20:56

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.

相关标签:
14条回答
  • 2020-12-05 21:18

    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;
        }
    
    0 讨论(0)
  • 2020-12-05 21:19

    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.

    0 讨论(0)
  • 2020-12-05 21:20

    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.


    Convert Roman Numerals by Recursion

    • The algorithm is able to non-adjacent numerals as well (f.e. XIIX).
    • This implementation may only work with well-formed (strings matching /[mdclxvi]*/i) roman numerals.
    • The implementation is not optimized for speed.
    // 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

    How does it work?

    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
    
    0 讨论(0)
  • 2020-12-05 21:21

    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");
                }
    
    
    
            }
    
        }
    
    0 讨论(0)
  • 2020-12-05 21:21

    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));
        }
    }
    
    0 讨论(0)
  • 2020-12-05 21:25

    A more simple and readable C# implementation that:

    • maps I to 1, V to 5, X to 10, L to 50, C to 100, D to 500, M to 1000.
    • uses one single foreach loop (foreach used on purpose, with previous value hold).
    • adds the mapped number to the total.
    • subtracts twice the number added before, if I before V or X, X before L or C, C before D or M (not all chars are allowed here!).
    • returns 0 (not used in Roman numerals) on empty string, wrong letter or not allowed char used for subtraction.
    • remark: it's still not totally complete, we didn't check all possible conditions for a valid input string!

    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;
    }
    
    0 讨论(0)
提交回复
热议问题