Skip to main content

mecab_ko_core/
kiwi_compat.rs

1//! Kiwi 형태소 분석기 호환 레이어
2//!
3//! 이 모듈은 Kiwi 형태소 분석기와의 상호 운용성을 제공합니다.
4//! MeCab-Ko와 Kiwi 간 품사 태그 변환 및 출력 형식 호환 기능을 포함합니다.
5//!
6//! # Kiwi 소개
7//!
8//! Kiwi(Korean Intelligent Word Identifier)는 C++로 작성된 고성능 한국어 형태소 분석기입니다.
9//! 세종 품사 태그 체계를 기반으로 하며, 일부 확장 태그를 포함합니다.
10//!
11//! # 품사 태그 매핑
12//!
13//! MeCab-Ko와 Kiwi는 대부분의 품사 태그를 공유하지만, 일부 차이점이 있습니다:
14//!
15//! - MeCab-Ko의 `NNBC` (단위 의존 명사)는 Kiwi에서 `NNB`로 통합
16//! - MeCab-Ko의 `SSO`/`SSC` (여는/닫는 괄호)는 Kiwi에서 `SS`로 통합
17//! - MeCab-Ko의 `SC` (구분자)는 Kiwi에서 `SP`로 매핑
18//! - MeCab-Ko의 `SY` (기타 기호)는 Kiwi에서 `SO`로 매핑
19//! - Kiwi의 웹 관련 태그 (`W_URL`, `W_EMAIL` 등)는 MeCab-Ko의 `SL`로 매핑
20//!
21//! # Example
22//!
23//! ```
24//! use mecab_ko_core::kiwi_compat::{KiwiPosTag, to_kiwi_tag, from_kiwi_tag};
25//! use mecab_ko_core::pos_tag::PosTag;
26//!
27//! // MeCab -> Kiwi 변환
28//! let kiwi_tag = to_kiwi_tag(PosTag::NNG);
29//! assert_eq!(kiwi_tag, KiwiPosTag::NNG);
30//!
31//! // Kiwi -> MeCab 변환
32//! let mecab_tag = from_kiwi_tag(KiwiPosTag::NNG);
33//! assert_eq!(mecab_tag, PosTag::NNG);
34//!
35//! // 문자열 파싱
36//! let tag = KiwiPosTag::from_str("NNG").unwrap();
37//! assert_eq!(tag.as_str(), "NNG");
38//! ```
39
40use crate::pos_tag::PosTag;
41use std::fmt;
42
43/// Kiwi 품사 태그
44///
45/// Kiwi 형태소 분석기에서 사용하는 품사 태그 체계
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47#[repr(u8)]
48pub enum KiwiPosTag {
49    // ============================================
50    // 체언 (Nominals)
51    // ============================================
52    /// 일반 명사 (General Noun)
53    NNG,
54    /// 고유 명사 (Proper Noun)
55    NNP,
56    /// 의존 명사 (Dependent Noun)
57    NNB,
58    /// 수사 (Numeral)
59    NR,
60    /// 대명사 (Pronoun)
61    NP,
62
63    // ============================================
64    // 용언 (Predicates)
65    // ============================================
66    /// 동사 (Verb)
67    VV,
68    /// 형용사 (Adjective)
69    VA,
70    /// 보조 용언 (Auxiliary Predicate)
71    VX,
72    /// 긍정 지정사 (Positive Copula)
73    VCP,
74    /// 부정 지정사 (Negative Copula)
75    VCN,
76
77    // ============================================
78    // 수식언 (Modifiers)
79    // ============================================
80    /// 관형사 (Determiner)
81    MM,
82    /// 일반 부사 (General Adverb)
83    MAG,
84    /// 접속 부사 (Conjunctive Adverb)
85    MAJ,
86
87    // ============================================
88    // 독립언 (Interjection)
89    // ============================================
90    /// 감탄사 (Interjection)
91    IC,
92
93    // ============================================
94    // 관계언 - 조사 (Particles)
95    // ============================================
96    /// 주격 조사 (Subjective Case Marker)
97    JKS,
98    /// 보격 조사 (Complement Case Marker)
99    JKC,
100    /// 관형격 조사 (Genitive Case Marker)
101    JKG,
102    /// 목적격 조사 (Objective Case Marker)
103    JKO,
104    /// 부사격 조사 (Adverbial Case Marker)
105    JKB,
106    /// 호격 조사 (Vocative Case Marker)
107    JKV,
108    /// 인용격 조사 (Quotative Case Marker)
109    JKQ,
110    /// 보조사 (Auxiliary Particle)
111    JX,
112    /// 접속 조사 (Conjunctive Particle)
113    JC,
114
115    // ============================================
116    // 어미 (Endings)
117    // ============================================
118    /// 선어말 어미 (Pre-final Ending)
119    EP,
120    /// 종결 어미 (Final Ending)
121    EF,
122    /// 연결 어미 (Connective Ending)
123    EC,
124    /// 명사형 전성 어미 (Nominal Transformative Ending)
125    ETN,
126    /// 관형형 전성 어미 (Adnominal Transformative Ending)
127    ETM,
128
129    // ============================================
130    // 접사 (Affixes)
131    // ============================================
132    /// 체언 접두사 (Noun Prefix)
133    XPN,
134    /// 명사파생 접미사 (Noun Derivational Suffix)
135    XSN,
136    /// 동사파생 접미사 (Verb Derivational Suffix)
137    XSV,
138    /// 형용사파생 접미사 (Adjective Derivational Suffix)
139    XSA,
140    /// 어근 (Root)
141    XR,
142
143    // ============================================
144    // 기호 (Symbols)
145    // ============================================
146    /// 마침 부호 (Terminal Punctuation)
147    SF,
148    /// 쉼표, 가운뎃점, 콜론, 빗금 (Separator)
149    SP,
150    /// 따옴표, 괄호 등 (Quote/Bracket)
151    SS,
152    /// 줄임표 (Ellipsis)
153    SE,
154    /// 그 외 기호 (Other Symbol)
155    SO,
156    /// 붙임표(물결,숨김,빠짐) (Wave dash)
157    SW,
158
159    /// 외국어 (Foreign Language)
160    SL,
161    /// 한자 (Hanja/Chinese Character)
162    SH,
163    /// 숫자 (Number)
164    SN,
165
166    // ============================================
167    // 웹 관련 (Web-related)
168    // ============================================
169    /// URL
170    #[allow(non_camel_case_types)]
171    W_URL,
172    /// 이메일
173    #[allow(non_camel_case_types)]
174    W_EMAIL,
175    /// 해시태그
176    #[allow(non_camel_case_types)]
177    W_HASHTAG,
178    /// 멘션
179    #[allow(non_camel_case_types)]
180    W_MENTION,
181    /// 이모티콘
182    #[allow(non_camel_case_types)]
183    W_EMOJI,
184    /// 기타 웹 관련
185    #[allow(non_camel_case_types)]
186    W_OTHER,
187
188    // ============================================
189    // 특수 (Special)
190    // ============================================
191    /// 미등록어 (Unknown Word)
192    Unknown,
193}
194
195impl KiwiPosTag {
196    /// 문자열에서 Kiwi 품사 태그 파싱
197    ///
198    /// # Example
199    /// ```
200    /// use mecab_ko_core::kiwi_compat::KiwiPosTag;
201    ///
202    /// assert_eq!(KiwiPosTag::from_str("NNG"), Some(KiwiPosTag::NNG));
203    /// assert_eq!(KiwiPosTag::from_str("W_URL"), Some(KiwiPosTag::W_URL));
204    /// assert_eq!(KiwiPosTag::from_str("INVALID"), None);
205    /// ```
206    #[must_use]
207    #[allow(clippy::should_implement_trait)]
208    pub fn from_str(s: &str) -> Option<Self> {
209        match s {
210            // 체언
211            "NNG" => Some(Self::NNG),
212            "NNP" => Some(Self::NNP),
213            "NNB" => Some(Self::NNB),
214            "NR" => Some(Self::NR),
215            "NP" => Some(Self::NP),
216            // 용언
217            "VV" => Some(Self::VV),
218            "VA" => Some(Self::VA),
219            "VX" => Some(Self::VX),
220            "VCP" => Some(Self::VCP),
221            "VCN" => Some(Self::VCN),
222            // 수식언
223            "MM" => Some(Self::MM),
224            "MAG" => Some(Self::MAG),
225            "MAJ" => Some(Self::MAJ),
226            // 독립언
227            "IC" => Some(Self::IC),
228            // 조사
229            "JKS" => Some(Self::JKS),
230            "JKC" => Some(Self::JKC),
231            "JKG" => Some(Self::JKG),
232            "JKO" => Some(Self::JKO),
233            "JKB" => Some(Self::JKB),
234            "JKV" => Some(Self::JKV),
235            "JKQ" => Some(Self::JKQ),
236            "JX" => Some(Self::JX),
237            "JC" => Some(Self::JC),
238            // 어미
239            "EP" => Some(Self::EP),
240            "EF" => Some(Self::EF),
241            "EC" => Some(Self::EC),
242            "ETN" => Some(Self::ETN),
243            "ETM" => Some(Self::ETM),
244            // 접사
245            "XPN" => Some(Self::XPN),
246            "XSN" => Some(Self::XSN),
247            "XSV" => Some(Self::XSV),
248            "XSA" => Some(Self::XSA),
249            "XR" => Some(Self::XR),
250            // 기호
251            "SF" => Some(Self::SF),
252            "SP" => Some(Self::SP),
253            "SS" => Some(Self::SS),
254            "SE" => Some(Self::SE),
255            "SO" => Some(Self::SO),
256            "SW" => Some(Self::SW),
257            "SL" => Some(Self::SL),
258            "SH" => Some(Self::SH),
259            "SN" => Some(Self::SN),
260            // 웹 관련
261            "W_URL" => Some(Self::W_URL),
262            "W_EMAIL" => Some(Self::W_EMAIL),
263            "W_HASHTAG" => Some(Self::W_HASHTAG),
264            "W_MENTION" => Some(Self::W_MENTION),
265            "W_EMOJI" => Some(Self::W_EMOJI),
266            "W_OTHER" => Some(Self::W_OTHER),
267            // 특수
268            "UNKNOWN" | "UNK" => Some(Self::Unknown),
269            _ => None,
270        }
271    }
272
273    /// Kiwi 품사 태그 문자열 반환
274    #[must_use]
275    pub const fn as_str(&self) -> &'static str {
276        match self {
277            // 체언
278            Self::NNG => "NNG",
279            Self::NNP => "NNP",
280            Self::NNB => "NNB",
281            Self::NR => "NR",
282            Self::NP => "NP",
283            // 용언
284            Self::VV => "VV",
285            Self::VA => "VA",
286            Self::VX => "VX",
287            Self::VCP => "VCP",
288            Self::VCN => "VCN",
289            // 수식언
290            Self::MM => "MM",
291            Self::MAG => "MAG",
292            Self::MAJ => "MAJ",
293            // 독립언
294            Self::IC => "IC",
295            // 조사
296            Self::JKS => "JKS",
297            Self::JKC => "JKC",
298            Self::JKG => "JKG",
299            Self::JKO => "JKO",
300            Self::JKB => "JKB",
301            Self::JKV => "JKV",
302            Self::JKQ => "JKQ",
303            Self::JX => "JX",
304            Self::JC => "JC",
305            // 어미
306            Self::EP => "EP",
307            Self::EF => "EF",
308            Self::EC => "EC",
309            Self::ETN => "ETN",
310            Self::ETM => "ETM",
311            // 접사
312            Self::XPN => "XPN",
313            Self::XSN => "XSN",
314            Self::XSV => "XSV",
315            Self::XSA => "XSA",
316            Self::XR => "XR",
317            // 기호
318            Self::SF => "SF",
319            Self::SP => "SP",
320            Self::SS => "SS",
321            Self::SE => "SE",
322            Self::SO => "SO",
323            Self::SW => "SW",
324            Self::SL => "SL",
325            Self::SH => "SH",
326            Self::SN => "SN",
327            // 웹 관련
328            Self::W_URL => "W_URL",
329            Self::W_EMAIL => "W_EMAIL",
330            Self::W_HASHTAG => "W_HASHTAG",
331            Self::W_MENTION => "W_MENTION",
332            Self::W_EMOJI => "W_EMOJI",
333            Self::W_OTHER => "W_OTHER",
334            // 특수
335            Self::Unknown => "UNKNOWN",
336        }
337    }
338}
339
340impl fmt::Display for KiwiPosTag {
341    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342        write!(f, "{}", self.as_str())
343    }
344}
345
346/// MeCab-Ko 품사 태그를 Kiwi 품사 태그로 변환
347///
348/// # Example
349/// ```
350/// use mecab_ko_core::kiwi_compat::{to_kiwi_tag, KiwiPosTag};
351/// use mecab_ko_core::pos_tag::PosTag;
352///
353/// assert_eq!(to_kiwi_tag(PosTag::NNG), KiwiPosTag::NNG);
354/// assert_eq!(to_kiwi_tag(PosTag::NNBC), KiwiPosTag::NNB); // 단위명사 -> 의존명사
355/// assert_eq!(to_kiwi_tag(PosTag::SSO), KiwiPosTag::SS); // 여는괄호 -> 따옴표/괄호
356/// assert_eq!(to_kiwi_tag(PosTag::SC), KiwiPosTag::SP); // 구분자 -> 쉼표
357/// ```
358#[must_use]
359pub const fn to_kiwi_tag(mecab_tag: PosTag) -> KiwiPosTag {
360    match mecab_tag {
361        // 체언 - 대부분 1:1 매핑
362        PosTag::NNG => KiwiPosTag::NNG,
363        PosTag::NNP => KiwiPosTag::NNP,
364        PosTag::NNB | PosTag::NNBC => KiwiPosTag::NNB, // 단위명사 -> 의존명사 통합
365        PosTag::NP => KiwiPosTag::NP,
366        PosTag::NR => KiwiPosTag::NR,
367
368        // 용언 - 1:1 매핑
369        PosTag::VV => KiwiPosTag::VV,
370        PosTag::VA => KiwiPosTag::VA,
371        PosTag::VX => KiwiPosTag::VX,
372        PosTag::VCP => KiwiPosTag::VCP,
373        PosTag::VCN => KiwiPosTag::VCN,
374
375        // 수식언 - 1:1 매핑
376        PosTag::MM => KiwiPosTag::MM,
377        PosTag::MAG => KiwiPosTag::MAG,
378        PosTag::MAJ => KiwiPosTag::MAJ,
379
380        // 독립언 - 1:1 매핑
381        PosTag::IC => KiwiPosTag::IC,
382
383        // 조사 - 1:1 매핑
384        PosTag::JKS => KiwiPosTag::JKS,
385        PosTag::JKC => KiwiPosTag::JKC,
386        PosTag::JKG => KiwiPosTag::JKG,
387        PosTag::JKO => KiwiPosTag::JKO,
388        PosTag::JKB => KiwiPosTag::JKB,
389        PosTag::JKV => KiwiPosTag::JKV,
390        PosTag::JKQ => KiwiPosTag::JKQ,
391        PosTag::JX => KiwiPosTag::JX,
392        PosTag::JC => KiwiPosTag::JC,
393
394        // 어미 - 1:1 매핑
395        PosTag::EP => KiwiPosTag::EP,
396        PosTag::EF => KiwiPosTag::EF,
397        PosTag::EC => KiwiPosTag::EC,
398        PosTag::ETN => KiwiPosTag::ETN,
399        PosTag::ETM => KiwiPosTag::ETM,
400
401        // 접사 - 1:1 매핑
402        PosTag::XPN => KiwiPosTag::XPN,
403        PosTag::XSN => KiwiPosTag::XSN,
404        PosTag::XSV => KiwiPosTag::XSV,
405        PosTag::XSA => KiwiPosTag::XSA,
406        PosTag::XR => KiwiPosTag::XR,
407
408        // 기호 - 일부 통합
409        PosTag::SF => KiwiPosTag::SF,
410        PosTag::SP | PosTag::SC => KiwiPosTag::SP, // 구분자 -> 쉼표 통합
411        PosTag::SSO | PosTag::SSC => KiwiPosTag::SS, // 여는/닫는괄호 -> 따옴표/괄호 통합
412        PosTag::SE => KiwiPosTag::SE,
413        PosTag::SY => KiwiPosTag::SO, // 기타기호 -> 그외기호
414        PosTag::SL => KiwiPosTag::SL,
415        PosTag::SH => KiwiPosTag::SH,
416        PosTag::SN => KiwiPosTag::SN,
417
418        // 특수
419        PosTag::Unknown => KiwiPosTag::Unknown,
420    }
421}
422
423/// Kiwi 품사 태그를 MeCab-Ko 품사 태그로 변환
424///
425/// Kiwi의 웹 관련 태그는 MeCab-Ko의 SL (외국어)로 매핑됩니다.
426///
427/// # Example
428/// ```
429/// use mecab_ko_core::kiwi_compat::{from_kiwi_tag, KiwiPosTag};
430/// use mecab_ko_core::pos_tag::PosTag;
431///
432/// assert_eq!(from_kiwi_tag(KiwiPosTag::NNG), PosTag::NNG);
433/// assert_eq!(from_kiwi_tag(KiwiPosTag::SS), PosTag::SSO); // 괄호 -> 여는괄호 (기본값)
434/// assert_eq!(from_kiwi_tag(KiwiPosTag::W_URL), PosTag::SL); // 웹 태그 -> 외국어
435/// ```
436#[must_use]
437pub const fn from_kiwi_tag(kiwi_tag: KiwiPosTag) -> PosTag {
438    match kiwi_tag {
439        // 체언 - 1:1 매핑
440        KiwiPosTag::NNG => PosTag::NNG,
441        KiwiPosTag::NNP => PosTag::NNP,
442        KiwiPosTag::NNB => PosTag::NNB, // Kiwi NNB -> MeCab NNB (NNBC는 손실)
443        KiwiPosTag::NP => PosTag::NP,
444        KiwiPosTag::NR => PosTag::NR,
445
446        // 용언 - 1:1 매핑
447        KiwiPosTag::VV => PosTag::VV,
448        KiwiPosTag::VA => PosTag::VA,
449        KiwiPosTag::VX => PosTag::VX,
450        KiwiPosTag::VCP => PosTag::VCP,
451        KiwiPosTag::VCN => PosTag::VCN,
452
453        // 수식언 - 1:1 매핑
454        KiwiPosTag::MM => PosTag::MM,
455        KiwiPosTag::MAG => PosTag::MAG,
456        KiwiPosTag::MAJ => PosTag::MAJ,
457
458        // 독립언 - 1:1 매핑
459        KiwiPosTag::IC => PosTag::IC,
460
461        // 조사 - 1:1 매핑
462        KiwiPosTag::JKS => PosTag::JKS,
463        KiwiPosTag::JKC => PosTag::JKC,
464        KiwiPosTag::JKG => PosTag::JKG,
465        KiwiPosTag::JKO => PosTag::JKO,
466        KiwiPosTag::JKB => PosTag::JKB,
467        KiwiPosTag::JKV => PosTag::JKV,
468        KiwiPosTag::JKQ => PosTag::JKQ,
469        KiwiPosTag::JX => PosTag::JX,
470        KiwiPosTag::JC => PosTag::JC,
471
472        // 어미 - 1:1 매핑
473        KiwiPosTag::EP => PosTag::EP,
474        KiwiPosTag::EF => PosTag::EF,
475        KiwiPosTag::EC => PosTag::EC,
476        KiwiPosTag::ETN => PosTag::ETN,
477        KiwiPosTag::ETM => PosTag::ETM,
478
479        // 접사 - 1:1 매핑
480        KiwiPosTag::XPN => PosTag::XPN,
481        KiwiPosTag::XSN => PosTag::XSN,
482        KiwiPosTag::XSV => PosTag::XSV,
483        KiwiPosTag::XSA => PosTag::XSA,
484        KiwiPosTag::XR => PosTag::XR,
485
486        // 기호 - 역변환 시 정보 손실 가능
487        KiwiPosTag::SF => PosTag::SF,
488        KiwiPosTag::SP => PosTag::SP, // SP는 SC도 포함할 수 있음 (손실)
489        KiwiPosTag::SS => PosTag::SSO, // SS -> SSO (기본값, SSC는 손실)
490        KiwiPosTag::SE => PosTag::SE,
491        KiwiPosTag::SO | KiwiPosTag::SW => PosTag::SY, // SO, SW -> SY (붙임표를 기타기호로)
492        KiwiPosTag::SL
493        | KiwiPosTag::W_URL
494        | KiwiPosTag::W_EMAIL
495        | KiwiPosTag::W_HASHTAG
496        | KiwiPosTag::W_MENTION
497        | KiwiPosTag::W_EMOJI
498        | KiwiPosTag::W_OTHER => PosTag::SL, // 웹 관련 - SL (외국어)로 통합
499        KiwiPosTag::SH => PosTag::SH,
500        KiwiPosTag::SN => PosTag::SN,
501
502        // 특수
503        KiwiPosTag::Unknown => PosTag::Unknown,
504    }
505}
506
507/// Kiwi 호환 토큰 구조체
508///
509/// Kiwi 형태소 분석 결과와 호환되는 출력 형식
510///
511/// # Example
512/// ```
513/// use mecab_ko_core::kiwi_compat::{KiwiToken, KiwiPosTag};
514///
515/// let token = KiwiToken {
516///     form: "안녕".to_string(),
517///     tag: KiwiPosTag::NNG,
518///     start: 0,
519///     length: 6, // UTF-8 바이트 길이
520///     score: -10.5,
521/// };
522///
523/// assert_eq!(token.form, "안녕");
524/// assert_eq!(token.tag, KiwiPosTag::NNG);
525/// ```
526#[derive(Debug, Clone, PartialEq)]
527pub struct KiwiToken {
528    /// 형태소 표면형
529    pub form: String,
530    /// 품사 태그
531    pub tag: KiwiPosTag,
532    /// 시작 위치 (바이트 오프셋)
533    pub start: usize,
534    /// 길이 (바이트)
535    pub length: usize,
536    /// 분석 점수 (로그 확률)
537    pub score: f64,
538}
539
540impl KiwiToken {
541    /// 새 토큰 생성
542    ///
543    /// # Example
544    /// ```
545    /// use mecab_ko_core::kiwi_compat::{KiwiToken, KiwiPosTag};
546    ///
547    /// let token = KiwiToken::new("하다", KiwiPosTag::VV, 0, 6, -5.2);
548    /// assert_eq!(token.form, "하다");
549    /// ```
550    pub fn new(
551        form: impl Into<String>,
552        tag: KiwiPosTag,
553        start: usize,
554        length: usize,
555        score: f64,
556    ) -> Self {
557        Self {
558            form: form.into(),
559            tag,
560            start,
561            length,
562            score,
563        }
564    }
565
566    /// 끝 위치 계산 (start + length)
567    #[must_use]
568    pub const fn end(&self) -> usize {
569        self.start + self.length
570    }
571
572    /// MeCab-Ko 품사 태그로 변환
573    #[must_use]
574    pub const fn to_mecab_tag(&self) -> PosTag {
575        from_kiwi_tag(self.tag)
576    }
577}
578
579impl fmt::Display for KiwiToken {
580    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
581        write!(f, "{}/{}", self.form, self.tag)
582    }
583}
584
585#[cfg(test)]
586#[allow(clippy::float_cmp)]
587mod tests {
588    use super::*;
589
590    #[test]
591    fn test_kiwi_tag_from_str() {
592        assert_eq!(KiwiPosTag::from_str("NNG"), Some(KiwiPosTag::NNG));
593        assert_eq!(KiwiPosTag::from_str("VV"), Some(KiwiPosTag::VV));
594        assert_eq!(KiwiPosTag::from_str("W_URL"), Some(KiwiPosTag::W_URL));
595        assert_eq!(KiwiPosTag::from_str("UNKNOWN"), Some(KiwiPosTag::Unknown));
596        assert_eq!(KiwiPosTag::from_str("INVALID"), None);
597    }
598
599    #[test]
600    fn test_kiwi_tag_as_str() {
601        assert_eq!(KiwiPosTag::NNG.as_str(), "NNG");
602        assert_eq!(KiwiPosTag::W_URL.as_str(), "W_URL");
603        assert_eq!(KiwiPosTag::Unknown.as_str(), "UNKNOWN");
604    }
605
606    #[test]
607    fn test_to_kiwi_tag_nominals() {
608        // 체언
609        assert_eq!(to_kiwi_tag(PosTag::NNG), KiwiPosTag::NNG);
610        assert_eq!(to_kiwi_tag(PosTag::NNP), KiwiPosTag::NNP);
611        assert_eq!(to_kiwi_tag(PosTag::NNB), KiwiPosTag::NNB);
612        assert_eq!(to_kiwi_tag(PosTag::NNBC), KiwiPosTag::NNB); // 단위명사 통합
613        assert_eq!(to_kiwi_tag(PosTag::NP), KiwiPosTag::NP);
614        assert_eq!(to_kiwi_tag(PosTag::NR), KiwiPosTag::NR);
615    }
616
617    #[test]
618    fn test_to_kiwi_tag_predicates() {
619        // 용언
620        assert_eq!(to_kiwi_tag(PosTag::VV), KiwiPosTag::VV);
621        assert_eq!(to_kiwi_tag(PosTag::VA), KiwiPosTag::VA);
622        assert_eq!(to_kiwi_tag(PosTag::VX), KiwiPosTag::VX);
623        assert_eq!(to_kiwi_tag(PosTag::VCP), KiwiPosTag::VCP);
624        assert_eq!(to_kiwi_tag(PosTag::VCN), KiwiPosTag::VCN);
625    }
626
627    #[test]
628    fn test_to_kiwi_tag_particles() {
629        // 조사
630        assert_eq!(to_kiwi_tag(PosTag::JKS), KiwiPosTag::JKS);
631        assert_eq!(to_kiwi_tag(PosTag::JKO), KiwiPosTag::JKO);
632        assert_eq!(to_kiwi_tag(PosTag::JX), KiwiPosTag::JX);
633    }
634
635    #[test]
636    fn test_to_kiwi_tag_symbols() {
637        // 기호 - 통합 확인
638        assert_eq!(to_kiwi_tag(PosTag::SSO), KiwiPosTag::SS); // 여는괄호 -> SS
639        assert_eq!(to_kiwi_tag(PosTag::SSC), KiwiPosTag::SS); // 닫는괄호 -> SS
640        assert_eq!(to_kiwi_tag(PosTag::SC), KiwiPosTag::SP); // 구분자 -> SP
641        assert_eq!(to_kiwi_tag(PosTag::SY), KiwiPosTag::SO); // 기타기호 -> SO
642    }
643
644    #[test]
645    fn test_from_kiwi_tag_nominals() {
646        // 체언
647        assert_eq!(from_kiwi_tag(KiwiPosTag::NNG), PosTag::NNG);
648        assert_eq!(from_kiwi_tag(KiwiPosTag::NNP), PosTag::NNP);
649        assert_eq!(from_kiwi_tag(KiwiPosTag::NNB), PosTag::NNB); // NNBC 정보 손실
650    }
651
652    #[test]
653    fn test_from_kiwi_tag_symbols() {
654        // 기호 - 역변환 확인
655        assert_eq!(from_kiwi_tag(KiwiPosTag::SS), PosTag::SSO); // SS -> SSO (기본값)
656        assert_eq!(from_kiwi_tag(KiwiPosTag::SO), PosTag::SY); // SO -> SY
657        assert_eq!(from_kiwi_tag(KiwiPosTag::SW), PosTag::SY); // SW -> SY
658    }
659
660    #[test]
661    fn test_from_kiwi_tag_web() {
662        // 웹 관련 태그 -> SL
663        assert_eq!(from_kiwi_tag(KiwiPosTag::W_URL), PosTag::SL);
664        assert_eq!(from_kiwi_tag(KiwiPosTag::W_EMAIL), PosTag::SL);
665        assert_eq!(from_kiwi_tag(KiwiPosTag::W_HASHTAG), PosTag::SL);
666        assert_eq!(from_kiwi_tag(KiwiPosTag::W_MENTION), PosTag::SL);
667        assert_eq!(from_kiwi_tag(KiwiPosTag::W_EMOJI), PosTag::SL);
668        assert_eq!(from_kiwi_tag(KiwiPosTag::W_OTHER), PosTag::SL);
669    }
670
671    #[test]
672    fn test_roundtrip_conversion() {
673        // 대부분의 태그는 왕복 변환 가능
674        let tags = [
675            PosTag::NNG,
676            PosTag::VV,
677            PosTag::JKS,
678            PosTag::EP,
679            PosTag::XPN,
680            PosTag::SF,
681        ];
682
683        for tag in tags {
684            let kiwi_tag = to_kiwi_tag(tag);
685            let back = from_kiwi_tag(kiwi_tag);
686            assert_eq!(tag, back, "Roundtrip failed for {tag:?}");
687        }
688    }
689
690    #[test]
691    fn test_lossy_conversion() {
692        // 정보 손실이 있는 변환
693        // NNBC -> NNB -> NNB (NNBC 손실)
694        assert_eq!(from_kiwi_tag(to_kiwi_tag(PosTag::NNBC)), PosTag::NNB);
695
696        // SSC -> SS -> SSO (SSC 손실)
697        assert_eq!(from_kiwi_tag(to_kiwi_tag(PosTag::SSC)), PosTag::SSO);
698
699        // SC -> SP -> SP (SC 손실)
700        assert_eq!(from_kiwi_tag(to_kiwi_tag(PosTag::SC)), PosTag::SP);
701    }
702
703    #[test]
704    fn test_kiwi_token_creation() {
705        let token = KiwiToken::new("안녕", KiwiPosTag::NNG, 0, 6, -10.5);
706        assert_eq!(token.form, "안녕");
707        assert_eq!(token.tag, KiwiPosTag::NNG);
708        assert_eq!(token.start, 0);
709        assert_eq!(token.length, 6);
710        assert_eq!(token.score, -10.5);
711        assert_eq!(token.end(), 6);
712    }
713
714    #[test]
715    fn test_kiwi_token_display() {
716        let token = KiwiToken::new("하다", KiwiPosTag::VV, 0, 6, -5.0);
717        assert_eq!(token.to_string(), "하다/VV");
718    }
719
720    #[test]
721    fn test_kiwi_token_to_mecab() {
722        let token = KiwiToken::new("것", KiwiPosTag::NNB, 0, 3, -8.2);
723        assert_eq!(token.to_mecab_tag(), PosTag::NNB);
724
725        let url_token = KiwiToken::new("http://example.com", KiwiPosTag::W_URL, 0, 18, -15.0);
726        assert_eq!(url_token.to_mecab_tag(), PosTag::SL);
727    }
728
729    #[test]
730    fn test_all_kiwi_tags_covered() {
731        // 모든 Kiwi 태그가 MeCab 태그로 변환 가능한지 확인
732        let kiwi_tags = [
733            KiwiPosTag::NNG,
734            KiwiPosTag::NNP,
735            KiwiPosTag::NNB,
736            KiwiPosTag::NR,
737            KiwiPosTag::NP,
738            KiwiPosTag::VV,
739            KiwiPosTag::VA,
740            KiwiPosTag::VX,
741            KiwiPosTag::VCP,
742            KiwiPosTag::VCN,
743            KiwiPosTag::MM,
744            KiwiPosTag::MAG,
745            KiwiPosTag::MAJ,
746            KiwiPosTag::IC,
747            KiwiPosTag::JKS,
748            KiwiPosTag::JKC,
749            KiwiPosTag::JKG,
750            KiwiPosTag::JKO,
751            KiwiPosTag::JKB,
752            KiwiPosTag::JKV,
753            KiwiPosTag::JKQ,
754            KiwiPosTag::JX,
755            KiwiPosTag::JC,
756            KiwiPosTag::EP,
757            KiwiPosTag::EF,
758            KiwiPosTag::EC,
759            KiwiPosTag::ETN,
760            KiwiPosTag::ETM,
761            KiwiPosTag::XPN,
762            KiwiPosTag::XSN,
763            KiwiPosTag::XSV,
764            KiwiPosTag::XSA,
765            KiwiPosTag::XR,
766            KiwiPosTag::SF,
767            KiwiPosTag::SP,
768            KiwiPosTag::SS,
769            KiwiPosTag::SE,
770            KiwiPosTag::SO,
771            KiwiPosTag::SW,
772            KiwiPosTag::SL,
773            KiwiPosTag::SH,
774            KiwiPosTag::SN,
775            KiwiPosTag::W_URL,
776            KiwiPosTag::W_EMAIL,
777            KiwiPosTag::W_HASHTAG,
778            KiwiPosTag::W_MENTION,
779            KiwiPosTag::W_EMOJI,
780            KiwiPosTag::W_OTHER,
781            KiwiPosTag::Unknown,
782        ];
783
784        for tag in kiwi_tags {
785            let mecab_tag = from_kiwi_tag(tag);
786            // 변환 결과가 유효한지만 확인 (구체적인 매핑은 위 테스트에서 검증)
787            assert_ne!(mecab_tag.as_str(), "", "Conversion failed for {tag:?}");
788        }
789    }
790
791    #[test]
792    fn test_all_mecab_tags_covered() {
793        // 모든 MeCab 태그가 Kiwi 태그로 변환 가능한지 확인
794        for tag in PosTag::all() {
795            let kiwi_tag = to_kiwi_tag(*tag);
796            // 변환 결과가 유효한지만 확인
797            assert_ne!(kiwi_tag.as_str(), "", "Conversion failed for {tag:?}");
798        }
799    }
800}