Thought this problem was interesting, this was my take on it.
It should (hopefully) deal with numbers up to the upper limit of overscored characters. Adding any other conventions should be just a matter of configuring new bands and adjusting the ConfigureNext
chain.
NumeralGenerator.cs
public static class NumeralGenerator
{
private static readonly INumeralBand RootNumeralBand = ConfigureMapping();
private static INumeralBand ConfigureMapping()
{
var unitBand = new FinalBand(1, "I");
var fiveBand = new NumeralBand(5, "V", unitBand);
var tenBand = new NumeralBand(10, "X", unitBand);
var fiftyBand = new NumeralBand(50, "L", tenBand);
var hundredBand = new NumeralBand(100, "C", tenBand);
var fiveHundredBand = new NumeralBand(500, "D", hundredBand);
var thousandBand = new NumeralBand(1000, "M", hundredBand);
var thousandUnitBand = new NumeralBand(1000, "I\u0305", thousandBand);
var fiveThousandBand = new NumeralBand(5000, "V\u0305", thousandUnitBand);
var tenThousandBand = new NumeralBand(10000, "X\u0305", thousandUnitBand);
var fiftyThousandBand = new NumeralBand(50000, "L\u0305", tenThousandBand);
var hundredThousandBand = new NumeralBand(100000, "C\u0305", tenThousandBand);
var fiveHundredThousandBand = new NumeralBand(500000, "D\u0305", hundredThousandBand);
var millionBand = new NumeralBand(1000000, "M\u0305", hundredThousandBand);
millionBand
.ConfigureNext(fiveHundredThousandBand)
.ConfigureNext(hundredThousandBand)
.ConfigureNext(fiftyThousandBand)
.ConfigureNext(tenThousandBand)
.ConfigureNext(fiveThousandBand)
.ConfigureNext(thousandBand)
.ConfigureNext(fiveHundredBand)
.ConfigureNext(hundredBand)
.ConfigureNext(fiftyBand)
.ConfigureNext(tenBand)
.ConfigureNext(fiveBand)
.ConfigureNext(unitBand);
return millionBand;
}
public static string ToNumeral(int number)
{
var numerals = new StringBuilder();
RootNumeralBand.Process(number, numerals);
return numerals.ToString();
}
}
INumeralBand.cs
public interface INumeralBand
{
int Value { get; }
string Numeral { get; }
void Process(int number, StringBuilder numerals);
}
NumeralBand.cs
public class NumeralBand : INumeralBand
{
private readonly INumeralBand _negatedBy;
private INumeralBand _nextBand;
public NumeralBand(int value, string numeral, INumeralBand negatedBy)
{
_negatedBy = negatedBy;
Value = value;
Numeral = numeral;
}
public int Value { get; }
public string Numeral { get; }
public void Process(int number, StringBuilder numerals)
{
if (ShouldNegateAndStop(number))
{
numerals.Append(NegatedNumeral);
return;
}
var numeralCount = Math.Abs(number / Value);
var remainder = number % Value;
numerals.Append(string.Concat(Enumerable.Range(1, numeralCount).Select(x => Numeral)));
if (ShouldNegateAndContinue(remainder))
{
NegateAndContinue(numerals, remainder);
return;
}
if (remainder > 0)
_nextBand.Process(remainder, numerals);
}
private string NegatedNumeral => $"{_negatedBy.Numeral}{Numeral}";
private bool ShouldNegateAndStop(int number) => number == Value - _negatedBy.Value;
private bool ShouldNegateAndContinue(int number) => number >= Value - _negatedBy.Value;
private void NegateAndContinue(StringBuilder stringBuilder, int remainder)
{
stringBuilder.Append(NegatedNumeral);
remainder = remainder % (Value - _negatedBy.Value);
_nextBand.Process(remainder, stringBuilder);
}
public T ConfigureNext<T>(T nextBand) where T : INumeralBand
{
_nextBand = nextBand;
return nextBand;
}
}
FinalBand.cs
public class FinalBand : INumeralBand
{
public FinalBand(int value, string numeral)
{
Value = value;
Numeral = numeral;
}
public int Value { get; }
public string Numeral { get; }
public void Process(int number, StringBuilder numerals)
{
numerals.Append(new string(Numeral[0], number));
}
}
The Tests:
FinalBandTests.cs
public class FinalBandTests
{
[Theory]
[InlineData(1, "I")]
[InlineData(2, "II")]
[InlineData(3, "III")]
[InlineData(4, "IIII")]
public void Process(int number, string expected)
{
var stringBuilder = new StringBuilder();
var numeralBand = new FinalBand(1, "I");
numeralBand.Process(number, stringBuilder);
Assert.Equal(expected, stringBuilder.ToString());
}
}
NumeralBandTests.cs
public class NumeralBandTests
{
private Mock<INumeralBand> _nextBand;
private Mock<INumeralBand> _negatedBy;
private StringBuilder _stringBuilder;
public NumeralBandTests()
{
_stringBuilder = new StringBuilder();
_nextBand = new Mock<INumeralBand>();
_negatedBy = new Mock<INumeralBand>();
}
[Fact]
public void Process_NegateAndStop()
{
var numeral = new NumeralBand(10, "X", _negatedBy.Object);
_negatedBy.Setup(x => x.Value).Returns(1);
_negatedBy.Setup(x => x.Numeral).Returns("I");
numeral.Process(9, _stringBuilder);
Assert.Equal("IX", _stringBuilder.ToString());
_nextBand.Verify(x => x.Process(It.IsAny<int>(), It.IsAny<StringBuilder>()), Times.Never);
}
[Fact]
public void Process_Exact()
{
var numeral = new NumeralBand(10, "X", _negatedBy.Object);
_negatedBy.Setup(x => x.Value).Returns(1);
_negatedBy.Setup(x => x.Numeral).Returns("I");
numeral.Process(10, _stringBuilder);
Assert.Equal("X", _stringBuilder.ToString());
_nextBand.Verify(x => x.Process(It.IsAny<int>(), It.IsAny<StringBuilder>()), Times.Never);
}
[Fact]
public void Process_NegateAndContinue()
{
var numeral = new NumeralBand(50, "L", _negatedBy.Object);
numeral.ConfigureNext(_nextBand.Object);
_negatedBy.Setup(x => x.Value).Returns(10);
_negatedBy.Setup(x => x.Numeral).Returns("X");
numeral.Process(54, _stringBuilder);
Assert.Equal("L", _stringBuilder.ToString());
_nextBand.Verify(x => x.Process(4, _stringBuilder), Times.Once);
}
}
NumeralGeneratorTests.cs
public class NumeralGeneratorTests
{
private readonly ITestOutputHelper _output;
public NumeralGeneratorTests(ITestOutputHelper output)
{
_output = output;
}
[Theory]
[InlineData(1, "I")]
[InlineData(2, "II")]
[InlineData(3, "III")]
[InlineData(4, "IV")]
[InlineData(5, "V")]
[InlineData(6, "VI")]
[InlineData(7, "VII")]
[InlineData(8, "VIII")]
[InlineData(9, "IX")]
[InlineData(10, "X")]
[InlineData(11, "XI")]
[InlineData(15, "XV")]
[InlineData(1490, "MCDXC")]
[InlineData(1480, "MCDLXXX")]
[InlineData(1580, "MDLXXX")]
[InlineData(1590, "MDXC")]
[InlineData(1594, "MDXCIV")]
[InlineData(1294, "MCCXCIV")]
[InlineData(3999, "MMMCMXCIX")]
[InlineData(4000, "I\u0305V\u0305")]
[InlineData(4001, "I\u0305V\u0305I")]
[InlineData(5002, "V\u0305II")]
[InlineData(10000, "X\u0305")]
[InlineData(15000, "X\u0305V\u0305")]
[InlineData(15494, "X\u0305V\u0305CDXCIV")]
[InlineData(2468523, "M\u0305M\u0305C\u0305D\u0305L\u0305X\u0305V\u0305MMMDXXIII")]
public void ToNumeral(int number, string expected)
{
var sw = Stopwatch.StartNew();
var actual = NumeralGenerator.ToNumeral(number);
sw.Stop();
_output.WriteLine(sw.ElapsedMilliseconds.ToString());
Assert.Equal(expected, actual);
}
}