Last active
December 20, 2015 11:09
-
-
Save bronumski/6121160 to your computer and use it in GitHub Desktop.
Roman numeral parser
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | |
{ | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
namespace RomanNumerals | |
{ | |
public class InvalidRomanNumeralSymbolException : Exception | |
{ | |
public InvalidRomanNumeralSymbolException(char symbol) | |
: base(string.Format("'{0}' is not valid roman numeral symbol", symbol)) | |
{ | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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