using System; using System.Collections.Generic; using System.Linq; using System.Text; using AxCopilot.Services; namespace AxCopilot.Core; public class FuzzyEngine { private readonly IndexService _index; private static readonly char[] Chosungs = new char[19] { 'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ' }; private static readonly HashSet ChosungSet = new HashSet(new _003C_003Ez__ReadOnlyArray(new char[19] { 'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ' })); private static readonly char[] Jungsungs = new char[21] { 'ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ', 'ㅕ', 'ㅖ', 'ㅗ', 'ㅘ', 'ㅙ', 'ㅚ', 'ㅛ', 'ㅜ', 'ㅝ', 'ㅞ', 'ㅟ', 'ㅠ', 'ㅡ', 'ㅢ', 'ㅣ' }; private static readonly char[] Jongsungs = new char[28] { '\0', 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ', 'ㄷ', 'ㄹ', 'ㄺ', 'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ', 'ㄿ', 'ㅀ', 'ㅁ', 'ㅂ', 'ㅄ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ' }; public FuzzyEngine(IndexService index) { _index = index; } public IEnumerable Search(string query, int maxResults = 7) { if (string.IsNullOrWhiteSpace(query)) { return Enumerable.Empty(); } string normalized = query.Trim().ToLowerInvariant(); IReadOnlyList entries = _index.Entries; bool queryHasKorean = false; string text = normalized; foreach (char c in text) { if ((c >= '가' && c <= '힣') || ChosungSet.Contains(c)) { queryHasKorean = true; break; } } if (entries.Count > 300) { return (from e in entries.AsParallel() select new FuzzyResult(e, CalculateScoreFast(normalized, e, queryHasKorean)) into r where r.Score > 0 orderby r.Score descending select r).Take(maxResults); } return (from e in entries select new FuzzyResult(e, CalculateScoreFast(normalized, e, queryHasKorean)) into r where r.Score > 0 orderby r.Score descending select r).Take(maxResults); } private static int CalculateScoreFast(string query, IndexEntry entry, bool queryHasKorean) { string text = (string.IsNullOrEmpty(entry.NameLower) ? entry.Name.ToLowerInvariant() : entry.NameLower); if (query.Length == 0) { return 0; } if (text == query) { return 1000 + entry.Score; } if (text.StartsWith(query)) { return 800 + entry.Score; } if (text.Contains(query)) { return 600 + entry.Score; } if (!queryHasKorean) { int num = FuzzyMatch(query, text); return (num > 0) ? (num + entry.Score) : 0; } int num2 = JamoContainsScoreFast(string.IsNullOrEmpty(entry.NameJamo) ? DecomposeToJamo(text) : entry.NameJamo, query); if (num2 > 0) { return num2 + entry.Score; } int num3 = ChosungMatchScoreFast(string.IsNullOrEmpty(entry.NameChosung) ? null : entry.NameChosung, text, query); if (num3 > 0) { return num3 + entry.Score; } int num4 = FuzzyMatch(query, text); if (num4 > 0) { return num4 + entry.Score; } return 0; } internal static int CalculateScore(string query, string target, int baseScore) { if (query.Length == 0) { return 0; } if (target == query) { return 1000 + baseScore; } if (target.StartsWith(query)) { return 800 + baseScore; } if (target.Contains(query)) { return 600 + baseScore; } int num = JamoContainsScore(target, query); if (num > 0) { return num + baseScore; } int num2 = ChosungMatchScore(target, query); if (num2 > 0) { return num2 + baseScore; } int num3 = FuzzyMatch(query, target); if (num3 > 0) { return num3 + baseScore; } return 0; } internal static int FuzzyMatch(string query, string target) { int num = 0; int num2 = 0; int num3 = 0; int num4 = -1; while (num < query.Length && num2 < target.Length) { if (query[num] == target[num2]) { num3 = ((num4 != num2 - 1) ? (num3 + 10) : (num3 + 30)); if (num2 == 0) { num3 += 15; } num4 = num2; num++; } num2++; } return (num == query.Length) ? Math.Max(num3, 50) : 0; } internal static string DecomposeToJamo(string text) { StringBuilder stringBuilder = new StringBuilder(text.Length * 3); foreach (char c in text) { if (c >= '가' && c <= '힣') { int num = c - 44032; int num2 = num / 588; int num3 = num % 588 / 28; int num4 = num % 28; stringBuilder.Append(Chosungs[num2]); stringBuilder.Append(Jungsungs[num3]); if (num4 > 0) { stringBuilder.Append(Jongsungs[num4]); } } else { stringBuilder.Append(c); } } return stringBuilder.ToString(); } internal static char GetChosung(char hangul) { if (hangul < '가' || hangul > '힣') { return '\0'; } int num = hangul - 44032; return Chosungs[num / 588]; } internal static int JamoContainsScore(string target, string query) { if (!HasKorean(query)) { return 0; } string text = DecomposeToJamo(target); string text2 = DecomposeToJamo(query); if (text2.Length == 0 || text.Length == 0) { return 0; } if (text.Contains(text2)) { return (text.IndexOf(text2) == 0) ? 580 : 550; } int num = 0; for (int i = 0; i < text.Length; i++) { if (num >= text2.Length) { break; } if (text2[num] == text[i]) { num++; } } if (num == text2.Length) { return 400; } return 0; } internal static bool HasChosung(string text) { return text.Any((char c) => ChosungSet.Contains(c)); } internal static bool IsChosung(string text) { return text.Length > 0 && text.All((char c) => ChosungSet.Contains(c)); } private static bool HasKorean(string text) { return text.Any((char c) => c >= '가' && c <= '힣'); } internal static int ChosungMatchScore(string target, string query) { if (!HasChosung(query)) { return 0; } List list = new List(); List list2 = new List(); foreach (char c in target) { char chosung = GetChosung(c); if (chosung != 0) { list.Add(chosung); list2.Add(c); } else if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) { list.Add(c); list2.Add(c); } } if (list.Count == 0) { return 0; } if (IsChosung(query)) { if (ContainsChosungConsecutive(list, query)) { return 520; } if (ContainsChosungSubsequence(list, query)) { return 480; } return 0; } return MixedChosungMatch(list2, list, query); } private static bool ContainsChosungConsecutive(List targetChosungs, string query) { for (int i = 0; i <= targetChosungs.Count - query.Length; i++) { bool flag = true; for (int j = 0; j < query.Length; j++) { if (targetChosungs[i + j] != query[j]) { flag = false; break; } } if (flag) { return true; } } return false; } private static bool ContainsChosungSubsequence(List targetChosungs, string query) { int num = 0; for (int i = 0; i < targetChosungs.Count; i++) { if (num >= query.Length) { break; } if (targetChosungs[i] == query[num]) { num++; } } return num == query.Length; } private static int MixedChosungMatch(List targetChars, List targetChosungs, string query) { int num = 0; int num2 = 0; while (num < query.Length && num2 < targetChars.Count) { char c = query[num]; if (ChosungSet.Contains(c)) { if (targetChosungs[num2] == c) { num++; } } else if (targetChars[num2] == c) { num++; } num2++; } return (num == query.Length) ? 460 : 0; } private static int JamoContainsScoreFast(string targetJamo, string query) { if (!HasKorean(query)) { return 0; } string text = DecomposeToJamo(query); if (text.Length == 0 || targetJamo.Length == 0) { return 0; } if (targetJamo.Contains(text)) { return (targetJamo.IndexOf(text, StringComparison.Ordinal) == 0) ? 580 : 550; } int num = 0; for (int i = 0; i < targetJamo.Length; i++) { if (num >= text.Length) { break; } if (text[num] == targetJamo[i]) { num++; } } return (num == text.Length) ? 400 : 0; } private static int ChosungMatchScoreFast(string? targetChosung, string targetLower, string query) { if (!HasChosung(query)) { return 0; } if (IsChosung(query)) { if (string.IsNullOrEmpty(targetChosung)) { return 0; } if (targetChosung.Contains(query, StringComparison.Ordinal)) { return 520; } int num = 0; for (int i = 0; i < targetChosung.Length; i++) { if (num >= query.Length) { break; } if (targetChosung[i] == query[num]) { num++; } } if (num == query.Length) { return 480; } return 0; } int num2 = 0; int num3 = 0; while (num2 < query.Length && num3 < targetLower.Length) { char c = query[num2]; char c2 = targetLower[num3]; if (ChosungSet.Contains(c)) { char c3 = GetChosung(c2); if (c3 == '\0' && ((c2 >= 'a' && c2 <= 'z') || (c2 >= '0' && c2 <= '9'))) { c3 = c2; } if (c3 == c) { num2++; } } else if (c2 == c) { num2++; } num3++; } return (num2 == query.Length) ? 460 : 0; } internal static bool ContainsChosung(string target, string chosungQuery) { List list = (from c in target.Select(GetChosung) where c != '\0' select c).ToList(); if (list.Count < chosungQuery.Length) { return false; } return ContainsChosungConsecutive(list, chosungQuery) || ContainsChosungSubsequence(list, chosungQuery); } }