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