Skip to main content

mecab_ko_core/sejong/
converter.rs

1//! 세종 코퍼스 형식 변환기
2
3use std::collections::HashMap;
4
5use crate::tokenizer::Token;
6
7use super::corrections::apply_context_corrections;
8use super::ending_rules::init_ending_rules;
9use super::hangul::normalize_jamo as hangul_normalize_jamo;
10use super::lexicon::apply_lexicon_overrides;
11use super::postprocess::{
12    apply_decomposition_corrections, apply_token_merges, apply_vv_seyo_splits,
13};
14use super::splitter::{is_compound_tag, split_compound_tag, split_morpheme};
15use super::tag_map::tag_map;
16use super::types::{DecomposedMorpheme, EndingRule, SejongToken};
17
18/// 세종 코퍼스 형식 변환기
19pub struct SejongConverter {
20    /// 품사 태그 매핑 테이블 (복합 → 분리) — 전역 정적 참조
21    tag_map: &'static HashMap<String, Vec<String>>,
22    /// 어미 분리 규칙
23    ending_rules: Vec<EndingRule>,
24    /// 분석결과 컬럼 사용 여부 (불규칙 활용 지원)
25    use_decomposition: bool,
26}
27
28impl Default for SejongConverter {
29    fn default() -> Self {
30        Self::new()
31    }
32}
33
34impl SejongConverter {
35    /// 기본 설정으로 변환기 생성
36    #[must_use]
37    pub fn new() -> Self {
38        Self {
39            tag_map: tag_map(),
40            ending_rules: init_ending_rules(),
41            use_decomposition: true, // 기본값: 분석결과 컬럼 활용
42        }
43    }
44
45    /// 분석결과 사용 여부 설정
46    ///
47    /// `true`이면 mecab-ko-dic의 12번째 컬럼(분석결과)을 우선 사용합니다.
48    /// 불규칙 활용을 정확하게 처리하려면 `true`로 설정하세요.
49    #[must_use]
50    pub const fn with_decomposition(mut self, use_decomposition: bool) -> Self {
51        self.use_decomposition = use_decomposition;
52        self
53    }
54
55    /// 분석결과 컬럼에서 형태소 분해 정보 파싱
56    ///
57    /// 형식: `stem/POS/*+ending/POS/*+...`
58    /// 예시: `가깝/VA/*+아/EC/*` → [("가깝", "VA"), ("아", "EC")]
59    #[must_use]
60    pub fn parse_decomposition(decomposition: &str) -> Vec<DecomposedMorpheme> {
61        if decomposition.is_empty() || decomposition == "*" {
62            return Vec::new();
63        }
64
65        let mut result = Vec::new();
66
67        // '+' 로 분리하여 각 형태소 파싱
68        for part in decomposition.split('+') {
69            let part = part.trim();
70            if part.is_empty() {
71                continue;
72            }
73
74            // 형식: surface/POS/* 또는 surface/POS
75            let segments: Vec<&str> = part.split('/').collect();
76            if segments.len() >= 2 {
77                let surface = segments[0].to_string();
78                let pos = segments[1].to_string();
79
80                // 빈 표면형이나 '*' 는 스킵
81                if !surface.is_empty() && surface != "*" && !pos.is_empty() && pos != "*" {
82                    result.push(DecomposedMorpheme { surface, pos });
83                }
84            }
85        }
86
87        result
88    }
89
90    /// feature 문자열에서 분析결과(12번째 컬럼) 추출
91    ///
92    /// mecab-ko-dic CSV 형식:
93    /// `품사,의미분류,종성,읽기,타입,첫품사,끝품사,분석결과`
94    /// (0~7, 총 8개 필드이지만 인덱스 7이 분析결과)
95    #[must_use]
96    pub fn extract_decomposition(features: &str) -> Option<String> {
97        let fields: Vec<&str> = features.split(',').collect();
98        // 분析결과는 8번째 필드 (인덱스 7) 또는 그 이후
99        // Inflect 타입의 경우 인덱스 7에 분析결과가 있음
100        if fields.len() >= 8 {
101            let decomp = fields[7].trim();
102            if !decomp.is_empty() && decomp != "*" {
103                return Some(decomp.to_string());
104            }
105        }
106        None
107    }
108
109    // 166차: is_compound_token 함수 제거
110    // 복합어 분리는 세종 코퍼스 표준에 맞게 선별적으로 적용 필요
111    // 현재는 복합어를 분리하지 않음
112
113    /// 복합 품사 태그인지 확인
114    #[must_use]
115    pub fn is_compound_tag(&self, pos: &str) -> bool {
116        is_compound_tag(pos)
117    }
118
119    /// 복합 품사 태그를 분리된 태그 목록으로 변환
120    #[must_use]
121    pub fn split_compound_tag(&self, pos: &str) -> Vec<String> {
122        split_compound_tag(self.tag_map, pos)
123    }
124
125    /// 표면형에서 어미를 분리
126    ///
127    /// # Arguments
128    /// * `surface` - 표면형 (예: "갔다")
129    /// * `pos` - 품사 태그 (예: "VV+EF")
130    ///
131    /// # Returns
132    /// 분리된 (표면형, 품사) 쌍의 벡터
133    #[must_use]
134    pub fn split_morpheme(&self, surface: &str, pos: &str) -> Vec<(String, String)> {
135        split_morpheme(surface, pos, self.tag_map, &self.ending_rules)
136    }
137
138    /// 토큰을 세종 형식으로 변환
139    ///
140    /// 변환 우선순위:
141    /// 1. 분析결과 컬럼 사용 (`use_decomposition=true`, features에 분析결과 있는 경우)
142    /// 2. 규칙 기반 어미 분리 (`ending_rules`)
143    /// 3. 태그만 분리 (복합 태그인 경우)
144    /// 4. 그대로 반환 (단순 태그인 경우)
145    #[must_use]
146    pub fn convert_token(&self, token: &Token) -> Vec<SejongToken> {
147        // 143차: "는다/VV+EC"는 사전 분析결과가 잘못됨 (늘/VV+ㄴ다/EC)
148        // 규칙 기반으로 직접 처리: "는다/VV+EC" → "는다/EF"
149        let skip_decomposition = token.surface == "는다" && token.pos == "VV+EC";
150
151        // 166차: Compound 분리 보류
152        // 일부 복합어(밤낮)는 분리해야 하지만 대부분(인공지능, 서울특별시)은 분리하면 안 됨
153        // 세종 코퍼스 표준에 따라 선별적 적용 필요 - 현재는 비활성화
154
155        // 212차: 특수 동사는 분析결과 무시하고 규칙 기반 사용
156        // "들리다", "놀리다" 등은 VV+EF로 처리해야 함 (VV+VX+EF 아님)
157        let skip_decomp_verbs = ["들리다", "놀리다"];
158        let force_rule_based =
159            token.pos == "VV+EF" && skip_decomp_verbs.contains(&token.surface.as_str());
160
161        // 1. 분析결과 컬럼 활용 시도
162        if self.use_decomposition
163            && !token.features.is_empty()
164            && !skip_decomposition
165            && !force_rule_based
166        {
167            if let Some(decomp) = Self::extract_decomposition(&token.features) {
168                let morphemes = Self::parse_decomposition(&decomp);
169                if !morphemes.is_empty() {
170                    // 분析결과의 POS 태그 구조가 토큰 POS와 일치하는지 검증
171                    let decomp_pos: String = morphemes
172                        .iter()
173                        .map(|m| m.pos.as_str())
174                        .collect::<Vec<_>>()
175                        .join("+");
176                    if decomp_pos == token.pos {
177                        // 245차 보정: 단일 형태소의 경우 표면형이 변경되면 decomposition 무시
178                        if morphemes.len() == 1 && morphemes[0].surface != token.surface {
179                            // 표면형이 다르면 decomposition 무시
180                        } else {
181                            return Self::morphemes_to_sejong_tokens(&morphemes, token);
182                        }
183                    }
184                    // POS 구조가 일치하지 않으면 규칙 기반으로 폴백
185                }
186            }
187        }
188
189        // 2. 규칙 기반 어미 분리
190        let morphemes = self.split_morpheme(&token.surface, &token.pos);
191
192        if morphemes.len() == 1 {
193            // 분리되지 않은 경우
194            return vec![SejongToken::new(
195                &token.surface,
196                &morphemes[0].1,
197                token.start_pos,
198                token.end_pos,
199            )];
200        }
201
202        // 분리된 경우
203        let mut result = Vec::new();
204        let mut current_pos = token.start_pos;
205
206        for (surface, pos) in &morphemes {
207            let char_len = surface.chars().count();
208            let end_pos = current_pos + char_len;
209
210            result.push(SejongToken::from_split(
211                surface,
212                pos,
213                current_pos,
214                end_pos,
215                &token.surface,
216                &token.pos,
217            ));
218
219            current_pos = end_pos;
220        }
221
222        result
223    }
224
225    /// 분해된 형태소를 `SejongToken`으로 변환
226    fn morphemes_to_sejong_tokens(
227        morphemes: &[DecomposedMorpheme],
228        original_token: &Token,
229    ) -> Vec<SejongToken> {
230        let mut result = Vec::new();
231        let mut current_pos = original_token.start_pos;
232
233        for morpheme in morphemes {
234            let char_len = morpheme.surface.chars().count();
235            let end_pos = current_pos + char_len;
236
237            result.push(SejongToken::from_split(
238                &morpheme.surface,
239                &morpheme.pos,
240                current_pos,
241                end_pos,
242                &original_token.surface,
243                &original_token.pos,
244            ));
245
246            current_pos = end_pos;
247        }
248
249        result
250    }
251
252    /// 토큰 목록을 세종 형식으로 변환
253    #[must_use]
254    pub fn convert_tokens(&self, tokens: &[Token]) -> Vec<SejongToken> {
255        let mut sejong_tokens: Vec<SejongToken> =
256            tokens.iter().flat_map(|t| self.convert_token(t)).collect();
257
258        // 잘못된 분해 패턴 보정 (갔다오/VV + ㄴ/ETM → 갔/VV + 다/EF)
259        apply_decomposition_corrections(&mut sejong_tokens);
260
261        // 잘못 분해된 토큰 병합 (친/VV + 구와/NNG → 친구/NNG + 와/JC)
262        apply_token_merges(&mut sejong_tokens);
263
264        // 고빈도 어휘 강제 매핑 (문맥 무관)
265        apply_lexicon_overrides(&mut sejong_tokens);
266
267        // VV "세요" 패턴 분리 (가세요/VV → 가/VV + 세요/EF)
268        sejong_tokens = apply_vv_seyo_splits(sejong_tokens);
269
270        // 컨텍스트 기반 품사 보정
271        apply_context_corrections(&mut sejong_tokens);
272
273        sejong_tokens
274    }
275
276    /// 세종 형식 문자열로 변환 (자모 정규화 포함)
277    #[must_use]
278    pub fn format_sejong(&self, tokens: &[SejongToken]) -> String {
279        tokens
280            .iter()
281            .map(|t| {
282                let normalized_surface = hangul_normalize_jamo(&t.surface);
283                format!("{}/{}", normalized_surface, t.pos)
284            })
285            .collect::<Vec<_>>()
286            .join(" ")
287    }
288
289    /// 토큰을 세종 형식 문자열로 직접 변환
290    #[must_use]
291    pub fn tokens_to_sejong_string(&self, tokens: &[Token]) -> String {
292        let sejong_tokens = self.convert_tokens(tokens);
293        self.format_sejong(&sejong_tokens)
294    }
295
296    /// 한글 자모 정규화 (외부 호환성 유지)
297    ///
298    /// ㅏ, ㅓ, ㅣ 등 자모가 포함된 문자열을 완성형으로 변환합니다.
299    #[must_use]
300    pub fn normalize_jamo(text: &str) -> String {
301        hangul_normalize_jamo(text)
302    }
303
304    #[cfg(test)]
305    #[must_use]
306    pub(crate) fn split_prefinal_ending(ending: &str) -> (String, String) {
307        super::splitter::split_prefinal_ending(ending)
308    }
309}