V13 / 7879e8b5740b
KokoroRomanNumerals.cs
Lilith/Core/Kokoro/KokoroRomanNumerals.cs
Unchanged from the preceding versionCS
using System.Text;
using System.Text.RegularExpressions;
namespace Lilith.Core.Kokoro;
/// <summary>English cardinal pronunciation of canonical Roman numerals, without changing chat text.</summary>
internal static partial class KokoroRomanNumerals
{
// Boundaries exclude identifiers and hyphenated model/product codes.
[GeneratedRegex(@"(?<![\w-])[IVXLCDMivxlcdm\u2160-\u217F]+(?![\w-])", RegexOptions.CultureInvariant)]
private static partial Regex Tokens();
[GeneratedRegex(@"\AM{0,3}(?:CM|CD|D?C{0,3})(?:XC|XL|L?X{0,3})(?:IX|IV|V?I{0,3})\z", RegexOptions.CultureInvariant)]
private static partial Regex Canonical();
[GeneratedRegex(@"(?:\b(?:chapter|part|book|volume|vol\.?|act|scene|episode|section|appendix|level|version|mark|type|class|phase|stage|GTA)|\bGrand Theft Auto)\s+\z", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex NumberContext();
[GeneratedRegex(@"\A[ \t]*[.)][ \t]+", RegexOptions.CultureInvariant)]
private static partial Regex ListSuffix();
private static readonly HashSet<string> Ambiguous = new(StringComparer.Ordinal)
{ "IV", "CD", "DC", "CM", "MD", "MI", "DI", "MIX", "DIM", "MID", "DIV", "LIV" };
internal static string Expand(string text) => Tokens().Replace(text, match =>
{
string token = match.Value.Normalize(NormalizationForm.FormKC).ToUpperInvariant();
if (token.Length == 0 || !Canonical().IsMatch(token)) return match.Value;
bool explicitGlyph = match.Value.Any(c => c is >= '\u2160' and <= '\u217F');
bool context = NumberContext().IsMatch(text[..match.Index]);
int lineStart = text.LastIndexOf('\n', Math.Max(0, match.Index - 1));
bool list = string.IsNullOrWhiteSpace(text[(lineStart + 1)..match.Index]) &&
ListSuffix().IsMatch(text[(match.Index + match.Length)..]);
bool uppercase = match.Value == match.Value.ToUpperInvariant();
if (!explicitGlyph && !context && !list && (!uppercase || token.Length == 1 || Ambiguous.Contains(token))) return match.Value;
int value = 0;
for (int i = 0; i < token.Length; i++)
{
int current = Digit(token[i]);
value += i + 1 < token.Length && current < Digit(token[i + 1]) ? -current : current;
}
return English(value);
});
private static int Digit(char digit) => digit switch
{ 'I' => 1, 'V' => 5, 'X' => 10, 'L' => 50, 'C' => 100, 'D' => 500, 'M' => 1000, _ => 0 };
private static readonly string[] Small = ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen"];
private static readonly string[] Tens = ["", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"];
private static string English(int value) => value switch
{
< 20 => Small[value],
< 100 => Tens[value / 10] + Remainder(value % 10),
< 1000 => Small[value / 100] + " hundred" + Remainder(value % 100),
_ => Small[value / 1000] + " thousand" + Remainder(value % 1000)
};
private static string Remainder(int value) => value == 0 ? "" : " " + English(value);
}