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.Select((v, i) => (v, i)).ToDictionary(x => x.v, x => x.i);
81 private static readonly Dictionary<string, int> JungIndex = Jung.Select((v, i) => (v, i)).ToDictionary(x => x.v, x => x.i);
90 private static readonly Dictionary<string, int> JongIndex = Jong.Select((v, i) => (v, i)).Where(x => x.v.Length > 0).ToDictionary(x => x.v, x => x.i);
100 private static readonly Dictionary<(
int First,
int Second),
int> CombinedJung =
new()
102 [(8, 0)] = 9, [(8, 1)] = 10, [(8, 20)] = 11,
103 [(13, 4)] = 14, [(13, 5)] = 15, [(13, 20)] = 16,
115 private static readonly Dictionary<(
int First,
int Second),
int> CombinedJong =
new()
117 [(1, 19)] = 3, [(4, 22)] = 5, [(4, 27)] = 6,
118 [(8, 1)] = 9, [(8, 16)] = 10, [(8, 17)] = 11,
119 [(8, 19)] = 12, [(8, 25)] = 13, [(8, 26)] = 14,
120 [(8, 27)] = 15, [(17, 19)] = 18,
131 private static readonly Dictionary<int, (
int First,
int Second)> SplitJong = CombinedJong.ToDictionary(x => x.Value, x => x.Key);
141 private int _cho = -1;
150 private int _jung = -1;
208 public static bool IsComposableJamo(
string text)
210 return text.Length == 1 && (ChoIndex.ContainsKey(text) || JungIndex.ContainsKey(text));
245 public HangulEdit Input(
string text,
string textBeforeCaret)
247 if (
string.IsNullOrEmpty(text) || text.Length != 1)
250 return new HangulEdit(0, text);
253 if (ChoIndex.TryGetValue(text, out var cho))
255 if (TryComposeConsonantWithTrailingText(textBeforeCaret, text, out var edit))
258 if (!
string.IsNullOrEmpty(textBeforeCaret))
261 return new HangulEdit(0, text);
264 return InputConsonant(text, cho);
267 if (JungIndex.TryGetValue(text, out var jung))
269 if (TryComposeVowelWithTrailingText(textBeforeCaret, jung, out var edit))
272 if (!
string.IsNullOrEmpty(textBeforeCaret))
275 return new HangulEdit(0, text);
278 return InputVowel(text, jung);
282 return new HangulEdit(0, text);
325 private bool TryComposeConsonantWithTrailingText(
string textBeforeCaret,
string text, out HangulEdit edit)
328 if (
string.IsNullOrEmpty(textBeforeCaret) || !JongIndex.TryGetValue(text, out var jong))
331 var last = textBeforeCaret[^1];
332 if (!TryDecompose(last, out var cho, out var jung, out var currentJong))
335 if (currentJong == 0)
340 edit =
new HangulEdit(1, Compose(_cho, _jung, _jong));
344 if (CombinedJong.TryGetValue((currentJong, jong), out var combinedJong))
348 _jong = combinedJong;
349 edit =
new HangulEdit(1, Compose(_cho, _jung, _jong));
396 private bool TryComposeVowelWithTrailingText(
string textBeforeCaret,
int jung, out HangulEdit edit)
399 if (
string.IsNullOrEmpty(textBeforeCaret))
402 var last = textBeforeCaret[^1].ToString();
403 if (ChoIndex.TryGetValue(last, out var trailingCho))
408 edit =
new HangulEdit(1, Compose(_cho, _jung, 0));
412 var lastChar = textBeforeCaret[^1];
413 if (!TryDecompose(lastChar, out var cho, out var currentJung, out var jong) || jong == 0)
416 if (SplitJong.TryGetValue(jong, out var split))
418 var previous = Compose(cho, currentJung, split.First);
419 _cho = ToChoIndex(Jong[split.Second]);
422 edit =
new HangulEdit(1, previous + Compose(_cho, _jung, 0));
426 _cho = ToChoIndex(Jong[jong]);
429 edit =
new HangulEdit(1, Compose(cho, currentJung, 0) + Compose(_cho, _jung, 0));
465 private HangulEdit InputConsonant(
string text,
int cho)
467 if (_cho < 0 || _jung < 0)
470 return new HangulEdit(0, text);
473 if (_jong == 0 && JongIndex.TryGetValue(text, out var jong))
476 return new HangulEdit(1, Compose(_cho, _jung, _jong));
479 if (JongIndex.TryGetValue(text, out var nextJong) && CombinedJong.TryGetValue((_jong, nextJong), out var combinedJong))
481 _jong = combinedJong;
482 return new HangulEdit(1, Compose(_cho, _jung, _jong));
488 return new HangulEdit(0, text);
523 private HangulEdit InputVowel(
string text,
int jung)
528 return new HangulEdit(0, text);
534 return new HangulEdit(1, Compose(_cho, _jung, 0));
537 if (_jong == 0 && CombinedJung.TryGetValue((_jung, jung), out var combinedJung))
539 _jung = combinedJung;
540 return new HangulEdit(1, Compose(_cho, _jung, 0));
544 return new HangulEdit(0, text);
571 private static int ToChoIndex(
string jong)
573 return ChoIndex.TryGetValue(jong, out var cho) ? cho : ChoIndex[
"ㅇ"];
624 private static string Compose(
int cho,
int jung,
int jong)
626 return char.ConvertFromUtf32(0xAC00 + ((cho * 21) + jung) * 28 + jong);
677 private static bool TryDecompose(
char value, out
int cho, out
int jung, out
int jong)
679 var code = value - 0xAC00;
680 if (code < 0 || code >= 11172)
688 cho = code / (21 * 28);
689 jung = (code % (21 * 28)) / 28;
719internal readonly record
struct HangulEdit(int ReplaceCount, string Text);