Skip to main content

mecab_ko_core/
tokenizer.rs

1//! # 토크나이저 모듈
2//!
3//! 형태소 분석의 메인 인터페이스입니다.
4//!
5//! ## 개요
6//!
7//! Tokenizer는 다음 컴포넌트들을 통합하여 형태소 분석을 수행합니다:
8//! - **Trie**: 사전 검색 (mecab-ko-dict)
9//! - **Matrix**: 연접 비용 계산
10//! - **Lattice**: 후보 그래프 구축
11//! - **Viterbi**: 최적 경로 탐색
12//! - `UnknownHandler`: 미등록어 처리
13//!
14//! ## 분석 과정
15//!
16//! 1. **입력 텍스트 전처리**: 공백 제거 및 위치 정보 생성
17//! 2. **Lattice 구축**: 각 위치에서 사전 검색 및 노드 추가
18//! 3. **미등록어 처리**: 사전에 없는 부분에 대해 미등록어 노드 추가
19//! 4. **Viterbi 탐색**: 최소 비용 경로 계산
20//! 5. **Token 변환**: 최적 경로의 노드를 Token으로 변환
21//!
22//! ## Example
23//!
24//! ```rust,no_run
25//! use mecab_ko_core::tokenizer::Tokenizer;
26//!
27//! // 기본 사전으로 초기화
28//! let mut tokenizer = Tokenizer::new().unwrap();
29//!
30//! // 형태소 분석
31//! let tokens = tokenizer.tokenize("아버지가방에들어가신다");
32//! for token in tokens {
33//!     println!("{}: {} ({}~{})", token.surface, token.pos, token.start_pos, token.end_pos);
34//! }
35//! ```
36
37use std::borrow::Cow;
38use std::path::Path;
39
40use mecab_ko_dict::{SystemDictionary, UserDictionary};
41
42use crate::error::Result;
43use crate::lattice::{Lattice, Node, NodeBuilder, NodeType};
44use crate::normalizer::{NormalizationConfig, Normalizer};
45use crate::pool::{PoolManager, PoolStats};
46use crate::pos_tag::PosTag;
47use crate::unknown::UnknownHandler;
48use crate::viterbi::{SpacePenalty, ViterbiSearcher};
49
50/// 토큰
51///
52/// 형태소 분석 결과의 개별 토큰을 표현합니다.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct Token {
55    /// 표면형 (원본 텍스트의 형태)
56    pub surface: String,
57
58    /// 품사 태그
59    pub pos: String,
60
61    /// 시작 위치 (문자 단위, 0-based)
62    pub start_pos: usize,
63
64    /// 끝 위치 (문자 단위, exclusive)
65    pub end_pos: usize,
66
67    /// 시작 위치 (바이트 단위)
68    pub start_byte: usize,
69
70    /// 끝 위치 (바이트 단위)
71    pub end_byte: usize,
72
73    /// 읽기 (발음)
74    pub reading: Option<String>,
75
76    /// 원형 (기본형)
77    pub lemma: Option<String>,
78
79    /// 비용
80    pub cost: i32,
81
82    /// 전체 품사 정보 (CSV feature string)
83    pub features: String,
84
85    /// 정규화된 형태 (외래어 정규화 활성화 시)
86    pub normalized: Option<String>,
87}
88
89impl Token {
90    /// 새 토큰 생성
91    #[must_use]
92    pub const fn new(
93        surface: String,
94        pos: String,
95        start_pos: usize,
96        end_pos: usize,
97        start_byte: usize,
98        end_byte: usize,
99    ) -> Self {
100        Self {
101            surface,
102            pos,
103            start_pos,
104            end_pos,
105            start_byte,
106            end_byte,
107            reading: None,
108            lemma: None,
109            cost: 0,
110            features: String::new(),
111            normalized: None,
112        }
113    }
114
115    /// Lattice 노드에서 토큰 생성
116    ///
117    /// # Arguments
118    ///
119    /// * `node` - Lattice 노드
120    #[must_use]
121    pub fn from_node(node: &Node) -> Self {
122        let features = node.feature.to_string();
123        let (pos, reading, lemma) = parse_features(&features);
124
125        Self {
126            surface: node.surface.to_string(),
127            pos: pos.to_string(),
128            start_pos: node.start_pos,
129            end_pos: node.end_pos,
130            start_byte: node.start_byte,
131            end_byte: node.end_byte,
132            reading,
133            lemma,
134            cost: node.total_cost,
135            features,
136            normalized: None,
137        }
138    }
139
140    /// 토큰 길이 (문자 단위)
141    #[inline]
142    #[must_use]
143    pub const fn char_len(&self) -> usize {
144        self.end_pos - self.start_pos
145    }
146
147    /// 토큰 길이 (바이트 단위)
148    #[inline]
149    #[must_use]
150    pub const fn byte_len(&self) -> usize {
151        self.end_byte - self.start_byte
152    }
153
154    /// 품사 태그를 `PosTag` 타입으로 파싱
155    #[must_use]
156    pub fn pos_tag(&self) -> Option<PosTag> {
157        self.pos.parse().ok()
158    }
159}
160
161/// Feature 문자열 파싱
162///
163/// `MeCab` feature 포맷: `품사,의미분류,종성유무,읽기,타입,첫번째품사,마지막품사,표현`
164///
165/// # Returns
166///
167/// (품사, 읽기, 원형)
168fn parse_features(features: &str) -> (Cow<'_, str>, Option<String>, Option<String>) {
169    // Avoid allocating a Vec – iterate the splits directly.
170    let mut split = features.splitn(5, ',');
171
172    let pos = split.next().unwrap_or("*");
173
174    // indices: 0=pos, 1=semantic, 2=jongseong, 3=reading
175    let reading = split
176        .nth(2) // skip indices 1 and 2, land on index 3
177        .filter(|s| !s.is_empty() && *s != "*")
178        .map(std::string::ToString::to_string);
179
180    let lemma = reading.clone();
181
182    (Cow::Borrowed(pos), reading, lemma)
183}
184
185/// 토크나이저
186///
187/// 형태소 분석의 메인 인터페이스입니다.
188/// 시스템 사전, 사용자 사전, 미등록어 처리기를 통합하여 형태소 분석을 수행합니다.
189///
190/// # 메모리 최적화
191///
192/// - `lattice` 재사용으로 매 분석마다 재할당 방지
193/// - `pool_manager`로 Token, Node 객체 재사용
194/// - String interning으로 중복 문자열 제거
195pub struct Tokenizer {
196    /// 시스템 사전
197    dictionary: SystemDictionary,
198
199    /// 미등록어 처리기
200    unknown_handler: UnknownHandler,
201
202    /// Viterbi 탐색기
203    viterbi_searcher: ViterbiSearcher,
204
205    /// 재사용 가능한 Lattice (성능 최적화)
206    lattice: Lattice,
207
208    /// 외래어 정규화기 (옵션)
209    normalizer: Option<Normalizer>,
210
211    /// 정규화 활성화 여부
212    enable_normalization: bool,
213
214    /// 메모리 풀 관리자
215    pool_manager: PoolManager,
216}
217
218impl Tokenizer {
219    /// 기본 사전으로 토크나이저 생성
220    ///
221    /// 환경변수 `MECAB_DICDIR`이나 기본 경로에서 시스템 사전을 로드합니다.
222    ///
223    /// # Errors
224    ///
225    /// - 사전을 찾을 수 없는 경우
226    /// - 사전 파일 포맷이 잘못된 경우
227    ///
228    /// # Example
229    ///
230    /// ```rust,no_run
231    /// use mecab_ko_core::tokenizer::Tokenizer;
232    ///
233    /// let mut tokenizer = Tokenizer::new().unwrap();
234    /// let tokens = tokenizer.tokenize("안녕하세요");
235    /// ```
236    pub fn new() -> Result<Self> {
237        let dictionary = SystemDictionary::load_default()?;
238        let unknown_handler = UnknownHandler::korean_default();
239        let viterbi_searcher =
240            ViterbiSearcher::new().with_space_penalty(SpacePenalty::korean_default());
241
242        // 초기 Lattice 생성 (빈 텍스트)
243        let lattice = Lattice::new("");
244
245        Ok(Self {
246            dictionary,
247            unknown_handler,
248            viterbi_searcher,
249            lattice,
250            normalizer: None,
251            enable_normalization: false,
252            pool_manager: PoolManager::new(),
253        })
254    }
255
256    /// 사전 경로를 지정하여 토크나이저 생성
257    ///
258    /// # Arguments
259    ///
260    /// * `dict_path` - 사전 디렉토리 경로
261    ///
262    /// # Errors
263    ///
264    /// - 사전을 찾을 수 없는 경우
265    /// - 사전 파일 포맷이 잘못된 경우
266    pub fn with_dict<P: AsRef<Path>>(dict_path: P) -> Result<Self> {
267        let dictionary = SystemDictionary::load(dict_path)?;
268        let unknown_handler = UnknownHandler::korean_default();
269        let viterbi_searcher =
270            ViterbiSearcher::new().with_space_penalty(SpacePenalty::korean_default());
271
272        let lattice = Lattice::new("");
273
274        Ok(Self {
275            dictionary,
276            unknown_handler,
277            viterbi_searcher,
278            lattice,
279            normalizer: None,
280            enable_normalization: false,
281            pool_manager: PoolManager::new(),
282        })
283    }
284
285    /// 사용자 사전 추가
286    ///
287    /// # Arguments
288    ///
289    /// * `user_dict` - 사용자 사전
290    ///
291    /// # Example
292    ///
293    /// ```rust,no_run
294    /// use mecab_ko_core::tokenizer::Tokenizer;
295    /// use mecab_ko_dict::UserDictionary;
296    ///
297    /// let mut user_dict = UserDictionary::new();
298    /// user_dict.add_entry("딥러닝", "NNG", Some(-1000), None);
299    ///
300    /// let tokenizer = Tokenizer::new().unwrap()
301    ///     .with_user_dict(user_dict);
302    /// ```
303    #[must_use]
304    pub fn with_user_dict(mut self, user_dict: UserDictionary) -> Self {
305        self.dictionary.set_user_dictionary(user_dict);
306        self
307    }
308
309    /// 사용자 사전 설정
310    pub fn set_user_dict(&mut self, user_dict: UserDictionary) {
311        self.dictionary.set_user_dictionary(user_dict);
312    }
313
314    /// 띄어쓰기 패널티 설정
315    ///
316    /// # Arguments
317    ///
318    /// * `penalty` - 띄어쓰기 패널티 설정
319    #[must_use]
320    pub fn with_space_penalty(mut self, penalty: SpacePenalty) -> Self {
321        self.viterbi_searcher = ViterbiSearcher::new().with_space_penalty(penalty);
322        self
323    }
324
325    /// 형태소 분석
326    ///
327    /// 입력 텍스트를 형태소 단위로 분석하여 Token 목록을 반환합니다.
328    ///
329    /// # Arguments
330    ///
331    /// * `text` - 분석할 텍스트
332    ///
333    /// # Returns
334    ///
335    /// 토큰 목록
336    ///
337    /// # Example
338    ///
339    /// ```rust,no_run
340    /// # use mecab_ko_core::tokenizer::Tokenizer;
341    /// # let mut tokenizer = Tokenizer::new().unwrap();
342    /// let tokens = tokenizer.tokenize("아버지가방에들어가신다");
343    /// for token in tokens {
344    ///     println!("{}: {}", token.surface, token.pos);
345    /// }
346    /// ```
347    pub fn tokenize(&mut self, text: &str) -> Vec<Token> {
348        if text.is_empty() {
349            return Vec::new();
350        }
351
352        // Lattice 재설정
353        self.lattice.reset(text);
354
355        // Lattice 구축
356        self.build_lattice();
357
358        // Viterbi 탐색
359        let path = self
360            .viterbi_searcher
361            .search(&mut self.lattice, self.dictionary.matrix());
362
363        // Token 변환
364        path.iter()
365            .filter_map(|&node_id| self.lattice.node(node_id))
366            .map(Token::from_node)
367            .collect()
368    }
369
370    /// Lattice 구축
371    ///
372    /// 입력 텍스트의 각 위치에서 사전 검색 및 미등록어 처리를 수행하여
373    /// Lattice에 노드를 추가합니다.
374    fn build_lattice(&mut self) {
375        let char_len = self.lattice.char_len();
376
377        // 각 문자 위치에서 사전 검색 및 미등록어 처리
378        for pos in 0..char_len {
379            // 사전 검색
380            let has_dict_entry = self.add_dict_nodes(pos);
381
382            // 미등록어 처리
383            self.unknown_handler
384                .add_unknown_nodes(&mut self.lattice, pos, has_dict_entry);
385        }
386    }
387
388    /// 사전 노드 추가
389    ///
390    /// 특정 위치에서 시작하는 모든 사전 엔트리를 Lattice에 추가합니다.
391    ///
392    /// # Arguments
393    ///
394    /// * `start_pos` - 시작 위치 (문자 단위)
395    ///
396    /// # Returns
397    ///
398    /// 사전 엔트리가 하나라도 있으면 true
399    fn add_dict_nodes(&mut self, start_pos: usize) -> bool {
400        // Get the byte range for the suffix starting at `start_pos` without
401        // allocating a new String.  We collect only the trie-match indices
402        // (small integers) before any lattice mutation, so the immutable borrow
403        // of `self.lattice` is released before we call `add_node`.
404        let char_len = self.lattice.char_len();
405        let search_text: &str = self.lattice.substring(start_pos, char_len);
406
407        if search_text.is_empty() {
408            return false;
409        }
410
411        // Collect match indices first (tiny integers – no O(N) string copy).
412        // This releases the immutable borrow on self.lattice before we call
413        // add_node which needs a mutable borrow.
414        let match_indices: Vec<(u32, usize)> = self
415            .dictionary
416            .trie()
417            .common_prefix_search(search_text)
418            .collect();
419
420        // Collect user-dict entries as owned data before mutating lattice.
421        // user_dict.common_prefix_search returns owned UserEntry values so
422        // this is already allocation-minimal; we just need to separate the
423        // immutable borrow from the mutable one.
424        let user_entries: Vec<_> = self
425            .dictionary
426            .user_dictionary()
427            .map(|ud| ud.common_prefix_search(search_text))
428            .unwrap_or_default();
429
430        // Immutable borrows on self.lattice are now finished; we can mutate.
431        let mut found = false;
432
433        for (index, byte_len) in match_indices {
434            if let Some(entry) = self.dictionary.get_entry(index) {
435                // Use the trie-provided byte_len to compute end_pos via
436                // binary search on char_positions, avoiding chars().count().
437                let end_pos = self
438                    .lattice
439                    .char_pos_from_start_and_byte_len(start_pos, byte_len);
440
441                self.lattice.add_node(
442                    NodeBuilder::new(&entry.surface, start_pos, end_pos)
443                        .left_id(entry.left_id)
444                        .right_id(entry.right_id)
445                        .word_cost(i32::from(entry.cost))
446                        .node_type(NodeType::Known)
447                        .feature(&entry.feature),
448                );
449
450                found = true;
451            }
452        }
453
454        for user_entry in user_entries {
455            let surface_char_len = user_entry.surface.chars().count();
456            let end_pos = start_pos + surface_char_len;
457
458            self.lattice.add_node(
459                NodeBuilder::new(&user_entry.surface, start_pos, end_pos)
460                    .left_id(user_entry.left_id)
461                    .right_id(user_entry.right_id)
462                    .word_cost(i32::from(user_entry.cost))
463                    .node_type(NodeType::User)
464                    .feature(&user_entry.feature),
465            );
466
467            found = true;
468        }
469
470        found
471    }
472
473    /// Lattice를 반환하여 검사
474    ///
475    /// Viterbi 탐색 전의 Lattice 상태를 반환합니다. (디버깅/테스트용)
476    ///
477    /// # Arguments
478    ///
479    /// * `text` - 분석할 텍스트
480    ///
481    /// # Returns
482    ///
483    /// 구축된 Lattice
484    pub fn tokenize_to_lattice(&mut self, text: &str) -> &Lattice {
485        if !text.is_empty() {
486            self.lattice.reset(text);
487            self.build_lattice();
488        }
489        &self.lattice
490    }
491
492    /// 표면형만 추출 (wakati)
493    ///
494    /// # Arguments
495    ///
496    /// * `text` - 분석할 텍스트
497    ///
498    /// # Returns
499    ///
500    /// 표면형 목록
501    pub fn wakati(&mut self, text: &str) -> Vec<String> {
502        self.tokenize(text).into_iter().map(|t| t.surface).collect()
503    }
504
505    /// 명사만 추출
506    ///
507    /// # Arguments
508    ///
509    /// * `text` - 분석할 텍스트
510    ///
511    /// # Returns
512    ///
513    /// 명사 목록
514    pub fn nouns(&mut self, text: &str) -> Vec<String> {
515        self.tokenize(text)
516            .into_iter()
517            .filter(|t| t.pos.starts_with("NN"))
518            .map(|t| t.surface)
519            .collect()
520    }
521
522    /// 형태소만 추출 (wakati와 동일)
523    pub fn morphs(&mut self, text: &str) -> Vec<String> {
524        self.wakati(text)
525    }
526
527    /// 품사 태깅
528    ///
529    /// # Arguments
530    ///
531    /// * `text` - 분석할 텍스트
532    ///
533    /// # Returns
534    ///
535    /// (표면형, 품사) 쌍의 벡터
536    pub fn pos(&mut self, text: &str) -> Vec<(String, String)> {
537        self.tokenize(text)
538            .into_iter()
539            .map(|t| (t.surface, t.pos))
540            .collect()
541    }
542
543    /// 시스템 사전 참조 반환
544    #[must_use]
545    pub const fn dictionary(&self) -> &SystemDictionary {
546        &self.dictionary
547    }
548
549    /// Lattice 통계 정보 (디버깅용)
550    pub fn lattice_stats(&self) -> crate::lattice::LatticeStats {
551        self.lattice.stats()
552    }
553
554    /// 메모리 풀 통계 정보
555    ///
556    /// 메모리 풀의 사용 현황을 반환합니다.
557    #[must_use]
558    pub fn pool_stats(&self) -> PoolStats {
559        self.pool_manager.stats()
560    }
561
562    /// 메모리 풀 초기화
563    ///
564    /// 모든 풀을 비워 메모리를 해제합니다.
565    /// 장기 실행 프로세스에서 주기적으로 호출하여 메모리 누수 방지.
566    pub fn clear_pools(&self) {
567        self.pool_manager.clear_all();
568    }
569
570    /// 외래어 정규화 활성화
571    ///
572    /// # Arguments
573    ///
574    /// * `enable` - 정규화 활성화 여부
575    /// * `config` - 정규화 설정 (None이면 기본 설정 사용)
576    ///
577    /// # Errors
578    ///
579    /// 정규화기 초기화 실패 시 에러 반환
580    pub fn set_normalization(
581        &mut self,
582        enable: bool,
583        config: Option<NormalizationConfig>,
584    ) -> Result<()> {
585        self.enable_normalization = enable;
586
587        if enable {
588            let normalizer_config = config.unwrap_or_default();
589            self.normalizer = Some(Normalizer::new(normalizer_config)?);
590        } else {
591            self.normalizer = None;
592        }
593
594        Ok(())
595    }
596
597    /// 외래어 정규화기 참조 반환
598    #[must_use]
599    pub const fn normalizer(&self) -> Option<&Normalizer> {
600        self.normalizer.as_ref()
601    }
602
603    /// 정규화가 활성화되어 있는지 확인
604    #[must_use]
605    pub const fn is_normalization_enabled(&self) -> bool {
606        self.enable_normalization
607    }
608
609    /// 정규화 적용 형태소 분석
610    ///
611    /// 토큰의 표면형에 대해 정규화를 적용하고, 정규화된 형태도 함께 반환합니다.
612    ///
613    /// # Arguments
614    ///
615    /// * `text` - 분석할 텍스트
616    ///
617    /// # Returns
618    ///
619    /// 정규화 정보가 포함된 토큰 목록
620    pub fn tokenize_with_normalization(&mut self, text: &str) -> Vec<Token> {
621        let mut tokens = self.tokenize(text);
622
623        // 정규화 적용
624        if let Some(normalizer) = &self.normalizer {
625            for token in &mut tokens {
626                token.normalized = Some(normalizer.normalize(&token.surface));
627            }
628        }
629
630        tokens
631    }
632
633    /// 변이형 확장 검색
634    ///
635    /// 입력 단어의 변이형들을 모두 고려하여 사전 검색을 수행합니다.
636    ///
637    /// # Arguments
638    ///
639    /// * `word` - 검색할 단어
640    ///
641    /// # Returns
642    ///
643    /// `(표준형, [변이형들])` 튜플
644    #[must_use]
645    pub fn get_word_variants(&self, word: &str) -> (String, Vec<String>) {
646        self.normalizer.as_ref().map_or_else(
647            || (word.to_string(), Vec::new()),
648            |normalizer| {
649                let standard = normalizer.normalize(word);
650                let variants = normalizer.get_variants(&standard);
651                (standard, variants)
652            },
653        )
654    }
655}
656
657// Note: Default implementation is not provided for Tokenizer because initialization
658// can fail (dictionary loading, etc.). Use Tokenizer::new() explicitly instead.
659
660#[cfg(test)]
661#[allow(clippy::expect_used, clippy::vec_init_then_push)]
662mod tests {
663    use super::*;
664    use mecab_ko_dict::{matrix::DenseMatrix, trie::TrieBuilder, DictEntry};
665
666    /// 테스트용 토크나이저 생성
667    fn create_test_tokenizer() -> Tokenizer {
668        // 테스트용 Trie 생성
669        let mut trie_entries = vec![
670            ("아버지", 0u32),
671            ("가", 1),
672            ("방", 2),
673            ("에", 3),
674            ("들어가", 4),
675            ("신다", 5),
676        ];
677        let trie_bytes = TrieBuilder::build_unsorted(&mut trie_entries).expect("should build trie");
678        let trie = mecab_ko_dict::Trie::from_vec(trie_bytes);
679
680        // 테스트용 Matrix 생성
681        let matrix = DenseMatrix::new(10, 10, 100);
682        let matrix = mecab_ko_dict::matrix::ConnectionMatrix::Dense(matrix);
683
684        // 테스트용 엔트리 생성
685        let mut entries = Vec::new();
686        entries.push(DictEntry::new(
687            "아버지",
688            1,
689            1,
690            1000,
691            "NNG,*,T,아버지,*,*,*,*",
692        ));
693        entries.push(DictEntry::new("가", 5, 5, 500, "JKS,*,F,가,*,*,*,*"));
694        entries.push(DictEntry::new("방", 2, 2, 2000, "NNG,*,T,방,*,*,*,*"));
695        entries.push(DictEntry::new("에", 6, 6, 400, "JKB,*,F,에,*,*,*,*"));
696        entries.push(DictEntry::new(
697            "들어가",
698            3,
699            3,
700            1500,
701            "VV,*,F,들어가다,*,*,*,*",
702        ));
703        entries.push(DictEntry::new("신다", 4, 4, 1800, "VV+EP,*,F,신다,*,*,*,*"));
704
705        let dictionary = SystemDictionary::new_test(
706            std::path::PathBuf::from("./test_dic"),
707            trie,
708            matrix,
709            entries,
710        );
711
712        let unknown_handler = UnknownHandler::korean_default();
713        let viterbi_searcher =
714            ViterbiSearcher::new().with_space_penalty(SpacePenalty::korean_default());
715        let lattice = Lattice::new("");
716
717        Tokenizer {
718            dictionary,
719            unknown_handler,
720            viterbi_searcher,
721            lattice,
722            normalizer: None,
723            enable_normalization: false,
724            pool_manager: PoolManager::new(),
725        }
726    }
727
728    #[test]
729    fn test_token_creation() {
730        let token = Token::new("안녕".to_string(), "NNG".to_string(), 0, 2, 0, 6);
731
732        assert_eq!(token.surface, "안녕");
733        assert_eq!(token.pos, "NNG");
734        assert_eq!(token.start_pos, 0);
735        assert_eq!(token.end_pos, 2);
736        assert_eq!(token.char_len(), 2);
737        assert_eq!(token.byte_len(), 6);
738    }
739
740    #[test]
741    fn test_parse_features() {
742        let features = "NNG,*,T,안녕,*,*,*,*";
743        let (pos, reading, lemma) = parse_features(features);
744
745        assert_eq!(pos, "NNG");
746        assert_eq!(reading, Some("안녕".to_string()));
747        assert_eq!(lemma, Some("안녕".to_string()));
748    }
749
750    #[test]
751    fn test_parse_features_no_reading() {
752        let features = "JKS,*,F,*,*,*,*,*";
753        let (pos, reading, _lemma) = parse_features(features);
754
755        assert_eq!(pos, "JKS");
756        assert_eq!(reading, None);
757    }
758
759    #[test]
760    fn test_tokenize_simple() {
761        let mut tokenizer = create_test_tokenizer();
762        let tokens = tokenizer.tokenize("아버지");
763
764        assert!(!tokens.is_empty());
765        assert_eq!(tokens[0].surface, "아버지");
766        assert_eq!(tokens[0].pos, "NNG");
767    }
768
769    #[test]
770    fn test_tokenize_with_particle() {
771        let mut tokenizer = create_test_tokenizer();
772        let tokens = tokenizer.tokenize("아버지가");
773
774        assert_eq!(tokens.len(), 2);
775        assert_eq!(tokens[0].surface, "아버지");
776        assert_eq!(tokens[0].pos, "NNG");
777        assert_eq!(tokens[1].surface, "가");
778        assert_eq!(tokens[1].pos, "JKS");
779    }
780
781    #[test]
782    fn test_tokenize_complex() {
783        let mut tokenizer = create_test_tokenizer();
784        let tokens = tokenizer.tokenize("아버지가방에들어가신다");
785
786        // 최소한 "아버지", "가", "방", "에", ... 등이 분석되어야 함
787        assert!(!tokens.is_empty());
788
789        // 첫 토큰은 "아버지"
790        assert_eq!(tokens[0].surface, "아버지");
791    }
792
793    #[test]
794    fn test_tokenize_empty() {
795        let mut tokenizer = create_test_tokenizer();
796        let tokens = tokenizer.tokenize("");
797
798        assert!(tokens.is_empty());
799    }
800
801    #[test]
802    fn test_tokenize_with_spaces() {
803        let mut tokenizer = create_test_tokenizer();
804        let tokens = tokenizer.tokenize("아버지 가방");
805
806        // 공백은 제거되고 "아버지가방"으로 분석됨
807        assert!(!tokens.is_empty());
808    }
809
810    #[test]
811    fn test_wakati() {
812        let mut tokenizer = create_test_tokenizer();
813        let surfaces = tokenizer.wakati("아버지가");
814
815        assert_eq!(surfaces.len(), 2);
816        assert_eq!(surfaces[0], "아버지");
817        assert_eq!(surfaces[1], "가");
818    }
819
820    #[test]
821    fn test_nouns() {
822        let mut tokenizer = create_test_tokenizer();
823        let nouns = tokenizer.nouns("아버지가방에");
824
825        // "아버지"와 "방"이 명사 (NNG)
826        assert!(nouns.contains(&"아버지".to_string()));
827        assert!(nouns.contains(&"방".to_string()));
828        assert!(!nouns.contains(&"가".to_string())); // 조사는 제외
829    }
830
831    #[test]
832    fn test_pos() {
833        let mut tokenizer = create_test_tokenizer();
834        let pos_tags = tokenizer.pos("아버지가");
835
836        assert_eq!(pos_tags.len(), 2);
837        assert_eq!(pos_tags[0], ("아버지".to_string(), "NNG".to_string()));
838        assert_eq!(pos_tags[1], ("가".to_string(), "JKS".to_string()));
839    }
840
841    #[test]
842    fn test_tokenize_to_lattice() {
843        let mut tokenizer = create_test_tokenizer();
844        let lattice = tokenizer.tokenize_to_lattice("아버지가");
845
846        // Lattice에 노드가 추가되었는지 확인
847        assert!(lattice.node_count() > 2); // BOS, EOS 외에 최소 1개 이상
848
849        // 통계 확인
850        let stats = lattice.stats();
851        assert!(stats.total_nodes > 2);
852    }
853
854    #[test]
855    fn test_lattice_stats() {
856        let mut tokenizer = create_test_tokenizer();
857        tokenizer.tokenize("아버지가");
858
859        let stats = tokenizer.lattice_stats();
860        assert!(stats.total_nodes > 0);
861        assert!(stats.char_length > 0);
862    }
863
864    #[test]
865    fn test_token_positions() {
866        let mut tokenizer = create_test_tokenizer();
867        let tokens = tokenizer.tokenize("아버지가");
868
869        // 첫 번째 토큰: "아버지"
870        assert_eq!(tokens[0].start_pos, 0);
871        assert_eq!(tokens[0].end_pos, 3);
872
873        // 두 번째 토큰: "가"
874        assert_eq!(tokens[1].start_pos, 3);
875        assert_eq!(tokens[1].end_pos, 4);
876    }
877
878    #[test]
879    fn test_multiple_tokenize_calls() {
880        let mut tokenizer = create_test_tokenizer();
881
882        // 첫 번째 분석
883        let tokens1 = tokenizer.tokenize("아버지");
884        assert!(!tokens1.is_empty());
885
886        // 두 번째 분석 (Lattice 재사용)
887        let tokens2 = tokenizer.tokenize("가방");
888        assert!(!tokens2.is_empty());
889
890        // 각 분석이 독립적으로 동작해야 함
891        assert_ne!(tokens1[0].surface, tokens2[0].surface);
892    }
893
894    #[test]
895    fn test_token_from_node() {
896        use crate::lattice::Node;
897        use std::borrow::Cow;
898
899        let node = Node {
900            id: 1,
901            surface: Cow::Borrowed("테스트"),
902            start_pos: 0,
903            end_pos: 3,
904            start_byte: 0,
905            end_byte: 9,
906            left_id: 1,
907            right_id: 1,
908            word_cost: 1000,
909            total_cost: 1500,
910            prev_node_id: 0,
911            node_type: NodeType::Known,
912            feature: Cow::Borrowed("NNG,*,T,테스트,*,*,*,*"),
913            has_space_before: false,
914        };
915
916        let token = Token::from_node(&node);
917
918        assert_eq!(token.surface, "테스트");
919        assert_eq!(token.pos, "NNG");
920        assert_eq!(token.start_pos, 0);
921        assert_eq!(token.end_pos, 3);
922        assert_eq!(token.reading, Some("테스트".to_string()));
923        assert_eq!(token.cost, 1500);
924    }
925
926    #[test]
927    fn test_with_user_dict() {
928        let mut tokenizer = create_test_tokenizer();
929
930        let mut user_dict = UserDictionary::new();
931        user_dict.add_entry("딥러닝", "NNG", Some(-1000), None);
932
933        tokenizer.set_user_dict(user_dict);
934
935        // 사용자 사전이 설정되었는지 확인
936        assert!(tokenizer.dictionary().user_dictionary().is_some());
937    }
938}