diff --git a/readme.md b/readme.md index 343ddd400..ff2821471 100644 --- a/readme.md +++ b/readme.md @@ -335,6 +335,32 @@ This is kind of mixing `ToWords` with `Ordinalize`. You can call `ToOrdinalWords 121.ToOrdinalWords() => "hundred and twenty first" ``` +###Roman numerals +Humanizer can change numbers to Roman numerals using the `ToRoman` extension. The numbers 1 to 10 can be expressed in Roman numerals as follows: + +```C# +1.ToRoman() => "I" +2.ToRoman() => "II" +3.ToRoman() => "III" +4.ToRoman() => "IV" +5.ToRoman() => "V" +6.ToRoman() => "VI" +7.ToRoman() => "VII" +8.ToRoman() => "VIII" +9.ToRoman() => "IX" +10.ToRoman() => "X" +``` + +Also the reverse operation using the `FromRoman` extension. + +```C# +"I".FromRoman() => 1 +"II".FromRoman() => 2 +"III".FromRoman() => 3 +"IV".FromRoman() => 4 +"V".FromRoman() => 5 +``` + ###Mix this into your framework to simplify your life This is just a baseline and you can use this to simplify your day to day job. For example, in Asp.Net MVC we keep chucking `Display` attribute on ViewModel properties so `HtmlHelper` can generate correct labels for us; but, just like enums, in vast majority of cases we just need a space between the words in property name - so why not use `"string".Humanize` for that?! @@ -468,3 +494,4 @@ Mehdi Khalili ([@MehdiKhalili](http://twitter.com/MehdiKhalili)) ###License Humanizer is released under the MIT License. See the [bundled LICENSE](https://github.com/MehdiK/Humanizer/blob/master/LICENSE) file for details. + diff --git a/src/Humanizer.Tests/Humanizer.Tests.csproj b/src/Humanizer.Tests/Humanizer.Tests.csproj index 5d01ef2c6..088335551 100644 --- a/src/Humanizer.Tests/Humanizer.Tests.csproj +++ b/src/Humanizer.Tests/Humanizer.Tests.csproj @@ -79,6 +79,7 @@ + diff --git a/src/Humanizer.Tests/RomanNumeralTests.cs b/src/Humanizer.Tests/RomanNumeralTests.cs new file mode 100644 index 000000000..841ad276f --- /dev/null +++ b/src/Humanizer.Tests/RomanNumeralTests.cs @@ -0,0 +1,49 @@ +using Xunit; +using Xunit.Extensions; +using Humanizer; + +namespace Humanizer.Tests +{ + public class RomanNumeralTests + { + [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(12, "XII")] + [InlineData(100, "C")] + [InlineData(3999, "MMMCMXCIX")] + public void CanRomanize(int input, string expected) + { + Assert.Equal(expected, input.ToRoman()); + } + + [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(12, "XII")] + [InlineData(100, "C")] + [InlineData(3999, "MMMCMXCIX")] + public void CanUnromanize(int expected, string input) + { + Assert.Equal(expected, input.FromRoman()); + } + } +} diff --git a/src/Humanizer/Humanizer.csproj b/src/Humanizer/Humanizer.csproj index 5c2329139..994997909 100644 --- a/src/Humanizer/Humanizer.csproj +++ b/src/Humanizer/Humanizer.csproj @@ -83,6 +83,7 @@ On.Days.tt + diff --git a/src/Humanizer/RomanNumeralExtensions.cs b/src/Humanizer/RomanNumeralExtensions.cs new file mode 100644 index 000000000..2d9d22504 --- /dev/null +++ b/src/Humanizer/RomanNumeralExtensions.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; + +namespace Humanizer +{ + public static class RomanNumeralExtensions + { + private const int NumberOfRomanNumeralMaps = 13; + + private static readonly Dictionary romanNumerals = new Dictionary(NumberOfRomanNumeralMaps) + { + { "M", 1000 }, + { "CM", 900 }, + { "D", 500 }, + { "CD", 400 }, + { "C", 100 }, + { "XC", 90 }, + { "L", 50 }, + { "XL", 40 }, + { "X", 10 }, + { "IX", 9 }, + { "V", 5 }, + { "IV", 4 }, + { "I", 1 } + }; + + private static readonly Regex validRomanNumeral = new Regex("^(?i:(?=[MDCLXVI])((M{0,3})((C[DM])|(D?C{0,3}))" + "?((X[LC])|(L?XX{0,2})|L)?((I[VX])|(V?(II{0,2}))|V)?))$", RegexOptions.None); + + /// + /// Converts Roman numbers into integer + /// + /// Roman number + /// Human-readable number + public static int FromRoman(this string value) + { + if (value == null) + throw new ArgumentNullException("value"); + + value = value.ToUpper().Trim(); + var length = value.Length; + + if (length == 0 || IsInvalidRomanNumeral(value)) + throw new ArgumentException("Empty or invalid Roman numeral string.", "value"); + + var total = 0; + var i = length; + + while (i > 0) + { + var digit = romanNumerals[value[--i].ToString()]; + + if (i > 0) + { + var previousDigit = romanNumerals[value[i - 1].ToString()]; + + if (previousDigit < digit) + { + digit -= previousDigit; + i--; + } + } + + total += digit; + } + + return total; + } + + /// + /// Converts the input to Roman number + /// + /// Human-readable number + /// Roman number + public static string ToRoman(this int value) + { + const int MinValue = 1; + const int MaxValue = 3999; + const int MaxRomanNumeralLength = 15; + + if ((value < MinValue) || (value > MaxValue)) + throw new ArgumentOutOfRangeException(); + + var sb = new StringBuilder(MaxRomanNumeralLength); + + foreach (var pair in romanNumerals) + { + while (value / pair.Value > 0) + { + sb.Append(pair.Key); + value -= pair.Value; + } + } + + return sb.ToString(); + } + + private static bool IsInvalidRomanNumeral(string value) + { + return !validRomanNumeral.IsMatch(value); + } + } +} \ No newline at end of file