Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
IcaoNameComparer.cs
1using System;
3using System.Globalization;
4using System.Text;
5
7{
8 public static class IcaoNameComparer
9 {
10 private const int MaxVariants = 4096;
11 private const char Separator = ' ';
12
13 private sealed class VariantState
14 {
15 public VariantState(string text, bool atComponentStart, string lastEmission)
16 {
17 this.Text = text;
18 this.AtComponentStart = atComponentStart;
19 this.LastEmission = lastEmission;
20 }
21
22 public string Text { get; }
23 public bool AtComponentStart { get; }
24 public string LastEmission { get; }
25 }
26
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>()
33 {
34 '<', '-', '\'', '’', '.', ',', '/', '\\', '_', '·'
35 };
36
37 public static bool AreNamesSimilar(string s1, string s2)
38 {
39 if (s1 is null || s2 is null)
40 return string.Compare(s1, s2, true) == 0;
41
42 if (string.Compare(s1, s2, true) == 0)
43 return true;
44
45 HashSet<string> v1 = GenerateVariants(s1);
46 HashSet<string> v2 = GenerateVariants(s2);
47
48 if (Intersects(v1, v2))
49 return true;
50
51 HashSet<string> c1 = RemoveSeparators(v1);
52 HashSet<string> c2 = RemoveSeparators(v2);
53
54 return Intersects(c1, c2);
55 }
56
57 private static bool Intersects(HashSet<string> s1, HashSet<string> s2)
58 {
59 if (s1.Count > s2.Count)
60 {
61 HashSet<string> t = s1;
62 s1 = s2;
63 s2 = t;
64 }
65
66 foreach (string s in s1)
67 {
68 if (s2.Contains(s))
69 return true;
70 }
71
72 return false;
73 }
74
75 private static HashSet<string> RemoveSeparators(HashSet<string> source)
76 {
77 HashSet<string> result = new HashSet<string>(StringComparer.Ordinal);
78
79 foreach (string s in source)
80 {
81 if (s.IndexOf(Separator) < 0)
82 {
83 result.Add(s);
84 continue;
85 }
86
87 StringBuilder sb = new StringBuilder(s.Length);
88 foreach (char ch in s)
89 {
90 if (ch != Separator)
91 sb.Append(ch);
92 }
93
94 result.Add(sb.ToString());
95 }
96
97 return result;
98 }
99
100 private static HashSet<string> GenerateVariants(string input)
101 {
102 input = input.Normalize(NormalizationForm.FormC);
103
104 List<VariantState> states = new List<VariantState>()
105 {
106 new VariantState(string.Empty, true, null)
107 };
108
109 for (int i = 0; i < input.Length; i++)
110 {
111 char ch = input[i];
112 List<VariantState> next = new List<VariantState>();
113 HashSet<string> seen = new HashSet<string>(StringComparer.Ordinal);
114
115 foreach (VariantState state in states)
116 ApplyChar(input, i, ch, state, next, seen);
117
118 states = next;
119 if (states.Count == 0)
120 break;
121 }
122
123 HashSet<string> result = new HashSet<string>(StringComparer.Ordinal);
124 foreach (VariantState state in states)
125 {
126 string normalized = TrimAndCollapseSeparators(state.Text);
127 if (!string.IsNullOrEmpty(normalized))
128 result.Add(normalized);
129 }
130
131 if (result.Count == 0)
132 result.Add(string.Empty);
133
134 return result;
135 }
136
137 private static void ApplyChar(string input, int index, char ch, VariantState state, List<VariantState> next, HashSet<string> seen)
138 {
139 if (ArabicShaddaChars.Contains(ch))
140 {
141 if (!string.IsNullOrEmpty(state.LastEmission))
142 AddState(next, seen, state.Text + state.LastEmission, false, state.LastEmission);
143 else
144 AddState(next, seen, state.Text, state.AtComponentStart, state.LastEmission);
145 return;
146 }
147
148 if (IsIgnorableUnicode(ch))
149 {
150 AddState(next, seen, state.Text, state.AtComponentStart, state.LastEmission);
151 return;
152 }
153
154 if (IsSeparator(ch))
155 {
156 string text = AppendSeparator(state.Text);
157 AddState(next, seen, text, true, null);
158 return;
159 }
160
161 if (ArabicIgnoredChars.Contains(ch))
162 {
163 AddState(next, seen, state.Text, state.AtComponentStart, state.LastEmission);
164 return;
165 }
166
167 string[] mapped = GetMappedValues(ch, state.AtComponentStart, IsArabicTehMarbutaAtEndOfComponent(input, index, ch));
168 if (!(mapped is null))
169 {
170 foreach (string value in mapped)
171 AddState(next, seen, state.Text + value, false, value);
172 return;
173 }
174
175 if (TryGetAsciiFallback(ch, out string fallback))
176 {
177 AddState(next, seen, state.Text + fallback, false, fallback);
178 return;
179 }
180
181 if (char.IsLetterOrDigit(ch))
182 {
183 string upper = ch.ToString().ToUpperInvariant();
184 AddState(next, seen, state.Text + upper, false, upper);
185 return;
186 }
187
188 AddState(next, seen, state.Text, state.AtComponentStart, state.LastEmission);
189 }
190
191 private static bool TryGetAsciiFallback(char ch, out string fallback)
192 {
193 fallback = null;
194
195 if (char.IsLetterOrDigit(ch))
196 {
197 string s = ch.ToString();
198 string stripped = RemoveDiacritics(s).Normalize(NormalizationForm.FormC).ToUpperInvariant();
199 if (!string.IsNullOrEmpty(stripped) && IsAsciiLettersDigitsOrX(stripped))
200 {
201 fallback = stripped;
202 return true;
203 }
204
205 string upper = s.ToUpperInvariant();
206 if (IsAsciiLettersDigitsOrX(upper))
207 {
208 fallback = upper;
209 return true;
210 }
211 }
212
213 return false;
214 }
215
221 public static string RemoveDiacritics(string s)
222 {
223 string FormD = s.Normalize(NormalizationForm.FormD); // Diacritics become special characters
224 StringBuilder sb = new StringBuilder();
225
226 foreach (char ch in FormD)
227 {
228 UnicodeCategory Category = CharUnicodeInfo.GetUnicodeCategory(ch);
229 if (Category != UnicodeCategory.NonSpacingMark)
230 {
231 switch (ch)
232 {
233 case 'Đ': sb.Append('D'); break;
234 case 'đ': sb.Append('d'); break;
235 default: sb.Append(ch); break;
236 }
237 }
238 }
239
240 return sb.ToString().Normalize(NormalizationForm.FormC);
241 }
242
243 private static bool IsAsciiLettersDigitsOrX(string s)
244 {
245 foreach (char ch in s)
246 {
247 if (!(ch >= 'A' && ch <= 'Z') && !(ch >= '0' && ch <= '9'))
248 return false;
249 }
250
251 return true;
252 }
253
254 private static string[] GetMappedValues(char ch, bool atComponentStart, bool isArabicTehMarbutaEnd)
255 {
256 if (ch == '\u0629')
257 return isArabicTehMarbutaEnd ? new string[] { "XAH" } : new string[] { "XTA" };
258
259 if (ArabicMappings.TryGetValue(ch, out string[] arabic))
260 return arabic;
261
262 if (LatinMappings.TryGetValue(ch, out string[] latin))
263 return latin;
264
265 char upper = char.ToUpperInvariant(ch);
266
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" };
277
278 if (CyrillicMappings.TryGetValue(upper, out string[] cyr))
279 return cyr;
280
281 return null;
282 }
283
284 private static bool IsArabicTehMarbutaAtEndOfComponent(string input, int index, char ch)
285 {
286 if (ch != '\u0629')
287 return false;
288
289 for (int i = index + 1; i < input.Length; i++)
290 {
291 char c = input[i];
292
293 if (IsIgnorableUnicode(c) || ArabicIgnoredChars.Contains(c) || ArabicShaddaChars.Contains(c))
294 continue;
295
296 if (IsSeparator(c))
297 return true;
298
299 return false;
300 }
301
302 return true;
303 }
304
305 private static bool IsIgnorableUnicode(char ch)
306 {
307 UnicodeCategory category = CharUnicodeInfo.GetUnicodeCategory(ch);
308 return category == UnicodeCategory.NonSpacingMark
309 || category == UnicodeCategory.SpacingCombiningMark
310 || category == UnicodeCategory.EnclosingMark
311 || category == UnicodeCategory.Format;
312 }
313
314 private static bool IsSeparator(char ch)
315 {
316 return char.IsWhiteSpace(ch) || SeparatorChars.Contains(ch);
317 }
318
319 private static string AppendSeparator(string text)
320 {
321 if (string.IsNullOrEmpty(text) || text[text.Length - 1] == Separator)
322 return text;
323
324 return text + Separator;
325 }
326
327 private static string TrimAndCollapseSeparators(string text)
328 {
329 if (string.IsNullOrEmpty(text))
330 return string.Empty;
331
332 StringBuilder sb = new StringBuilder(text.Length);
333 bool lastWasSeparator = true;
334
335 foreach (char ch in text)
336 {
337 if (ch == Separator)
338 {
339 if (!lastWasSeparator)
340 {
341 sb.Append(Separator);
342 lastWasSeparator = true;
343 }
344 }
345 else
346 {
347 sb.Append(ch);
348 lastWasSeparator = false;
349 }
350 }
351
352 if (sb.Length > 0 && sb[sb.Length - 1] == Separator)
353 sb.Length--;
354
355 return sb.ToString();
356 }
357
358 private static void AddState(List<VariantState> next, HashSet<string> seen, string text, bool atComponentStart, string lastEmission)
359 {
360 string key = text + "\u001F" + (atComponentStart ? "1" : "0") + "\u001F" + (lastEmission ?? string.Empty);
361 if (!seen.Add(key))
362 return;
363
364 if (next.Count >= MaxVariants)
365 return;
366
367 next.Add(new VariantState(text, atComponentStart, lastEmission));
368 }
369
370 private static Dictionary<char, string[]> CreateLatinMappings()
371 {
372 Dictionary<char, string[]> d = new Dictionary<char, string[]>();
373
374 void Add(char ch, params string[] values)
375 {
376 d[ch] = values;
377 char lower = char.ToLowerInvariant(ch);
378 d[lower] = values;
379 }
380
381 Add('\u00C0', "A");
382 Add('\u00C1', "A");
383 Add('\u00C2', "A");
384 Add('\u00C3', "A");
385 Add('\u00C4', "AE", "A");
386 Add('\u00C5', "AA", "A");
387 Add('\u00C6', "AE");
388 Add('\u00C7', "C");
389 Add('\u00C8', "E");
390 Add('\u00C9', "E");
391 Add('\u00CA', "E");
392 Add('\u00CB', "E");
393 Add('\u00CC', "I");
394 Add('\u00CD', "I");
395 Add('\u00CE', "I");
396 Add('\u00CF', "I");
397 Add('\u00D0', "D");
398 Add('\u00D1', "N", "NXX");
399 Add('\u00D2', "O");
400 Add('\u00D3', "O");
401 Add('\u00D4', "O");
402 Add('\u00D5', "O");
403 Add('\u00D6', "OE", "O");
404 Add('\u00D8', "OE");
405 Add('\u00D9', "U");
406 Add('\u00DA', "U");
407 Add('\u00DB', "U");
408 Add('\u00DC', "UE", "UXX", "U");
409 Add('\u00DD', "Y");
410 Add('\u00DE', "TH");
411 Add('\u0100', "A");
412 Add('\u0102', "A");
413 Add('\u0104', "A");
414 Add('\u0106', "C");
415 Add('\u0108', "C");
416 Add('\u010A', "C");
417 Add('\u010C', "C");
418 Add('\u010E', "D");
419 Add('\u0110', "D");
420 Add('\u0112', "E");
421 Add('\u0114', "E");
422 Add('\u0116', "E");
423 Add('\u0118', "E");
424 Add('\u011A', "E");
425 Add('\u011C', "G");
426 Add('\u011E', "G");
427 Add('\u0120', "G");
428 Add('\u0122', "G");
429 Add('\u0124', "H");
430 Add('\u0126', "H");
431 Add('\u0128', "I");
432 Add('\u012A', "I");
433 Add('\u012C', "I");
434 Add('\u012E', "I");
435 Add('\u0130', "I");
436 Add('\u0131', "I");
437 Add('\u0132', "IJ");
438 Add('\u0134', "J");
439 Add('\u0136', "K");
440 Add('\u0139', "L");
441 Add('\u013B', "L");
442 Add('\u013D', "L");
443 Add('\u013F', "L");
444 Add('\u0141', "L");
445 Add('\u0143', "N");
446 Add('\u0145', "N");
447 Add('\u0147', "N");
448 Add('\u014A', "N");
449 Add('\u014C', "O");
450 Add('\u014E', "O");
451 Add('\u0150', "O");
452 Add('\u0152', "OE");
453 Add('\u0154', "R");
454 Add('\u0156', "R");
455 Add('\u0158', "R");
456 Add('\u015A', "S");
457 Add('\u015C', "S");
458 Add('\u015E', "S");
459 Add('\u0160', "S");
460 Add('\u0162', "T");
461 Add('\u0164', "T");
462 Add('\u0166', "T");
463 Add('\u0168', "U");
464 Add('\u016A', "U");
465 Add('\u016C', "U");
466 Add('\u016E', "U");
467 Add('\u0170', "U");
468 Add('\u0172', "U");
469 Add('\u0174', "W");
470 Add('\u0176', "Y");
471 Add('\u0178', "Y");
472 Add('\u0179', "Z");
473 Add('\u017B', "Z");
474 Add('\u017D', "Z");
475 Add('\u1E9E', "SS");
476 d['\u00DF'] = new string[] { "SS" };
477
478 return d;
479 }
480
481 private static Dictionary<char, string[]> CreateCyrillicMappings()
482 {
483 Dictionary<char, string[]> d = new Dictionary<char, string[]>();
484
485 void Add(char ch, params string[] values)
486 {
487 d[ch] = values;
488 char lower = char.ToLowerInvariant(ch);
489 d[lower] = values;
490 }
491
492 Add('\u0401', "E", "IO");
493 Add('\u0402', "D", "DJ");
494 Add('\u040B', "D");
495 Add('\u0405', "DZ");
496 Add('\u0406', "I");
497 Add('\u0408', "J");
498 Add('\u0409', "LJ");
499 Add('\u040A', "NJ");
500 Add('\u040C', "K", "KJ");
501 Add('\u040E', "U");
502 Add('\u040F', "DZ", "DJ");
503 Add('\u0410', "A");
504 Add('\u0411', "B");
505 Add('\u0412', "V");
506 Add('\u0413', "G", "H");
507 Add('\u0414', "D");
508 Add('\u0415', "E");
509 Add('\u0416', "ZH", "Z");
510 Add('\u0417', "Z");
511 Add('\u0418', "I", "Y");
512 Add('\u041A', "K");
513 Add('\u041B', "L");
514 Add('\u041C', "M");
515 Add('\u041D', "N");
516 Add('\u041E', "O");
517 Add('\u041F', "P");
518 Add('\u0420', "R");
519 Add('\u0421', "S");
520 Add('\u0422', "T");
521 Add('\u0423', "U");
522 Add('\u0424', "F");
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");
528 Add('\u042A', "IE");
529 Add('\u042B', "Y");
530 Add('\u042D', "E");
531 Add('\u046A', "U");
532 Add('\u0474', "Y");
533 Add('\u0490', "G");
534 Add('\u0492', "G", "GJ");
535 Add('\u04BA', "C");
536
537 return d;
538 }
539
540 private static Dictionary<char, string[]> CreateArabicMappings()
541 {
542 return new Dictionary<char, string[]>()
543 {
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" }
609 };
610 }
611
612 private static HashSet<char> CreateArabicIgnoredChars()
613 {
614 return new HashSet<char>()
615 {
616 '\u0640',
617 '\u064B',
618 '\u064C',
619 '\u064D',
620 '\u064E',
621 '\u064F',
622 '\u0650',
623 '\u0652',
624 '\u0670',
625 '\u069C',
626 '\u06A2',
627 '\u06A7',
628 '\u06A8'
629 };
630 }
631 }
632}
static string RemoveDiacritics(string s)
Removes diacritics from a string.