3using System.Globalization;
10 private const int MaxVariants = 4096;
11 private const char Separator =
' ';
13 private sealed
class VariantState
15 public VariantState(
string text,
bool atComponentStart,
string lastEmission)
18 this.AtComponentStart = atComponentStart;
19 this.LastEmission = lastEmission;
22 public string Text {
get; }
23 public bool AtComponentStart {
get; }
24 public string LastEmission {
get; }
27 private static readonly Dictionary<char, string[]> LatinMappings = CreateLatinMappings();
28 private static readonly Dictionary<char, string[]> CyrillicMappings = CreateCyrillicMappings();
29 private static readonly Dictionary<char, string[]> ArabicMappings = CreateArabicMappings();
30 private static readonly HashSet<char> ArabicIgnoredChars = CreateArabicIgnoredChars();
31 private static readonly HashSet<char> ArabicShaddaChars =
new HashSet<char>() {
'\u0651' };
32 private static readonly HashSet<char> SeparatorChars =
new HashSet<char>()
34 '<',
'-',
'\'',
'’',
'.',
',',
'/',
'\\',
'_',
'·'
37 public static bool AreNamesSimilar(
string s1,
string s2)
39 if (s1 is
null || s2 is
null)
40 return string.Compare(s1, s2,
true) == 0;
42 if (
string.Compare(s1, s2,
true) == 0)
45 HashSet<string> v1 = GenerateVariants(s1);
46 HashSet<string> v2 = GenerateVariants(s2);
48 if (Intersects(v1, v2))
51 HashSet<string> c1 = RemoveSeparators(v1);
52 HashSet<string> c2 = RemoveSeparators(v2);
54 return Intersects(c1, c2);
57 private static bool Intersects(HashSet<string> s1, HashSet<string> s2)
59 if (s1.Count > s2.Count)
61 HashSet<string> t = s1;
66 foreach (
string s
in s1)
75 private static HashSet<string> RemoveSeparators(HashSet<string> source)
77 HashSet<string> result =
new HashSet<string>(StringComparer.Ordinal);
79 foreach (
string s
in source)
81 if (s.IndexOf(Separator) < 0)
87 StringBuilder sb =
new StringBuilder(s.Length);
88 foreach (
char ch
in s)
94 result.Add(sb.ToString());
100 private static HashSet<string> GenerateVariants(
string input)
102 input = input.Normalize(NormalizationForm.FormC);
104 List<VariantState> states =
new List<VariantState>()
106 new VariantState(
string.Empty,
true,
null)
109 for (
int i = 0; i < input.Length; i++)
112 List<VariantState> next =
new List<VariantState>();
113 HashSet<string> seen =
new HashSet<string>(StringComparer.Ordinal);
115 foreach (VariantState state
in states)
116 ApplyChar(input, i, ch, state, next, seen);
119 if (states.Count == 0)
123 HashSet<string> result =
new HashSet<string>(StringComparer.Ordinal);
124 foreach (VariantState state
in states)
126 string normalized = TrimAndCollapseSeparators(state.Text);
127 if (!
string.IsNullOrEmpty(normalized))
128 result.Add(normalized);
131 if (result.Count == 0)
132 result.Add(
string.Empty);
137 private static void ApplyChar(
string input,
int index,
char ch, VariantState state, List<VariantState> next, HashSet<string> seen)
139 if (ArabicShaddaChars.Contains(ch))
141 if (!
string.IsNullOrEmpty(state.LastEmission))
142 AddState(next, seen, state.Text + state.LastEmission,
false, state.LastEmission);
144 AddState(next, seen, state.Text, state.AtComponentStart, state.LastEmission);
148 if (IsIgnorableUnicode(ch))
150 AddState(next, seen, state.Text, state.AtComponentStart, state.LastEmission);
156 string text = AppendSeparator(state.Text);
157 AddState(next, seen, text,
true,
null);
161 if (ArabicIgnoredChars.Contains(ch))
163 AddState(next, seen, state.Text, state.AtComponentStart, state.LastEmission);
167 string[] mapped = GetMappedValues(ch, state.AtComponentStart, IsArabicTehMarbutaAtEndOfComponent(input, index, ch));
168 if (!(mapped is
null))
170 foreach (
string value
in mapped)
171 AddState(next, seen, state.Text + value,
false, value);
175 if (TryGetAsciiFallback(ch, out
string fallback))
177 AddState(next, seen, state.Text + fallback,
false, fallback);
181 if (
char.IsLetterOrDigit(ch))
183 string upper = ch.ToString().ToUpperInvariant();
184 AddState(next, seen, state.Text + upper,
false, upper);
188 AddState(next, seen, state.Text, state.AtComponentStart, state.LastEmission);
191 private static bool TryGetAsciiFallback(
char ch, out
string fallback)
195 if (
char.IsLetterOrDigit(ch))
197 string s = ch.ToString();
198 string stripped =
RemoveDiacritics(s).Normalize(NormalizationForm.FormC).ToUpperInvariant();
199 if (!
string.IsNullOrEmpty(stripped) && IsAsciiLettersDigitsOrX(stripped))
205 string upper = s.ToUpperInvariant();
206 if (IsAsciiLettersDigitsOrX(upper))
223 string FormD = s.Normalize(NormalizationForm.FormD);
224 StringBuilder sb =
new StringBuilder();
226 foreach (
char ch
in FormD)
228 UnicodeCategory Category = CharUnicodeInfo.GetUnicodeCategory(ch);
229 if (Category != UnicodeCategory.NonSpacingMark)
233 case 'Đ': sb.Append(
'D');
break;
234 case 'đ': sb.Append(
'd');
break;
235 default: sb.Append(ch);
break;
240 return sb.ToString().Normalize(NormalizationForm.FormC);
243 private static bool IsAsciiLettersDigitsOrX(
string s)
245 foreach (
char ch
in s)
247 if (!(ch >=
'A' && ch <=
'Z') && !(ch >=
'0' && ch <=
'9'))
254 private static string[] GetMappedValues(
char ch,
bool atComponentStart,
bool isArabicTehMarbutaEnd)
257 return isArabicTehMarbutaEnd ?
new string[] {
"XAH" } :
new string[] {
"XTA" };
259 if (ArabicMappings.TryGetValue(ch, out
string[] arabic))
262 if (LatinMappings.TryGetValue(ch, out
string[] latin))
265 char upper =
char.ToUpperInvariant(ch);
267 if (upper ==
'\u0404')
268 return atComponentStart ?
new string[] {
"IE",
"YE" } :
new string[] {
"IE" };
269 if (upper ==
'\u0407')
270 return atComponentStart ?
new string[] {
"I",
"YI" } :
new string[] {
"I" };
271 if (upper ==
'\u0419')
272 return atComponentStart ?
new string[] {
"I",
"Y" } :
new string[] {
"I" };
273 if (upper ==
'\u042E')
274 return atComponentStart ?
new string[] {
"IU",
"YU" } :
new string[] {
"IU" };
275 if (upper ==
'\u042F')
276 return atComponentStart ?
new string[] {
"IA",
"YA" } :
new string[] {
"IA" };
278 if (CyrillicMappings.TryGetValue(upper, out
string[] cyr))
284 private static bool IsArabicTehMarbutaAtEndOfComponent(
string input,
int index,
char ch)
289 for (
int i = index + 1; i < input.Length; i++)
293 if (IsIgnorableUnicode(c) || ArabicIgnoredChars.Contains(c) || ArabicShaddaChars.Contains(c))
305 private static bool IsIgnorableUnicode(
char ch)
307 UnicodeCategory category = CharUnicodeInfo.GetUnicodeCategory(ch);
308 return category == UnicodeCategory.NonSpacingMark
309 || category == UnicodeCategory.SpacingCombiningMark
310 || category == UnicodeCategory.EnclosingMark
311 || category == UnicodeCategory.Format;
314 private static bool IsSeparator(
char ch)
316 return char.IsWhiteSpace(ch) || SeparatorChars.Contains(ch);
319 private static string AppendSeparator(
string text)
321 if (
string.IsNullOrEmpty(text) || text[text.Length - 1] == Separator)
324 return text + Separator;
327 private static string TrimAndCollapseSeparators(
string text)
329 if (
string.IsNullOrEmpty(text))
332 StringBuilder sb =
new StringBuilder(text.Length);
333 bool lastWasSeparator =
true;
335 foreach (
char ch
in text)
339 if (!lastWasSeparator)
341 sb.Append(Separator);
342 lastWasSeparator =
true;
348 lastWasSeparator =
false;
352 if (sb.Length > 0 && sb[sb.Length - 1] == Separator)
355 return sb.ToString();
358 private static void AddState(List<VariantState> next, HashSet<string> seen,
string text,
bool atComponentStart,
string lastEmission)
360 string key = text +
"\u001F" + (atComponentStart ?
"1" :
"0") +
"\u001F" + (lastEmission ??
string.Empty);
364 if (next.Count >= MaxVariants)
367 next.Add(
new VariantState(text, atComponentStart, lastEmission));
370 private static Dictionary<char, string[]> CreateLatinMappings()
372 Dictionary<char, string[]> d =
new Dictionary<char, string[]>();
374 void Add(
char ch, params
string[] values)
377 char lower =
char.ToLowerInvariant(ch);
385 Add(
'\u00C4',
"AE",
"A");
386 Add(
'\u00C5',
"AA",
"A");
398 Add(
'\u00D1',
"N",
"NXX");
403 Add(
'\u00D6',
"OE",
"O");
408 Add(
'\u00DC',
"UE",
"UXX",
"U");
476 d[
'\u00DF'] =
new string[] {
"SS" };
481 private static Dictionary<char, string[]> CreateCyrillicMappings()
483 Dictionary<char, string[]> d =
new Dictionary<char, string[]>();
485 void Add(
char ch, params
string[] values)
488 char lower =
char.ToLowerInvariant(ch);
492 Add(
'\u0401',
"E",
"IO");
493 Add(
'\u0402',
"D",
"DJ");
500 Add(
'\u040C',
"K",
"KJ");
502 Add(
'\u040F',
"DZ",
"DJ");
506 Add(
'\u0413',
"G",
"H");
509 Add(
'\u0416',
"ZH",
"Z");
511 Add(
'\u0418',
"I",
"Y");
523 Add(
'\u0425',
"KH",
"H");
524 Add(
'\u0426',
"TS",
"C");
525 Add(
'\u0427',
"CH",
"C");
526 Add(
'\u0428',
"SH",
"S");
527 Add(
'\u0429',
"SHCH",
"SHT");
534 Add(
'\u0492',
"G",
"GJ");
540 private static Dictionary<char, string[]> CreateArabicMappings()
542 return new Dictionary<char, string[]>()
544 [
'\u0621'] =
new string[] {
"XE" },
545 [
'\u0622'] =
new string[] {
"XAA" },
546 [
'\u0623'] =
new string[] {
"XAE" },
547 [
'\u0624'] =
new string[] {
"U" },
548 [
'\u0625'] =
new string[] {
"I" },
549 [
'\u0626'] =
new string[] {
"XI" },
550 [
'\u0627'] =
new string[] {
"A" },
551 [
'\u0628'] =
new string[] {
"B" },
552 [
'\u062A'] =
new string[] {
"T" },
553 [
'\u062B'] =
new string[] {
"XTH" },
554 [
'\u062C'] =
new string[] {
"J" },
555 [
'\u062D'] =
new string[] {
"XH" },
556 [
'\u062E'] =
new string[] {
"XKH" },
557 [
'\u062F'] =
new string[] {
"D" },
558 [
'\u0630'] =
new string[] {
"XDH" },
559 [
'\u0631'] =
new string[] {
"R" },
560 [
'\u0632'] =
new string[] {
"Z" },
561 [
'\u0633'] =
new string[] {
"S" },
562 [
'\u0634'] =
new string[] {
"XSH" },
563 [
'\u0635'] =
new string[] {
"XSS" },
564 [
'\u0636'] =
new string[] {
"XDZ" },
565 [
'\u0637'] =
new string[] {
"XTT" },
566 [
'\u0638'] =
new string[] {
"XZZ" },
567 [
'\u0639'] =
new string[] {
"E" },
568 [
'\u063A'] =
new string[] {
"G" },
569 [
'\u0641'] =
new string[] {
"F" },
570 [
'\u0642'] =
new string[] {
"Q" },
571 [
'\u0643'] =
new string[] {
"K" },
572 [
'\u0644'] =
new string[] {
"L" },
573 [
'\u0645'] =
new string[] {
"M" },
574 [
'\u0646'] =
new string[] {
"N" },
575 [
'\u0647'] =
new string[] {
"H" },
576 [
'\u0648'] =
new string[] {
"W" },
577 [
'\u0649'] =
new string[] {
"XAY" },
578 [
'\u064A'] =
new string[] {
"Y" },
579 [
'\u0671'] =
new string[] {
"XXA" },
580 [
'\u0679'] =
new string[] {
"XXT" },
581 [
'\u067C'] =
new string[] {
"XRT" },
582 [
'\u067E'] =
new string[] {
"P" },
583 [
'\u0681'] =
new string[] {
"XKE" },
584 [
'\u0685'] =
new string[] {
"XXH" },
585 [
'\u0686'] =
new string[] {
"XC" },
586 [
'\u0688'] =
new string[] {
"XXD" },
587 [
'\u0689'] =
new string[] {
"XDR" },
588 [
'\u0691'] =
new string[] {
"XXR" },
589 [
'\u0693'] =
new string[] {
"XRR" },
590 [
'\u0696'] =
new string[] {
"XRX" },
591 [
'\u0698'] =
new string[] {
"XJ" },
592 [
'\u069A'] =
new string[] {
"XXS" },
593 [
'\u06A9'] =
new string[] {
"XKK" },
594 [
'\u06AB'] =
new string[] {
"XXK" },
595 [
'\u06AD'] =
new string[] {
"XNG" },
596 [
'\u06AF'] =
new string[] {
"XGG" },
597 [
'\u06BA'] =
new string[] {
"XNN" },
598 [
'\u06BC'] =
new string[] {
"XXN" },
599 [
'\u06BE'] =
new string[] {
"XDO" },
600 [
'\u06C0'] =
new string[] {
"XYH" },
601 [
'\u06C1'] =
new string[] {
"XXG" },
602 [
'\u06C2'] =
new string[] {
"XGE" },
603 [
'\u06C3'] =
new string[] {
"XTG" },
604 [
'\u06CC'] =
new string[] {
"XYA" },
605 [
'\u06CD'] =
new string[] {
"XXY" },
606 [
'\u06D0'] =
new string[] {
"Y" },
607 [
'\u06D2'] =
new string[] {
"XYB" },
608 [
'\u06D3'] =
new string[] {
"XBE" }
612 private static HashSet<char> CreateArabicIgnoredChars()
614 return new HashSet<char>()
static string RemoveDiacritics(string s)
Removes diacritics from a string.