← All source files

V8 / 34b774036cd1

KokoroRomanNumerals.cs

Lilith/Core/Kokoro/KokoroRomanNumerals.cs

Changed in this version
CS
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);
}