Skip to content

Instantly share code, notes, and snippets.

@bronumski
Last active December 20, 2015 11:09
Show Gist options
  • Save bronumski/6121160 to your computer and use it in GitHub Desktop.
Save bronumski/6121160 to your computer and use it in GitHub Desktop.
Roman numeral parser
Roman numerals
--------------
http://en.wikipedia.org/wiki/Roman_numerals
Reading Roman numerals
----------------------
Based on seven symbols
I = 1
V = 5
X = 10
L = 50
C = 100
D = 500
M = 1000
Numbers are formed by combining symbols together and adding the values.
Symbols are placed from left to right in order of value, starting with the largest.
However, in a few specific cases,[2] to avoid four characters being repeated in succession
(such as IIII or XXXX) these can be reduced using subtractive notation as follows:
-> The numeral I can be placed before V and X to make 4 units (IV) and 9 units (IX) respectively
-> X can be placed before L and C to make 40 (XL) and 90 (XC) respectively
-> C can be placed before D and M to make 400 and 900 according to the same pattern
Examples
--------
II = 2
IV = 4
VIII = 8
IX = 9
XVI = 16
XXXII = 32
LXIV = 64
XC = 90
CXXVIII = 128
CCLVI = 256
DXII = 512
CM = 900
MXXIV = 1024
MDCLXVI = 1666
using System;
namespace RomanNumerals
{
public class InvalidRomanNumeralException : Exception
{
public InvalidRomanNumeralException(string romanNumeral, RomanNumeralSymbol invalidSymbol1, RomanNumeralSymbol invalidSymbo2)
: base(string.Format("'{0}' is not valid roman numeral, symbol '{1}' cannot appear before symbol '{2}'", romanNumeral, invalidSymbol1, invalidSymbo2))
{
}
}
}
using System;
namespace RomanNumerals
{
public class InvalidRomanNumeralSymbolException : Exception
{
public InvalidRomanNumeralSymbolException(char symbol)
: base(string.Format("'{0}' is not valid roman numeral symbol", symbol))
{
}
}
}
using System.Linq;
namespace RomanNumerals
{
public class RomanNumeralParser
{
private readonly RomanNumeralSubtractionRule romanNumeralSubtractionRule = new RomanNumeralSubtractionRule();
public int Parse(string romanNumeral)
{
var result = 0;
var previousSymbol = new RomanNumeralSymbol();
foreach (var currentSymbol in romanNumeral.Reverse().Select(symbol => new RomanNumeralSymbol(symbol)))
{
if (previousSymbol > currentSymbol && !romanNumeralSubtractionRule.IsValid(currentSymbol, previousSymbol))
{
throw new InvalidRomanNumeralException(romanNumeral, currentSymbol, previousSymbol);
}
result += GetValueBasedOnPreviousChar(previousSymbol, currentSymbol);
previousSymbol = currentSymbol;
}
return result;
}
private static int GetValueBasedOnPreviousChar(RomanNumeralSymbol previousValue, RomanNumeralSymbol romanNumeralSymbol)
{
return previousValue > romanNumeralSymbol ? romanNumeralSymbol.Value * -1 : romanNumeralSymbol.Value;
}
}
}
using System.Collections.Generic;
using System.Linq;
namespace RomanNumerals
{
public class RomanNumeralSubtractionRule
{
private static readonly IDictionary<RomanNumeralSymbol, RomanNumeralSymbol[]> validSubtractionCombinations =
new Dictionary<RomanNumeralSymbol, RomanNumeralSymbol[]>
{
{RomanNumeralSymbol.I, new[] { RomanNumeralSymbol.V, RomanNumeralSymbol.X } },
{RomanNumeralSymbol.X, new[] { RomanNumeralSymbol.L, RomanNumeralSymbol.C } },
{RomanNumeralSymbol.C, new[] { RomanNumeralSymbol.D, RomanNumeralSymbol.M } },
};
public bool IsValid(RomanNumeralSymbol subtractionSymbol, RomanNumeralSymbol subsequentSymbol)
{
return validSubtractionCombinations.ContainsKey(subtractionSymbol) && validSubtractionCombinations[subtractionSymbol].Contains(subsequentSymbol);
}
}
}
using System.Collections.Generic;
using System.Globalization;
namespace RomanNumerals
{
public struct RomanNumeralSymbol
{
private static readonly IDictionary<char, int> RomanNumarlSymbolToDecimalMap = new Dictionary<char, int>
{
{'I', 1}, {'V', 5}, {'X', 10}, {'L', 50}, {'C', 100},{'D', 500}, {'M', 1000},
};
public static readonly RomanNumeralSymbol I = new RomanNumeralSymbol('I');
public static readonly RomanNumeralSymbol V = new RomanNumeralSymbol('V');
public static readonly RomanNumeralSymbol X = new RomanNumeralSymbol('X');
public static readonly RomanNumeralSymbol L = new RomanNumeralSymbol('L');
public static readonly RomanNumeralSymbol C = new RomanNumeralSymbol('C');
public static readonly RomanNumeralSymbol D = new RomanNumeralSymbol('D');
public static readonly RomanNumeralSymbol M = new RomanNumeralSymbol('M');
private readonly int value;
private readonly char symbol;
public RomanNumeralSymbol(char symbol)
{
if (!RomanNumarlSymbolToDecimalMap.ContainsKey(symbol))
{
throw new InvalidRomanNumeralSymbolException(symbol);
}
value = RomanNumarlSymbolToDecimalMap[symbol];
this.symbol = symbol;
}
public char Symbol { get { return symbol; } }
public int Value { get { return value; } }
public static bool operator ==(RomanNumeralSymbol symbol1, RomanNumeralSymbol symbol2)
{
return symbol1.Value == symbol2.Value;
}
public static bool operator !=(RomanNumeralSymbol symbol1, RomanNumeralSymbol symbol2)
{
return !(symbol1 == symbol2);
}
public static bool operator >(RomanNumeralSymbol symbol1, RomanNumeralSymbol symbol2)
{
return symbol1.Value > symbol2.Value;
}
public static bool operator >=(RomanNumeralSymbol symbol1, RomanNumeralSymbol symbol2)
{
return symbol1.Value >= symbol2.Value;
}
public static bool operator <(RomanNumeralSymbol symbol1, RomanNumeralSymbol symbol2)
{
return symbol1.Value < symbol2.Value;
}
public static bool operator <=(RomanNumeralSymbol symbol1, RomanNumeralSymbol symbol2)
{
return symbol1.Value <= symbol2.Value;
}
public override string ToString()
{
return Symbol.ToString(CultureInfo.InvariantCulture);
}
}
}
using System;
using FluentAssertions;
using NUnit.Framework;
namespace RomanNumerals.Tests
{
[TestFixture]
class When_loading_roman_numeral_string
{
private RomanNumeralParser romanNumeralParser;
[SetUp]
public void SetUp()
{
romanNumeralParser = new RomanNumeralParser();
}
[TestCase('I', 1)]
[TestCase('V', 5)]
[TestCase('X', 10)]
[TestCase('L', 50)]
[TestCase('C', 100)]
[TestCase('D', 500)]
[TestCase('M', 1000)]
public void Should_turn_single_roman_numeral_symbol_into_equivilent_decimal(char romanNumeral, int expectedValue)
{
var result = romanNumeralParser.Parse(romanNumeral.ToString());
result.Should().Be(expectedValue);
}
[TestCase("II", 2)]
[TestCase("III", 3)]
[TestCase("VI", 6)]
[TestCase("XV", 15)]
[TestCase("MDC", 1600)]
public void Should_turn_cumlative_roman_numerals_into_equivilent_decimal(string romanNumeral, int expectedValue)
{
var result = romanNumeralParser.Parse(romanNumeral);
result.Should().Be(expectedValue);
}
[TestCase("IV", 4)]
[TestCase("IX", 9)]
[TestCase("XL", 40)]
[TestCase("XC", 90)]
[TestCase("CD", 400)]
[TestCase("CM", 900)]
public void Should_turn_cumlative_roman_numerals_with_subtractions_into_equivilent_decimal(string romanNumeral, int expectedValue)
{
var result = romanNumeralParser.Parse(romanNumeral);
result.Should().Be(expectedValue);
}
[TestCase("P", 'P')]
[TestCase("IP", 'P')]
public void Should_throw_an_exception_when_an_invalid_roman_numeral_character_is_encountered(string romanNumeral, char expectedInvalidChar)
{
Action act = () => romanNumeralParser.Parse(romanNumeral);
act.ShouldThrow<InvalidRomanNumeralSymbolException>()
.WithMessage(string.Format("'{0}' is not valid roman numeral symbol", expectedInvalidChar));
}
[TestCase("IL", 'I', 'L')]
[TestCase("VL", 'V', 'L')]
[TestCase("XM", 'X', 'M')]
[TestCase("LM", 'L', 'M')]
public void Should_throw_an_exceprion_when_certain_symbols_are_placed_before_others(string romanNumeral, char expectedInvalidChar1, char expectedInvalidChar2)
{
Action act = () => romanNumeralParser.Parse(romanNumeral);
act.ShouldThrow<InvalidRomanNumeralException>()
.WithMessage(string.Format("'{0}' is not valid roman numeral, symbol '{1}' cannot appear before symbol '{2}'", romanNumeral, expectedInvalidChar1, expectedInvalidChar2));
}
[TestCase("II", 2)]
[TestCase("IV", 4)]
[TestCase("VIII", 8)]
[TestCase("IX", 9)]
[TestCase("XVI", 16)]
[TestCase("XXXII", 32)]
[TestCase("LXIV", 64)]
[TestCase("XC", 90)]
[TestCase("CXXVIII", 128)]
[TestCase("CCLVI", 256)]
[TestCase("DXII", 512)]
[TestCase("CM", 900)]
[TestCase("MXXIV", 1024)]
[TestCase("MDCLXVI", 1666)]
public void Should_parse_a_set_of_examples(string romanNumeral, int expectedValue)
{
var result = romanNumeralParser.Parse(romanNumeral);
result.Should().Be(expectedValue);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment