using System.Text.RegularExpressions; namespace PlotLine.Services; public static class StoryIntelligenceEntityTextNormaliser { private static readonly Regex Whitespace = new(@"\s+", RegexOptions.Compiled); private static readonly Regex TitleFullStops = new(@"\b(Mr|Mrs|Ms|Miss|Dr|Prof|Rev|Revd)\.", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex NonKeyCharacters = new(@"[^\p{L}\p{N}']+", RegexOptions.Compiled); public static string Clean(string? value) => Whitespace.Replace((value ?? string.Empty).Replace('’', '\''), " ").Trim(); public static string CleanName(string? value) => Clean(value).Trim(' ', '.', ',', ';', ':', '!', '?', '"', '\''); public static string StripLeadingArticle(string value) { var clean = Clean(value); return clean.StartsWith("the ", StringComparison.OrdinalIgnoreCase) ? clean[4..] : clean.StartsWith("a ", StringComparison.OrdinalIgnoreCase) ? clean[2..] : clean.StartsWith("an ", StringComparison.OrdinalIgnoreCase) ? clean[3..] : clean; } public static string SafeKey(string? value) { var clean = TitleFullStops.Replace(Clean(value), "$1"); clean = NonKeyCharacters.Replace(clean, " "); clean = Whitespace.Replace(clean, " ").Trim().ToLowerInvariant(); return clean; } public static string SingulariseSafeFinalWord(string value, IReadOnlySet singularisableWords) { var words = SafeKey(value).Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList(); if (words.Count == 0) { return string.Empty; } var last = words[^1]; if (last.EndsWith("ies", StringComparison.OrdinalIgnoreCase)) { var singular = $"{last[..^3]}y"; if (singularisableWords.Contains(singular)) { words[^1] = singular; } } else if (last.EndsWith('s') && last.Length > 3) { var singular = last[..^1]; if (singularisableWords.Contains(singular)) { words[^1] = singular; } } return string.Join(' ', words); } }