diff --git a/src/Humanizer/RomanNumeralExtensions.cs b/src/Humanizer/RomanNumeralExtensions.cs index b16528eef..6b6f91533 100644 --- a/src/Humanizer/RomanNumeralExtensions.cs +++ b/src/Humanizer/RomanNumeralExtensions.cs @@ -1,5 +1,7 @@ // Done by Jesse Slicer https://github.com/jslicer +using System.Diagnostics; + namespace Humanizer; /// @@ -7,54 +9,42 @@ namespace Humanizer; /// public static class RomanNumeralExtensions { - static readonly Dictionary RomanNumerals = - new(StringComparer.OrdinalIgnoreCase) + static readonly KeyValuePair[] RomanNumeralsSequence = + [ + new KeyValuePair("M", 1000), + new KeyValuePair("CM", 900), + new KeyValuePair("D", 500 ), + new KeyValuePair("CD", 400), + new KeyValuePair("C", 100 ), + new KeyValuePair("XC", 90 ), + new KeyValuePair("L", 50 ), + new KeyValuePair("XL", 40 ), + new KeyValuePair("X", 10 ), + new KeyValuePair("IX", 9 ), + new KeyValuePair("V", 5 ), + new KeyValuePair("IV", 4 ), + new KeyValuePair("I", 1 ), + ]; + + static int GetRomanNumeralCharValue(char c) + { + Debug.Assert(char.ToLowerInvariant(c) is 'M' or 'D' or 'C' or 'L' or 'X' or 'V' or 'I', "Invalid Roman numeral character"); + return (c & ~0x20) switch { - { - "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 - } + 'M' => 1000, + 'D' => 500, + 'C' => 100, + 'L' => 50, + 'X' => 10, + 'V' => 5, + _ => 1, }; + } static readonly Regex ValidRomanNumeral = new( "^(?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.Compiled | RegexOptions.IgnoreCase); + RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); /// /// Converts Roman numbers into integer @@ -92,14 +82,10 @@ public static int FromRoman(CharSpan input) while (i > 0) { - var digit = RomanNumerals[input[--i] - .ToString()]; - + var digit = GetRomanNumeralCharValue(input[--i]); if (i > 0) { - var previousDigit = RomanNumerals[input[i - 1] - .ToString()]; - + var previousDigit = GetRomanNumeralCharValue(input[i - 1]); if (previousDigit < digit) { digit -= previousDigit; @@ -130,18 +116,20 @@ public static string ToRoman(this int input) throw new ArgumentOutOfRangeException(); } - var builder = new StringBuilder(maxRomanNumeralLength); + Span builder = stackalloc char[maxRomanNumeralLength]; + var pos = 0; - foreach (var pair in RomanNumerals) + foreach (var pair in RomanNumeralsSequence) { - while (input / pair.Value > 0) + while (input >= pair.Value) { - builder.Append(pair.Key); + pair.Key.AsSpan().CopyTo(builder.Slice(pos)); + pos += pair.Key.Length; input -= pair.Value; } } - return builder.ToString(); + return builder.Slice(0, pos).ToString(); } static bool IsInvalidRomanNumeral(CharSpan input) =>