Skip to main content

geulbus_core/
unit.rs

1//! 날개셋 낱자(단위) 모델과 니모닉/operand 해석.
2//!
3//! `H3|<operand>` 의 operand 는 니모닉(`_GG`, `O_`, `RS`)이거나 숫자(`0x1F4`,
4//! `0x810000`)다. 니모닉은 위치(초/중/종)와 글자 정체를, 숫자는 갈마들이 토글(500)이나
5//! 가상 단위(`id<<16`)를 나타낸다. 참고: `research/01-nalgaeset-format.md` §2,§7.
6//!
7//! 한글 자모 유니코드 사실(조합/분해, 호환 자모 다리)은 `hanmo` 크레이트에 있다.
8
9/// 자모의 위치(낱자 갈래).
10#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
11pub enum Category {
12    /// 초성.
13    Cho,
14    /// 중성.
15    Jung,
16    /// 종성(받침).
17    Jong,
18}
19
20/// 해결된 한글 자모 단위: 위치 + 조합용 자모 코드포인트.
21#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
22pub struct Jamo {
23    pub category: Category,
24    /// 조합용 자모 코드포인트(U+1100 영역, 또는 옛한글/확장 영역).
25    pub cp: u32,
26}
27
28impl Jamo {
29    pub fn new(category: Category, cp: u32) -> Self {
30        Self { category, cp }
31    }
32
33    /// 현대 자모 집합에 속하면 그 호환 자모(U+31xx)를 돌려준다. 옛한글이면 `None`
34    /// (이 경우 설정의 FinalConvTable 에 의존).
35    pub fn default_compat(&self) -> Option<u32> {
36        match self.category {
37            Category::Cho => hanmo::cho_compat(self.cp),
38            Category::Jung => hanmo::jung_compat(self.cp),
39            Category::Jong => hanmo::jong_compat(self.cp),
40        }
41    }
42}
43
44/// `H3|<operand>` 가 가리키는 단위.
45#[derive(Clone, Copy, PartialEq, Eq, Debug)]
46pub enum Unit {
47    /// 보통의 한글 자모.
48    Jamo(Jamo),
49    /// 갈마들이 같은-키 토글 sentinel (500 = 0x1F4).
50    Toggle,
51    /// 미해결 가상 단위 id (VirtualUnitTable 로 해결해야 함). 예: 128/129/130.
52    Virtual(u32),
53}
54
55/// 갈마들이 토글을 나타내는 내부 단위 값.
56pub const TOGGLE: u32 = 500;
57
58/// 니모닉의 "핵심 토큰"(밑줄 제거) → 호환 자모(글자 정체). 위치 무관.
59fn mnemonic_to_compat(core: &str) -> Option<u32> {
60    Some(match core {
61        // 자음 (단일)
62        "G" => 0x3131,
63        "N" => 0x3134,
64        "D" => 0x3137,
65        "R" | "L" => 0x3139,
66        "M" => 0x3141,
67        "B" => 0x3142,
68        "S" => 0x3145,
69        "Q" | "NG" => 0x3147,
70        "J" => 0x3148,
71        "C" => 0x314A,
72        "K" => 0x314B,
73        "T" => 0x314C,
74        "P" => 0x314D,
75        "H" => 0x314E,
76        // 자음 (쌍/겹)
77        "GG" => 0x3132,
78        "GS" => 0x3133,
79        "NJ" => 0x3135,
80        "NH" => 0x3136,
81        "DD" => 0x3138,
82        "RG" => 0x313A,
83        "RM" => 0x313B,
84        "RB" => 0x313C,
85        "RS" => 0x313D,
86        "RT" => 0x313E,
87        "RP" => 0x313F,
88        "RH" => 0x3140,
89        "BB" => 0x3143,
90        "BS" => 0x3144,
91        "SS" => 0x3146,
92        "JJ" => 0x3149,
93        // 모음
94        "A" => 0x314F,
95        "AE" => 0x3150,
96        "YA" => 0x3151,
97        "YAE" => 0x3152,
98        "EO" => 0x3153,
99        "E" => 0x3154,
100        "YEO" => 0x3155,
101        "YE" => 0x3156,
102        "O" => 0x3157,
103        "WA" => 0x3158,
104        "WAE" => 0x3159,
105        "OI" => 0x315A,
106        "YO" => 0x315B,
107        "U" => 0x315C,
108        "UEO" => 0x315D,
109        "WE" => 0x315E,
110        "WI" => 0x315F,
111        "YU" => 0x3160,
112        "EU" => 0x3161,
113        "EUI" => 0x3162,
114        "I" => 0x3163,
115        _ => return None,
116    })
117}
118
119/// 니모닉을 단위로 해석. `ctx` 가 주어지면(UnitMix 처럼) 그 위치를 강제하고, 없으면
120/// 밑줄 위치(`_X`=종성, `X_`=초성/모음)와 글자 정체로 추론한다.
121pub fn resolve_mnemonic(s: &str, ctx: Option<Category>) -> Option<Unit> {
122    let (core, pos_category): (&str, Option<Category>) = if let Some(rest) = s.strip_prefix('_') {
123        (rest, Some(Category::Jong))
124    } else if let Some(rest) = s.strip_suffix('_') {
125        (rest, None) // 초성 또는 모음 (글자 정체로 결정)
126    } else {
127        (s, None)
128    };
129    let compat = mnemonic_to_compat(core)?;
130    let category = ctx.or(pos_category).unwrap_or_else(|| {
131        if hanmo::is_vowel_compat(compat) {
132            Category::Jung
133        } else {
134            Category::Cho
135        }
136    });
137    let cp = match category {
138        Category::Cho => hanmo::cho_cp_for_compat(compat)?,
139        Category::Jung => hanmo::jung_cp_for_compat(compat)?,
140        Category::Jong => hanmo::jong_cp_for_compat(compat)?,
141    };
142    Some(Unit::Jamo(Jamo::new(category, cp)))
143}
144
145/// 숫자 operand 를 단위로 해석.
146/// - 500 → 갈마들이 토글.
147/// - `id<<16` (하위 16비트 0, 상위 비0) → 가상 단위 id.
148/// - 그 외 → 조합용 자모 코드포인트로 보고 영역으로 위치 추론(옛한글 포함).
149pub fn resolve_numeric(n: u32) -> Option<Unit> {
150    if n == TOGGLE {
151        return Some(Unit::Toggle);
152    }
153    if n & 0xFFFF == 0 && n >> 16 != 0 {
154        return Some(Unit::Virtual(n >> 16));
155    }
156    category_of_codepoint(n).map(|cat| Unit::Jamo(Jamo::new(cat, n)))
157}
158
159/// 조합용 자모 코드포인트의 위치를 블록 범위로 추론(옛한글/확장 포함).
160pub fn category_of_codepoint(cp: u32) -> Option<Category> {
161    match cp {
162        0x1100..=0x115F => Some(Category::Cho), // 초성(현대+옛) + 초성 채움
163        0x1160..=0x11A7 => Some(Category::Jung), // 중성 채움 + 중성(현대+옛)
164        0x11A8..=0x11FF => Some(Category::Jong), // 종성(현대+옛)
165        0xA960..=0xA97F => Some(Category::Cho), // 확장-A: 옛 초성
166        0xD7B0..=0xD7CA => Some(Category::Jung), // 확장-B: 옛 중성
167        0xD7CB..=0xD7FF => Some(Category::Jong), // 확장-B: 옛 종성
168        _ => None,
169    }
170}
171
172/// `H3|<operand>` operand 문자열(니모닉 또는 숫자)을 단위로 해석.
173pub fn resolve_operand(s: &str, ctx: Option<Category>) -> Option<Unit> {
174    let s = s.trim();
175    if let Some(n) = parse_int(s) {
176        resolve_numeric(n)
177    } else {
178        resolve_mnemonic(s, ctx)
179    }
180}
181
182/// `0x..` 16진 또는 10진 정수 파싱.
183pub fn parse_int(s: &str) -> Option<u32> {
184    let s = s.trim();
185    if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
186        u32::from_str_radix(hex, 16).ok()
187    } else {
188        s.parse::<u32>().ok()
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    fn jamo_of(u: Unit) -> Jamo {
197        match u {
198            Unit::Jamo(j) => j,
199            other => panic!("expected jamo, got {other:?}"),
200        }
201    }
202
203    #[test]
204    fn keytable_mnemonics() {
205        // 초성: k=G_ → ㄱ 초성 U+1100
206        assert_eq!(
207            jamo_of(resolve_mnemonic("G_", None).unwrap()),
208            Jamo::new(Category::Cho, 0x1100)
209        );
210        // 종성: x=_G → ㄱ 종성 U+11A8
211        assert_eq!(
212            jamo_of(resolve_mnemonic("_G", None).unwrap()),
213            Jamo::new(Category::Jong, 0x11A8)
214        );
215        // 중성: f=A_ → ㅏ U+1161, / = O_ → ㅗ U+1169
216        assert_eq!(
217            jamo_of(resolve_mnemonic("A_", None).unwrap()),
218            Jamo::new(Category::Jung, 0x1161)
219        );
220        assert_eq!(
221            jamo_of(resolve_mnemonic("O_", None).unwrap()),
222            Jamo::new(Category::Jung, 0x1169)
223        );
224        // 바른 중성 bare: 8=EUI → ㅢ U+1174
225        assert_eq!(
226            jamo_of(resolve_mnemonic("EUI", None).unwrap()),
227            Jamo::new(Category::Jung, 0x1174)
228        );
229        // 겹받침 종성: @=_RG → ㄺ U+11B0
230        assert_eq!(
231            jamo_of(resolve_mnemonic("_RG", None).unwrap()),
232            Jamo::new(Category::Jong, 0x11B0)
233        );
234        // 초성 ㅇ: j=Q_ → U+110B
235        assert_eq!(
236            jamo_of(resolve_mnemonic("Q_", None).unwrap()),
237            Jamo::new(Category::Cho, 0x110B)
238        );
239        // 종성 ㅇ: a=_Q → U+11BC
240        assert_eq!(
241            jamo_of(resolve_mnemonic("_Q", None).unwrap()),
242            Jamo::new(Category::Jong, 0x11BC)
243        );
244    }
245
246    #[test]
247    fn unitmix_context_mnemonics() {
248        // UnitMix JONG: R_ + S_ → RS, 모두 종성 ctx
249        assert_eq!(
250            jamo_of(resolve_mnemonic("R_", Some(Category::Jong)).unwrap()),
251            Jamo::new(Category::Jong, 0x11AF) // ㄹ 종성
252        );
253        assert_eq!(
254            jamo_of(resolve_mnemonic("S_", Some(Category::Jong)).unwrap()),
255            Jamo::new(Category::Jong, 0x11BA) // ㅅ 종성
256        );
257        assert_eq!(
258            jamo_of(resolve_mnemonic("RS", Some(Category::Jong)).unwrap()),
259            Jamo::new(Category::Jong, 0x11B3) // ㄽ
260        );
261        // UnitMix CHO: GG → ㄲ 초성 U+1101
262        assert_eq!(
263            jamo_of(resolve_mnemonic("GG", Some(Category::Cho)).unwrap()),
264            Jamo::new(Category::Cho, 0x1101)
265        );
266        // UnitMix JUNG: WA → ㅘ U+116A
267        assert_eq!(
268            jamo_of(resolve_mnemonic("WA", Some(Category::Jung)).unwrap()),
269            Jamo::new(Category::Jung, 0x116A)
270        );
271    }
272
273    #[test]
274    fn numeric_operands() {
275        // 0x1F4 = 500 = 갈마들이 토글
276        assert_eq!(resolve_operand("0x1F4", None), Some(Unit::Toggle));
277        assert_eq!(resolve_operand("500", None), Some(Unit::Toggle));
278        // 0x800000 = 128<<16 = 가상 단위 128, 0x810000=129, 0x820000=130
279        assert_eq!(resolve_operand("0x800000", None), Some(Unit::Virtual(128)));
280        assert_eq!(resolve_operand("0x810000", None), Some(Unit::Virtual(129)));
281        assert_eq!(resolve_operand("0x820000", None), Some(Unit::Virtual(130)));
282    }
283
284    #[test]
285    fn raw_old_hangul_codepoint() {
286        // 옛이응 초성 U+114C → 초성으로 분류
287        assert_eq!(
288            resolve_operand("0x114C", None),
289            Some(Unit::Jamo(Jamo::new(Category::Cho, 0x114C)))
290        );
291        // 아래아 중성 U+119E → 중성
292        assert_eq!(
293            resolve_operand("0x119E", None),
294            Some(Unit::Jamo(Jamo::new(Category::Jung, 0x119E)))
295        );
296    }
297
298    #[test]
299    fn default_compat_roundtrip() {
300        assert_eq!(
301            Jamo::new(Category::Cho, 0x1100).default_compat(),
302            Some(0x3131)
303        );
304        assert_eq!(
305            Jamo::new(Category::Jong, 0x11A8).default_compat(),
306            Some(0x3131)
307        );
308        assert_eq!(
309            Jamo::new(Category::Jung, 0x1161).default_compat(),
310            Some(0x314F)
311        );
312        assert_eq!(
313            Jamo::new(Category::Jong, 0x11B0).default_compat(),
314            Some(0x313A)
315        );
316    }
317}