11internal sealed class HangulComposer
21 private static readonly
string[] Cho =
23 "ㄱ",
"ㄲ",
"ㄴ",
"ㄷ",
"ㄸ",
"ㄹ",
"ㅁ",
"ㅂ",
"ㅃ",
"ㅅ",
24 "ㅆ",
"ㅇ",
"ㅈ",
"ㅉ",
"ㅊ",
"ㅋ",
"ㅌ",
"ㅍ",
"ㅎ"
35 private static readonly
string[] Jung =
37 "ㅏ",
"ㅐ",
"ㅑ",
"ㅒ",
"ㅓ",
"ㅔ",
"ㅕ",
"ㅖ",
"ㅗ",
"ㅘ",
38 "ㅙ",
"ㅚ",
"ㅛ",
"ㅜ",
"ㅝ",
"ㅞ",
"ㅟ",
"ㅠ",
"ㅡ",
"ㅢ",
"ㅣ"
49 private static readonly
string[] Jong =
51 "",
"ㄱ",
"ㄲ",
"ㄳ",
"ㄴ",
"ㄵ",
"ㄶ",
"ㄷ",
"ㄹ",
"ㄺ",
52 "ㄻ",
"ㄼ",
"ㄽ",
"ㄾ",
"ㄿ",
"ㅀ",
"ㅁ",
"ㅂ",
"ㅄ",
"ㅅ",
53 "ㅆ",
"ㅇ",
"ㅈ",
"ㅊ",
"ㅋ",
"ㅌ",
"ㅍ",
"ㅎ"
64 private static readonly Dictionary<string, int> ChoIndex = Cho
65 .Select((value, index) => (value, index))
66 .ToDictionary(x => x.value, x => x.index);
76 private static readonly Dictionary<string, int> JungIndex = Jung
77 .Select((value, index) => (value, index))
78 .ToDictionary(x => x.value, x => x.index);
88 private static readonly Dictionary<string, int> JongIndex = Jong
89 .Select((value, index) => (value, index))
90 .Where(x => x.value.Length > 0)
91 .ToDictionary(x => x.value, x => x.index);
101 private static readonly Dictionary<(
int First,
int Second),
int> CombinedJung =
new()
120 private static readonly Dictionary<(
int First,
int Second),
int> CombinedJong =
new()
143 private static readonly Dictionary<int, (
int First,
int Second)> SplitJong = CombinedJong
144 .ToDictionary(x => x.Value, x => x.Key);
154 private int _cho = -1;
163 private int _jung = -1;
221 public static bool IsComposableJamo(
string text)
223 return text.Length == 1 && (ChoIndex.ContainsKey(text) || JungIndex.ContainsKey(text));
250 public HangulEdit Input(
string text)
252 return Input(text,
string.Empty);
287 public HangulEdit Input(
string text,
string textBeforeCaret)
289 if (
string.IsNullOrEmpty(text) || text.Length != 1)
292 return new HangulEdit(0, text);
295 if (ChoIndex.TryGetValue(text, out var cho))
297 if (TryComposeConsonantWithTrailingText(textBeforeCaret, text, out var edit))
300 if (!
string.IsNullOrEmpty(textBeforeCaret))
303 return new HangulEdit(0, text);
306 return InputConsonant(text, cho);
309 if (JungIndex.TryGetValue(text, out var jung))
311 if (TryComposeVowelWithTrailingText(textBeforeCaret, jung, out var edit))
314 if (!
string.IsNullOrEmpty(textBeforeCaret))
317 return new HangulEdit(0, text);
320 return InputVowel(text, jung);
324 return new HangulEdit(0, text);
367 private bool TryComposeConsonantWithTrailingText(
string textBeforeCaret,
string text, out HangulEdit edit)
370 if (
string.IsNullOrEmpty(textBeforeCaret) || !JongIndex.TryGetValue(text, out var jong))
373 var last = textBeforeCaret[^1];
374 if (!TryDecompose(last, out var cho, out var jung, out var currentJong))
377 if (currentJong == 0)
382 edit =
new HangulEdit(1, Compose(_cho, _jung, _jong));
386 if (CombinedJong.TryGetValue((currentJong, jong), out var combinedJong))
390 _jong = combinedJong;
391 edit =
new HangulEdit(1, Compose(_cho, _jung, _jong));
438 private bool TryComposeVowelWithTrailingText(
string textBeforeCaret,
int jung, out HangulEdit edit)
441 if (
string.IsNullOrEmpty(textBeforeCaret))
444 var last = textBeforeCaret[^1].ToString();
445 if (ChoIndex.TryGetValue(last, out var trailingCho))
450 edit =
new HangulEdit(1, Compose(_cho, _jung, 0));
454 var lastChar = textBeforeCaret[^1];
455 if (!TryDecompose(lastChar, out var cho, out var currentJung, out var jong) || jong == 0)
458 if (SplitJong.TryGetValue(jong, out var split))
460 var previous = Compose(cho, currentJung, split.First);
461 _cho = ToChoIndex(Jong[split.Second]);
464 edit =
new HangulEdit(1, previous + Compose(_cho, _jung, 0));
468 _cho = ToChoIndex(Jong[jong]);
471 edit =
new HangulEdit(1, Compose(cho, currentJung, 0) + Compose(_cho, _jung, 0));
507 private HangulEdit InputConsonant(
string text,
int cho)
512 return new HangulEdit(0, text);
518 return new HangulEdit(0, text);
523 if (JongIndex.TryGetValue(text, out var jong))
526 return new HangulEdit(1, Compose(_cho, _jung, _jong));
532 return new HangulEdit(0, text);
535 if (JongIndex.TryGetValue(text, out var nextJong) &&
536 CombinedJong.TryGetValue((_jong, nextJong), out var combinedJong))
538 _jong = combinedJong;
539 return new HangulEdit(1, Compose(_cho, _jung, _jong));
545 return new HangulEdit(0, text);
580 private HangulEdit InputVowel(
string text,
int jung)
585 return new HangulEdit(0, text);
591 return new HangulEdit(1, Compose(_cho, _jung, 0));
596 if (CombinedJung.TryGetValue((_jung, jung), out var combinedJung))
598 _jung = combinedJung;
599 return new HangulEdit(1, Compose(_cho, _jung, 0));
603 return new HangulEdit(0, text);
606 var previousCho = _cho;
607 var previousJung = _jung;
608 var previousJong = _jong;
610 if (SplitJong.TryGetValue(previousJong, out var split))
612 var previous = Compose(previousCho, previousJung, split.First);
613 _cho = ToChoIndex(Jong[split.Second]);
616 return new HangulEdit(1, previous + Compose(_cho, _jung, 0));
619 _cho = ToChoIndex(Jong[previousJong]);
622 return new HangulEdit(1, Compose(previousCho, previousJung, 0) + Compose(_cho, _jung, 0));
649 private static int ToChoIndex(
string jong)
651 return ChoIndex.TryGetValue(jong, out var cho) ? cho : ChoIndex[
"ㅇ"];
702 private static string Compose(
int cho,
int jung,
int jong)
704 return char.ConvertFromUtf32(0xAC00 + ((cho * 21) + jung) * 28 + jong);
755 private static bool TryDecompose(
char value, out
int cho, out
int jung, out
int jong)
757 var code = value - 0xAC00;
758 if (code < 0 || code >= 11172)
766 cho = code / (21 * 28);
767 jung = (code % (21 * 28)) / 28;
797internal readonly record
struct HangulEdit(int ReplaceCount, string Text);