19internal sealed class HangulComposer
29 private static readonly
string[] Cho =
31 "ㄱ",
"ㄲ",
"ㄴ",
"ㄷ",
"ㄸ",
"ㄹ",
"ㅁ",
"ㅂ",
"ㅃ",
"ㅅ",
32 "ㅆ",
"ㅇ",
"ㅈ",
"ㅉ",
"ㅊ",
"ㅋ",
"ㅌ",
"ㅍ",
"ㅎ"
43 private static readonly
string[] Jung =
45 "ㅏ",
"ㅐ",
"ㅑ",
"ㅒ",
"ㅓ",
"ㅔ",
"ㅕ",
"ㅖ",
"ㅗ",
"ㅘ",
46 "ㅙ",
"ㅚ",
"ㅛ",
"ㅜ",
"ㅝ",
"ㅞ",
"ㅟ",
"ㅠ",
"ㅡ",
"ㅢ",
"ㅣ"
57 private static readonly
string[] Jong =
59 "",
"ㄱ",
"ㄲ",
"ㄳ",
"ㄴ",
"ㄵ",
"ㄶ",
"ㄷ",
"ㄹ",
"ㄺ",
60 "ㄻ",
"ㄼ",
"ㄽ",
"ㄾ",
"ㄿ",
"ㅀ",
"ㅁ",
"ㅂ",
"ㅄ",
"ㅅ",
61 "ㅆ",
"ㅇ",
"ㅈ",
"ㅊ",
"ㅋ",
"ㅌ",
"ㅍ",
"ㅎ"
72 private static readonly Dictionary<string, int> ChoIndex = Cho
73 .Select((value, index) => (value, index))
74 .ToDictionary(x => x.value, x => x.index);
84 private static readonly Dictionary<string, int> JungIndex = Jung
85 .Select((value, index) => (value, index))
86 .ToDictionary(x => x.value, x => x.index);
96 private static readonly Dictionary<string, int> JongIndex = Jong
97 .Select((value, index) => (value, index))
98 .Where(x => x.value.Length > 0)
99 .ToDictionary(x => x.value, x => x.index);
109 private static readonly Dictionary<(
int First,
int Second),
int> CombinedJung =
new()
128 private static readonly Dictionary<(
int First,
int Second),
int> CombinedJong =
new()
151 private static readonly Dictionary<int, (
int First,
int Second)> SplitJong = CombinedJong
152 .ToDictionary(x => x.Value, x => x.Key);
162 private int _cho = -1;
171 private int _jung = -1;
229 public static bool IsComposableJamo(
string text)
231 return text.Length == 1 && (ChoIndex.ContainsKey(text) || JungIndex.ContainsKey(text));
258 public HangulEdit Input(
string text)
260 return Input(text,
string.Empty);
295 public HangulEdit Input(
string text,
string textBeforeCaret)
297 if (
string.IsNullOrEmpty(text) || text.Length != 1)
300 return new HangulEdit(0, text);
303 if (ChoIndex.TryGetValue(text, out var cho))
305 if (TryComposeConsonantWithTrailingText(textBeforeCaret, text, out var edit))
308 if (!
string.IsNullOrEmpty(textBeforeCaret))
311 return new HangulEdit(0, text);
314 return InputConsonant(text, cho);
317 if (JungIndex.TryGetValue(text, out var jung))
319 if (TryComposeVowelWithTrailingText(textBeforeCaret, jung, out var edit))
322 if (!
string.IsNullOrEmpty(textBeforeCaret))
325 return new HangulEdit(0, text);
328 return InputVowel(text, jung);
332 return new HangulEdit(0, text);
375 private bool TryComposeConsonantWithTrailingText(
string textBeforeCaret,
string text, out HangulEdit edit)
378 if (
string.IsNullOrEmpty(textBeforeCaret) || !JongIndex.TryGetValue(text, out var jong))
381 var last = textBeforeCaret[^1];
382 if (!TryDecompose(last, out var cho, out var jung, out var currentJong))
385 if (currentJong == 0)
390 edit =
new HangulEdit(1, Compose(_cho, _jung, _jong));
394 if (CombinedJong.TryGetValue((currentJong, jong), out var combinedJong))
398 _jong = combinedJong;
399 edit =
new HangulEdit(1, Compose(_cho, _jung, _jong));
446 private bool TryComposeVowelWithTrailingText(
string textBeforeCaret,
int jung, out HangulEdit edit)
449 if (
string.IsNullOrEmpty(textBeforeCaret))
452 var last = textBeforeCaret[^1].ToString();
453 if (ChoIndex.TryGetValue(last, out var trailingCho))
458 edit =
new HangulEdit(1, Compose(_cho, _jung, 0));
462 var lastChar = textBeforeCaret[^1];
463 if (!TryDecompose(lastChar, out var cho, out var currentJung, out var jong) || jong == 0)
466 if (SplitJong.TryGetValue(jong, out var split))
468 var previous = Compose(cho, currentJung, split.First);
469 _cho = ToChoIndex(Jong[split.Second]);
472 edit =
new HangulEdit(1, previous + Compose(_cho, _jung, 0));
476 _cho = ToChoIndex(Jong[jong]);
479 edit =
new HangulEdit(1, Compose(cho, currentJung, 0) + Compose(_cho, _jung, 0));
515 private HangulEdit InputConsonant(
string text,
int cho)
520 return new HangulEdit(0, text);
526 return new HangulEdit(0, text);
531 if (JongIndex.TryGetValue(text, out var jong))
534 return new HangulEdit(1, Compose(_cho, _jung, _jong));
540 return new HangulEdit(0, text);
543 if (JongIndex.TryGetValue(text, out var nextJong) &&
544 CombinedJong.TryGetValue((_jong, nextJong), out var combinedJong))
546 _jong = combinedJong;
547 return new HangulEdit(1, Compose(_cho, _jung, _jong));
553 return new HangulEdit(0, text);
588 private HangulEdit InputVowel(
string text,
int jung)
593 return new HangulEdit(0, text);
599 return new HangulEdit(1, Compose(_cho, _jung, 0));
604 if (CombinedJung.TryGetValue((_jung, jung), out var combinedJung))
606 _jung = combinedJung;
607 return new HangulEdit(1, Compose(_cho, _jung, 0));
611 return new HangulEdit(0, text);
614 var previousCho = _cho;
615 var previousJung = _jung;
616 var previousJong = _jong;
618 if (SplitJong.TryGetValue(previousJong, out var split))
620 var previous = Compose(previousCho, previousJung, split.First);
621 _cho = ToChoIndex(Jong[split.Second]);
624 return new HangulEdit(1, previous + Compose(_cho, _jung, 0));
627 _cho = ToChoIndex(Jong[previousJong]);
630 return new HangulEdit(1, Compose(previousCho, previousJung, 0) + Compose(_cho, _jung, 0));
657 private static int ToChoIndex(
string jong)
659 return ChoIndex.TryGetValue(jong, out var cho) ? cho : ChoIndex[
"ㅇ"];
710 private static string Compose(
int cho,
int jung,
int jong)
712 return char.ConvertFromUtf32(0xAC00 + ((cho * 21) + jung) * 28 + jong);
763 private static bool TryDecompose(
char value, out
int cho, out
int jung, out
int jong)
765 var code = value - 0xAC00;
766 if (code < 0 || code >= 11172)
774 cho = code / (21 * 28);
775 jung = (code % (21 * 28)) / 28;
805internal readonly record
struct HangulEdit(int ReplaceCount, string Text);