Skip to main content

mecab_ko_core/
lattice.rs

1//! Lattice 자료구조
2//!
3//! 형태소 분석을 위한 격자(Lattice) 구조를 제공합니다.
4//!
5//! # 개요
6//!
7//! Lattice는 입력 텍스트의 모든 가능한 형태소 분석 결과를 DAG(Directed Acyclic Graph)
8//! 형태로 표현합니다. Viterbi 알고리즘을 통해 최적 경로를 찾습니다.
9//!
10//! # 구조
11//!
12//! ```text
13//! 입력: "아버지가"
14//!
15//!     BOS ─→ [아버지] ─→ [가] ─→ EOS
16//!        │           │
17//!        └→ [아버] ─→ [지가] ─┘
18//!        │
19//!        └→ [아] ─→ [버지가] ─┘
20//! ```
21//!
22//! # 한국어 특화
23//!
24//! - 띄어쓰기 패널티 지원 (`left-space-penalty-factor`)
25//! - UTF-8 문자 위치 정확한 처리
26//! - 종성 기반 조사 연결 규칙
27//!
28//! # Example
29//!
30//! ```rust
31//! use mecab_ko_core::lattice::{Lattice, NodeBuilder};
32//!
33//! let mut lattice = Lattice::new("안녕하세요");
34//!
35//! // 노드 추가 (사전에서 검색된 결과)
36//! lattice.add_node(
37//!     NodeBuilder::new("안녕", 0, 2)
38//!         .left_id(1)
39//!         .right_id(1)
40//!         .word_cost(1000)
41//!         .feature("NNG,*,F,안녕,*,*,*,*")
42//!         .build()
43//! );
44//!
45//! assert_eq!(lattice.node_count(), 3); // BOS + 추가노드 + EOS
46//! ```
47
48use std::borrow::Cow;
49
50/// 노드 ID 타입
51pub type NodeId = u32;
52
53/// 특수 노드 ID
54pub const BOS_NODE_ID: NodeId = 0;
55/// EOS 노드는 마지막에 동적 할당
56pub const INVALID_NODE_ID: NodeId = u32::MAX;
57
58/// BOS (Beginning of Sentence) 컨텍스트 ID
59pub const BOS_CONTEXT_ID: u16 = 0;
60
61/// EOS (End of Sentence) 컨텍스트 ID
62pub const EOS_CONTEXT_ID: u16 = 0;
63
64/// 노드 타입
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
66pub enum NodeType {
67    /// 문장 시작 (Beginning of Sentence)
68    Bos,
69    /// 문장 끝 (End of Sentence)
70    Eos,
71    /// 사전에서 찾은 알려진 단어
72    #[default]
73    Known,
74    /// 미등록어 (Unknown word)
75    Unknown,
76    /// 사용자 정의 사전
77    User,
78}
79
80/// Lattice 노드
81///
82/// 형태소 후보를 나타내는 노드입니다.
83#[derive(Debug, Clone)]
84pub struct Node {
85    /// 노드 ID (Lattice 내에서 유일)
86    pub id: NodeId,
87
88    /// 표면형 (surface form)
89    pub surface: Cow<'static, str>,
90
91    /// 시작 위치 (문자 단위, 0-based)
92    pub start_pos: usize,
93
94    /// 끝 위치 (문자 단위, exclusive)
95    pub end_pos: usize,
96
97    /// 시작 위치 (바이트 단위)
98    pub start_byte: usize,
99
100    /// 끝 위치 (바이트 단위)
101    pub end_byte: usize,
102
103    /// 좌문맥 ID (연접 비용 계산용)
104    pub left_id: u16,
105
106    /// 우문맥 ID (연접 비용 계산용)
107    pub right_id: u16,
108
109    /// 단어 비용 (사전에 기록된 비용)
110    pub word_cost: i32,
111
112    /// 누적 비용 (Viterbi 계산용)
113    pub total_cost: i32,
114
115    /// 최적 경로의 이전 노드 ID (backtrack용)
116    pub prev_node_id: NodeId,
117
118    /// 노드 타입
119    pub node_type: NodeType,
120
121    /// 품사 및 부가 정보 (CSV feature string)
122    pub feature: Cow<'static, str>,
123
124    /// 띄어쓰기 앞에 있는지 여부 (space penalty 적용용)
125    pub has_space_before: bool,
126}
127
128impl Node {
129    /// BOS 노드 생성
130    #[must_use]
131    pub const fn bos() -> Self {
132        Self {
133            id: BOS_NODE_ID,
134            surface: Cow::Borrowed("BOS"),
135            start_pos: 0,
136            end_pos: 0,
137            start_byte: 0,
138            end_byte: 0,
139            left_id: BOS_CONTEXT_ID,
140            right_id: BOS_CONTEXT_ID,
141            word_cost: 0,
142            total_cost: 0,
143            prev_node_id: INVALID_NODE_ID,
144            node_type: NodeType::Bos,
145            feature: Cow::Borrowed("BOS/EOS,*,*,*,*,*,*,*"),
146            has_space_before: false,
147        }
148    }
149
150    /// EOS 노드 생성
151    #[must_use]
152    pub const fn eos(id: NodeId, char_len: usize, byte_len: usize) -> Self {
153        Self {
154            id,
155            surface: Cow::Borrowed("EOS"),
156            start_pos: char_len,
157            end_pos: char_len,
158            start_byte: byte_len,
159            end_byte: byte_len,
160            left_id: EOS_CONTEXT_ID,
161            right_id: EOS_CONTEXT_ID,
162            word_cost: 0,
163            total_cost: i32::MAX,
164            prev_node_id: INVALID_NODE_ID,
165            node_type: NodeType::Eos,
166            feature: Cow::Borrowed("BOS/EOS,*,*,*,*,*,*,*"),
167            has_space_before: false,
168        }
169    }
170
171    /// 노드가 BOS인지 확인
172    #[inline]
173    #[must_use]
174    pub fn is_bos(&self) -> bool {
175        self.node_type == NodeType::Bos
176    }
177
178    /// 노드가 EOS인지 확인
179    #[inline]
180    #[must_use]
181    pub fn is_eos(&self) -> bool {
182        self.node_type == NodeType::Eos
183    }
184
185    /// 노드 길이 (문자 단위)
186    #[inline]
187    #[must_use]
188    pub const fn char_len(&self) -> usize {
189        self.end_pos - self.start_pos
190    }
191
192    /// 노드 길이 (바이트 단위)
193    #[inline]
194    #[must_use]
195    pub const fn byte_len(&self) -> usize {
196        self.end_byte - self.start_byte
197    }
198}
199
200/// 노드 빌더 (Builder 패턴)
201#[derive(Debug, Clone)]
202pub struct NodeBuilder {
203    surface: String,
204    start_pos: usize,
205    end_pos: usize,
206    start_byte: usize,
207    end_byte: usize,
208    left_id: u16,
209    right_id: u16,
210    word_cost: i32,
211    node_type: NodeType,
212    feature: String,
213    has_space_before: bool,
214}
215
216impl NodeBuilder {
217    /// 새 빌더 생성
218    ///
219    /// # Arguments
220    ///
221    /// * `surface` - 표면형
222    /// * `start_pos` - 시작 위치 (문자 단위)
223    /// * `end_pos` - 끝 위치 (문자 단위)
224    #[must_use]
225    pub fn new(surface: &str, start_pos: usize, end_pos: usize) -> Self {
226        Self {
227            surface: surface.to_string(),
228            start_pos,
229            end_pos,
230            start_byte: 0,
231            end_byte: 0,
232            left_id: 0,
233            right_id: 0,
234            word_cost: 0,
235            node_type: NodeType::Known,
236            feature: String::new(),
237            has_space_before: false,
238        }
239    }
240
241    /// 바이트 위치 설정
242    #[must_use]
243    pub const fn byte_positions(mut self, start: usize, end: usize) -> Self {
244        self.start_byte = start;
245        self.end_byte = end;
246        self
247    }
248
249    /// 좌문맥 ID 설정
250    #[must_use]
251    pub const fn left_id(mut self, id: u16) -> Self {
252        self.left_id = id;
253        self
254    }
255
256    /// 우문맥 ID 설정
257    #[must_use]
258    pub const fn right_id(mut self, id: u16) -> Self {
259        self.right_id = id;
260        self
261    }
262
263    /// 단어 비용 설정
264    #[must_use]
265    pub const fn word_cost(mut self, cost: i32) -> Self {
266        self.word_cost = cost;
267        self
268    }
269
270    /// 노드 타입 설정
271    #[must_use]
272    pub const fn node_type(mut self, node_type: NodeType) -> Self {
273        self.node_type = node_type;
274        self
275    }
276
277    /// 품사 정보 설정
278    #[must_use]
279    pub fn feature(mut self, feature: &str) -> Self {
280        self.feature = feature.to_string();
281        self
282    }
283
284    /// 띄어쓰기 앞 여부 설정
285    #[must_use]
286    pub const fn has_space_before(mut self, value: bool) -> Self {
287        self.has_space_before = value;
288        self
289    }
290
291    /// Node 빌드 (ID는 Lattice에서 할당)
292    #[must_use]
293    pub const fn build(self) -> Self {
294        self
295    }
296}
297
298/// 문자 위치 정보
299///
300/// UTF-8에서 문자 위치와 바이트 위치의 매핑
301#[derive(Debug, Clone)]
302pub struct CharPositions {
303    /// 각 문자의 바이트 시작 위치
304    char_to_byte: Vec<usize>,
305    /// 총 바이트 길이
306    total_bytes: usize,
307}
308
309impl CharPositions {
310    /// 문자열에서 위치 정보 생성
311    #[must_use]
312    pub fn new(text: &str) -> Self {
313        let mut char_to_byte = Vec::with_capacity(text.chars().count() + 1);
314        let mut byte_pos = 0;
315
316        for c in text.chars() {
317            char_to_byte.push(byte_pos);
318            byte_pos += c.len_utf8();
319        }
320        char_to_byte.push(byte_pos); // 마지막 위치 (EOS용)
321
322        Self {
323            char_to_byte,
324            total_bytes: byte_pos,
325        }
326    }
327
328    /// 문자 위치 → 바이트 위치 변환
329    #[inline]
330    #[must_use]
331    pub fn char_to_byte(&self, char_pos: usize) -> usize {
332        self.char_to_byte
333            .get(char_pos)
334            .copied()
335            .unwrap_or(self.total_bytes)
336    }
337
338    /// 문자 개수
339    #[inline]
340    #[must_use]
341    pub fn char_count(&self) -> usize {
342        if self.char_to_byte.is_empty() {
343            0
344        } else {
345            self.char_to_byte.len() - 1
346        }
347    }
348
349    /// 바이트 위치 → 문자 위치 변환 (binary search)
350    ///
351    /// Returns the char index whose byte start equals `byte_pos`, or
352    /// `char_count()` if not found (e.g. `byte_pos` == `total_bytes`).
353    #[inline]
354    #[must_use]
355    pub fn byte_to_char(&self, byte_pos: usize) -> usize {
356        self.char_to_byte
357            .binary_search(&byte_pos)
358            .unwrap_or_else(|_| self.char_count())
359    }
360
361    /// 총 바이트 수
362    #[inline]
363    #[must_use]
364    pub const fn byte_count(&self) -> usize {
365        self.total_bytes
366    }
367}
368
369/// 띄어쓰기 위치 정보
370///
371/// Stores positions as a sorted `Vec` rather than a `HashSet`.  For typical
372/// sentences the number of spaces is small, so binary search in a sorted `Vec`
373/// is cheaper than hashing and avoids `HashMap` overhead.
374#[derive(Debug, Clone, Default)]
375pub struct SpacePositions {
376    /// 띄어쓰기 직후 문자 위치 목록 (정렬된 상태)
377    positions: Vec<usize>,
378}
379
380impl SpacePositions {
381    /// 문자열에서 띄어쓰기 위치 추출
382    #[must_use]
383    pub fn new(text: &str) -> Self {
384        let mut positions = Vec::new();
385        let mut char_pos = 0;
386        let mut prev_is_space = false;
387
388        for c in text.chars() {
389            if prev_is_space && !c.is_whitespace() {
390                positions.push(char_pos);
391            }
392            prev_is_space = c.is_whitespace();
393            if !c.is_whitespace() {
394                char_pos += 1;
395            }
396        }
397
398        // positions are already in ascending order since we iterate left→right
399        Self { positions }
400    }
401
402    /// 해당 위치가 띄어쓰기 직후인지 확인
403    #[inline]
404    #[must_use]
405    pub fn has_space_before(&self, char_pos: usize) -> bool {
406        self.positions.binary_search(&char_pos).is_ok()
407    }
408}
409
410/// Lattice 구조
411///
412/// 입력 텍스트의 모든 형태소 분석 후보를 담는 격자 구조입니다.
413///
414/// # 메모리 최적화
415///
416/// - `nodes` 벡터는 재사용을 위해 `clear()` 시 용량 유지
417/// - `ends_at`, `starts_at`도 용량 유지하여 재할당 최소화
418#[derive(Debug)]
419pub struct Lattice {
420    /// 원본 텍스트 (공백 제거 전)
421    original_text: String,
422
423    /// 분석용 텍스트 (공백 제거 후)
424    text: String,
425
426    /// 문자 위치 정보
427    char_positions: CharPositions,
428
429    /// 띄어쓰기 위치 정보
430    space_positions: SpacePositions,
431
432    /// Stripped char position → original-text byte offset.
433    /// Length = stripped char count + 1 (sentinel for EOS).
434    stripped_to_original_byte: Vec<usize>,
435
436    /// 모든 노드 (ID로 인덱싱)
437    /// 메모리 최적화: 재사용을 위해 용량 유지
438    nodes: Vec<Node>,
439
440    /// 각 문자 위치에서 끝나는 노드 ID 목록
441    /// `ends_at[pos]` = pos에서 끝나는 노드들의 ID
442    ends_at: Vec<Vec<NodeId>>,
443
444    /// 각 문자 위치에서 시작하는 노드 ID 목록
445    /// `starts_at[pos]` = pos에서 시작하는 노드들의 ID
446    starts_at: Vec<Vec<NodeId>>,
447
448    /// BOS 노드 ID
449    bos_id: NodeId,
450
451    /// EOS 노드 ID
452    eos_id: NodeId,
453}
454
455impl Lattice {
456    /// 새 Lattice 생성
457    ///
458    /// # Arguments
459    ///
460    /// * `text` - 분석할 텍스트
461    ///
462    /// # Example
463    ///
464    /// ```rust
465    /// use mecab_ko_core::lattice::Lattice;
466    ///
467    /// let lattice = Lattice::new("안녕하세요");
468    /// assert_eq!(lattice.text(), "안녕하세요");
469    /// ```
470    #[must_use]
471    pub fn new(text: &str) -> Self {
472        // 공백 제거 (분석용)
473        let original_text = text.to_string();
474        let text_no_space: String = text.chars().filter(|c| !c.is_whitespace()).collect();
475
476        let char_positions = CharPositions::new(&text_no_space);
477        let space_positions = SpacePositions::new(text);
478        let stripped_to_original_byte = Self::build_stripped_to_original(text);
479
480        let char_len = char_positions.char_count();
481        let byte_len = char_positions.byte_count();
482
483        // BOS 노드
484        let bos = Node::bos();
485        let bos_id = bos.id;
486
487        // EOS 노드 (ID = 1)
488        let eos_id = 1;
489        let eos = Node::eos(eos_id, char_len, byte_len);
490
491        // 노드 벡터 초기화 (BOS, EOS)
492        let nodes = vec![bos, eos];
493
494        // 위치별 노드 목록 초기화
495        let mut ends_at = vec![Vec::new(); char_len + 1];
496        let mut starts_at = vec![Vec::new(); char_len + 1];
497
498        // BOS는 위치 0에서 끝남
499        ends_at[0].push(bos_id);
500        // EOS는 마지막 위치에서 시작
501        starts_at[char_len].push(eos_id);
502
503        Self {
504            original_text,
505            text: text_no_space,
506            char_positions,
507            space_positions,
508            stripped_to_original_byte,
509            nodes,
510            ends_at,
511            starts_at,
512            bos_id,
513            eos_id,
514        }
515    }
516
517    /// 분석용 텍스트 반환 (공백 제거됨)
518    #[inline]
519    #[must_use]
520    pub fn text(&self) -> &str {
521        &self.text
522    }
523
524    /// 원본 텍스트 반환
525    #[inline]
526    #[must_use]
527    pub fn original_text(&self) -> &str {
528        &self.original_text
529    }
530
531    /// Map a stripped-text char position to the corresponding byte offset in
532    /// the original (space-included) text.
533    #[inline]
534    #[must_use]
535    pub fn original_byte_pos(&self, stripped_char_pos: usize) -> usize {
536        self.stripped_to_original_byte
537            .get(stripped_char_pos)
538            .copied()
539            .unwrap_or(self.original_text.len())
540    }
541
542    fn build_stripped_to_original(original: &str) -> Vec<usize> {
543        let mut map = Vec::new();
544        let mut byte_offset = 0;
545        for c in original.chars() {
546            if !c.is_whitespace() {
547                map.push(byte_offset);
548            }
549            byte_offset += c.len_utf8();
550        }
551        map.push(byte_offset);
552        map
553    }
554
555    /// 문자 개수
556    #[inline]
557    #[must_use]
558    pub fn char_len(&self) -> usize {
559        self.char_positions.char_count()
560    }
561
562    /// 특정 위치에서 시작하는 바이트 오프셋을 주어진 바이트 길이만큼 더한 뒤
563    /// 해당 위치의 문자 인덱스를 반환합니다.
564    ///
565    /// `start_pos`의 바이트 시작 위치에 `byte_len`을 더한 결과에 대응하는
566    /// 문자 인덱스를 binary search로 빠르게 구합니다.
567    /// 이를 통해 `entry.surface.chars().count()` 비용을 줄일 수 있습니다.
568    #[inline]
569    #[must_use]
570    pub fn char_pos_from_start_and_byte_len(&self, start_pos: usize, byte_len: usize) -> usize {
571        let start_byte = self.char_positions.char_to_byte(start_pos);
572        self.char_positions.byte_to_char(start_byte + byte_len)
573    }
574
575    /// 바이트 길이
576    #[inline]
577    #[must_use]
578    pub const fn byte_len(&self) -> usize {
579        self.char_positions.byte_count()
580    }
581
582    /// 노드 개수 (BOS, EOS 포함)
583    #[inline]
584    #[must_use]
585    pub fn node_count(&self) -> usize {
586        self.nodes.len()
587    }
588
589    /// BOS 노드 참조
590    #[inline]
591    #[must_use]
592    pub fn bos(&self) -> &Node {
593        &self.nodes[self.bos_id as usize]
594    }
595
596    /// EOS 노드 참조
597    #[inline]
598    #[must_use]
599    pub fn eos(&self) -> &Node {
600        &self.nodes[self.eos_id as usize]
601    }
602
603    /// EOS 노드 가변 참조
604    #[inline]
605    pub fn eos_mut(&mut self) -> &mut Node {
606        let eos_id = self.eos_id as usize;
607        &mut self.nodes[eos_id]
608    }
609
610    /// ID로 노드 참조
611    #[inline]
612    #[must_use]
613    pub fn node(&self, id: NodeId) -> Option<&Node> {
614        self.nodes.get(id as usize)
615    }
616
617    /// ID로 노드 가변 참조
618    #[inline]
619    pub fn node_mut(&mut self, id: NodeId) -> Option<&mut Node> {
620        self.nodes.get_mut(id as usize)
621    }
622
623    /// 모든 노드 반복자
624    #[inline]
625    pub fn nodes(&self) -> impl Iterator<Item = &Node> {
626        self.nodes.iter()
627    }
628
629    /// 특정 위치에서 끝나는 노드들
630    #[inline]
631    pub fn nodes_ending_at(&self, pos: usize) -> impl Iterator<Item = &Node> {
632        self.ends_at
633            .get(pos)
634            .map(|ids| ids.iter())
635            .into_iter()
636            .flatten()
637            .filter_map(|&id| self.nodes.get(id as usize))
638    }
639
640    /// 특정 위치에서 시작하는 노드들
641    #[inline]
642    pub fn nodes_starting_at(&self, pos: usize) -> impl Iterator<Item = &Node> {
643        self.starts_at
644            .get(pos)
645            .map(|ids| ids.iter())
646            .into_iter()
647            .flatten()
648            .filter_map(|&id| self.nodes.get(id as usize))
649    }
650
651    /// 노드 추가
652    ///
653    /// # Arguments
654    ///
655    /// * `builder` - `NodeBuilder`로 구성된 노드 정보
656    ///
657    /// # Returns
658    ///
659    /// 추가된 노드의 ID
660    #[allow(clippy::cast_possible_truncation)]
661    pub fn add_node(&mut self, builder: NodeBuilder) -> NodeId {
662        let id = self.nodes.len() as NodeId;
663
664        // 바이트 위치 계산
665        let start_byte = self.char_positions.char_to_byte(builder.start_pos);
666        let end_byte = self.char_positions.char_to_byte(builder.end_pos);
667
668        // 띄어쓰기 앞 여부 확인
669        let has_space_before =
670            builder.has_space_before || self.space_positions.has_space_before(builder.start_pos);
671
672        let node = Node {
673            id,
674            surface: Cow::Owned(builder.surface),
675            start_pos: builder.start_pos,
676            end_pos: builder.end_pos,
677            start_byte,
678            end_byte,
679            left_id: builder.left_id,
680            right_id: builder.right_id,
681            word_cost: builder.word_cost,
682            total_cost: i32::MAX, // Viterbi에서 계산
683            prev_node_id: INVALID_NODE_ID,
684            node_type: builder.node_type,
685            feature: Cow::Owned(builder.feature),
686            has_space_before,
687        };
688
689        // 위치 인덱스 업데이트
690        if builder.start_pos < self.starts_at.len() {
691            self.starts_at[builder.start_pos].push(id);
692        }
693        if builder.end_pos < self.ends_at.len() {
694            self.ends_at[builder.end_pos].push(id);
695        }
696
697        self.nodes.push(node);
698        id
699    }
700
701    /// 문자 위치에서 부분 문자열 추출
702    #[must_use]
703    pub fn substring(&self, start: usize, end: usize) -> &str {
704        let start_byte = self.char_positions.char_to_byte(start);
705        let end_byte = self.char_positions.char_to_byte(end);
706        &self.text[start_byte..end_byte]
707    }
708
709    /// 특정 위치에 띄어쓰기가 있는지 확인
710    #[inline]
711    #[must_use]
712    pub fn has_space_at(&self, char_pos: usize) -> bool {
713        self.space_positions.has_space_before(char_pos)
714    }
715
716    /// Lattice 초기화 (노드 재사용)
717    pub fn clear(&mut self) {
718        // BOS, EOS 유지하고 나머지 제거
719        self.nodes.truncate(2);
720
721        // 위치 인덱스 초기화
722        for v in &mut self.ends_at {
723            v.clear();
724        }
725        for v in &mut self.starts_at {
726            v.clear();
727        }
728
729        // BOS, EOS 재등록
730        if !self.ends_at.is_empty() {
731            self.ends_at[0].push(self.bos_id);
732        }
733        let char_len = self.char_len();
734        if char_len < self.starts_at.len() {
735            self.starts_at[char_len].push(self.eos_id);
736        }
737
738        // EOS 노드 리셋
739        if let Some(eos) = self.nodes.get_mut(self.eos_id as usize) {
740            eos.total_cost = i32::MAX;
741            eos.prev_node_id = INVALID_NODE_ID;
742        }
743    }
744
745    /// 새 텍스트로 Lattice 재설정
746    pub fn reset(&mut self, text: &str) {
747        // Reuse the String allocation for original_text/text instead of
748        // dropping and recreating.
749        self.original_text.clear();
750        self.original_text.push_str(text);
751
752        self.text.clear();
753        for c in text.chars().filter(|c| !c.is_whitespace()) {
754            self.text.push(c);
755        }
756
757        self.char_positions = CharPositions::new(&self.text);
758        self.stripped_to_original_byte = Self::build_stripped_to_original(text);
759        self.space_positions = SpacePositions::new(text);
760
761        let char_len = self.char_positions.char_count();
762        let byte_len = self.char_positions.byte_count();
763
764        // Resize the position index vectors without dropping inner Vec capacity.
765        // If new size <= old size, clear existing slots and truncate.
766        // If new size > old size, clear existing slots and push new empty vecs.
767        let new_len = char_len + 1;
768        let old_ends_len = self.ends_at.len();
769        let old_starts_len = self.starts_at.len();
770
771        // Clear the slots we will reuse (preserving their heap capacity).
772        for v in self.ends_at.iter_mut().take(new_len.min(old_ends_len)) {
773            v.clear();
774        }
775        for v in self.starts_at.iter_mut().take(new_len.min(old_starts_len)) {
776            v.clear();
777        }
778
779        // Resize (truncate or extend).
780        self.ends_at.truncate(new_len);
781        self.starts_at.truncate(new_len);
782        while self.ends_at.len() < new_len {
783            self.ends_at.push(Vec::new());
784        }
785        while self.starts_at.len() < new_len {
786            self.starts_at.push(Vec::new());
787        }
788
789        // 노드 리셋
790        self.nodes.truncate(2);
791
792        // EOS 업데이트
793        if let Some(eos) = self.nodes.get_mut(self.eos_id as usize) {
794            eos.start_pos = char_len;
795            eos.end_pos = char_len;
796            eos.start_byte = byte_len;
797            eos.end_byte = byte_len;
798            eos.total_cost = i32::MAX;
799            eos.prev_node_id = INVALID_NODE_ID;
800        }
801
802        // BOS, EOS 재등록
803        self.ends_at[0].push(self.bos_id);
804        self.starts_at[char_len].push(self.eos_id);
805    }
806
807    /// 최적 경로 추출 (Viterbi 실행 후 호출)
808    ///
809    /// EOS에서 BOS까지 역추적하여 최적 경로의 노드들을 반환합니다.
810    #[must_use]
811    pub fn best_path(&self) -> Vec<&Node> {
812        let mut path = Vec::new();
813        let mut current_id = self.eos_id;
814
815        while current_id != INVALID_NODE_ID {
816            if let Some(node) = self.nodes.get(current_id as usize) {
817                if node.node_type != NodeType::Bos && node.node_type != NodeType::Eos {
818                    path.push(node);
819                }
820                current_id = node.prev_node_id;
821            } else {
822                break;
823            }
824        }
825
826        path.reverse();
827        path
828    }
829
830    /// 디버그용: Lattice 시각화
831    #[cfg(test)]
832    #[must_use]
833    #[allow(clippy::format_push_string, clippy::uninlined_format_args)]
834    pub fn visualize(&self) -> String {
835        let mut output = String::new();
836        output.push_str(&format!("Lattice for: \"{}\"\n", self.text));
837        output.push_str(&format!("Nodes: {}\n", self.node_count()));
838
839        for pos in 0..=self.char_len() {
840            let ending: Vec<_> = self.nodes_ending_at(pos).collect();
841            if !ending.is_empty() {
842                output.push_str(&format!("\nPosition {}: ", pos));
843                for node in ending {
844                    output.push_str(&format!(
845                        "[{}: {} ({}-{})]",
846                        node.id, node.surface, node.start_pos, node.end_pos
847                    ));
848                }
849            }
850        }
851
852        output
853    }
854}
855
856/// Lattice 통계 정보
857#[derive(Debug, Clone, Default)]
858pub struct LatticeStats {
859    /// 총 노드 수
860    pub total_nodes: usize,
861    /// Known 노드 수
862    pub known_nodes: usize,
863    /// Unknown 노드 수
864    pub unknown_nodes: usize,
865    /// User 노드 수
866    pub user_nodes: usize,
867    /// 문자 길이
868    pub char_length: usize,
869}
870
871impl Lattice {
872    /// 통계 정보 계산
873    #[must_use]
874    pub fn stats(&self) -> LatticeStats {
875        let mut stats = LatticeStats {
876            total_nodes: self.nodes.len(),
877            char_length: self.char_len(),
878            ..Default::default()
879        };
880
881        for node in &self.nodes {
882            match node.node_type {
883                NodeType::Known => stats.known_nodes += 1,
884                NodeType::Unknown => stats.unknown_nodes += 1,
885                NodeType::User => stats.user_nodes += 1,
886                _ => {}
887            }
888        }
889
890        stats
891    }
892
893    /// 메모리 사용량 추정 (바이트)
894    ///
895    /// Lattice가 사용하는 대략적인 메모리 크기를 반환합니다.
896    #[must_use]
897    pub fn memory_usage(&self) -> usize {
898        // 텍스트 저장
899        let text_bytes = self.text.len() + self.original_text.len();
900
901        // 노드 배열
902        let nodes_bytes = self.nodes.capacity() * std::mem::size_of::<Node>();
903
904        // starts_at, ends_at 배열
905        let index_bytes = self.starts_at.capacity() * std::mem::size_of::<Vec<u32>>()
906            + self.ends_at.capacity() * std::mem::size_of::<Vec<u32>>()
907            + self
908                .starts_at
909                .iter()
910                .map(|v| v.capacity() * 4)
911                .sum::<usize>()
912            + self.ends_at.iter().map(|v| v.capacity() * 4).sum::<usize>();
913
914        // char_positions (estimated from char_count)
915        let pos_bytes = (self.char_positions.char_count() + 1) * std::mem::size_of::<usize>();
916
917        // space_positions (estimated from char_len)
918        let space_bytes = self.char_len() * std::mem::size_of::<usize>() / 10; // ~10% spaces
919
920        // 노드 내 문자열 추정
921        let node_strings: usize = self
922            .nodes
923            .iter()
924            .map(|n| n.surface.len() + n.feature.len())
925            .sum();
926
927        text_bytes + nodes_bytes + index_bytes + pos_bytes + space_bytes + node_strings
928    }
929}
930
931#[cfg(test)]
932#[allow(clippy::unwrap_used, clippy::needless_collect)]
933mod tests {
934    use super::*;
935
936    #[test]
937    fn test_lattice_creation() {
938        let lattice = Lattice::new("안녕하세요");
939
940        assert_eq!(lattice.text(), "안녕하세요");
941        assert_eq!(lattice.char_len(), 5);
942        assert_eq!(lattice.node_count(), 2); // BOS + EOS
943    }
944
945    #[test]
946    fn test_lattice_with_spaces() {
947        let lattice = Lattice::new("안녕 하세요");
948
949        // 공백 제거된 텍스트
950        assert_eq!(lattice.text(), "안녕하세요");
951        assert_eq!(lattice.original_text(), "안녕 하세요");
952        assert_eq!(lattice.char_len(), 5);
953
954        // 띄어쓰기 위치 확인
955        assert!(!lattice.has_space_at(0));
956        assert!(!lattice.has_space_at(1));
957        assert!(lattice.has_space_at(2)); // "하" 앞에 공백
958    }
959
960    #[test]
961    fn test_add_node() {
962        let mut lattice = Lattice::new("안녕하세요");
963
964        let node_id = lattice.add_node(
965            NodeBuilder::new("안녕", 0, 2)
966                .left_id(100)
967                .right_id(100)
968                .word_cost(1000)
969                .feature("NNG,*,F,안녕,*,*,*,*"),
970        );
971
972        assert_eq!(node_id, 2); // BOS=0, EOS=1, 새 노드=2
973        assert_eq!(lattice.node_count(), 3);
974
975        let node = lattice.node(node_id).unwrap();
976        assert_eq!(node.surface.as_ref(), "안녕");
977        assert_eq!(node.start_pos, 0);
978        assert_eq!(node.end_pos, 2);
979        assert_eq!(node.left_id, 100);
980        assert_eq!(node.word_cost, 1000);
981    }
982
983    #[test]
984    fn test_nodes_at_position() {
985        let mut lattice = Lattice::new("안녕하세요");
986
987        // "안녕" (0-2)
988        lattice.add_node(NodeBuilder::new("안녕", 0, 2));
989        // "안" (0-1)
990        lattice.add_node(NodeBuilder::new("안", 0, 1));
991        // "녕하" (1-3)
992        lattice.add_node(NodeBuilder::new("녕하", 1, 3));
993
994        // 위치 0에서 시작하는 노드들
995        let starting_at_0: Vec<_> = lattice.nodes_starting_at(0).collect();
996        assert_eq!(starting_at_0.len(), 2); // "안녕", "안"
997
998        // 위치 2에서 끝나는 노드들
999        let ending_at_2: Vec<_> = lattice.nodes_ending_at(2).collect();
1000        assert_eq!(ending_at_2.len(), 1); // "안녕"
1001    }
1002
1003    #[test]
1004    fn test_char_positions() {
1005        let positions = CharPositions::new("한글test");
1006
1007        assert_eq!(positions.char_count(), 6);
1008        assert_eq!(positions.char_to_byte(0), 0); // '한' 시작
1009        assert_eq!(positions.char_to_byte(1), 3); // '글' 시작 (한글 3바이트)
1010        assert_eq!(positions.char_to_byte(2), 6); // 't' 시작
1011        assert_eq!(positions.char_to_byte(3), 7); // 'e' 시작
1012    }
1013
1014    #[test]
1015    fn test_substring() {
1016        let lattice = Lattice::new("안녕하세요");
1017
1018        assert_eq!(lattice.substring(0, 2), "안녕");
1019        assert_eq!(lattice.substring(2, 5), "하세요");
1020        assert_eq!(lattice.substring(0, 5), "안녕하세요");
1021    }
1022
1023    #[test]
1024    fn test_bos_eos() {
1025        let lattice = Lattice::new("테스트");
1026
1027        let bos = lattice.bos();
1028        assert!(bos.is_bos());
1029        assert_eq!(bos.id, BOS_NODE_ID);
1030
1031        let eos = lattice.eos();
1032        assert!(eos.is_eos());
1033        assert_eq!(eos.start_pos, 3);
1034    }
1035
1036    #[test]
1037    fn test_lattice_reset() {
1038        let mut lattice = Lattice::new("안녕");
1039        lattice.add_node(NodeBuilder::new("안녕", 0, 2));
1040        assert_eq!(lattice.node_count(), 3);
1041
1042        lattice.reset("하세요");
1043        assert_eq!(lattice.text(), "하세요");
1044        assert_eq!(lattice.char_len(), 3);
1045        assert_eq!(lattice.node_count(), 2); // BOS + EOS만
1046    }
1047
1048    #[test]
1049    fn test_space_before_detection() {
1050        let mut lattice = Lattice::new("아버지가 방에");
1051
1052        // "방" 노드 추가 (공백 뒤)
1053        let node_id = lattice.add_node(NodeBuilder::new("방에", 4, 6));
1054
1055        let node = lattice.node(node_id).unwrap();
1056        assert!(node.has_space_before);
1057
1058        // "아버지가" 노드 추가 (공백 앞)
1059        let node_id2 = lattice.add_node(NodeBuilder::new("아버지가", 0, 4));
1060        let node2 = lattice.node(node_id2).unwrap();
1061        assert!(!node2.has_space_before);
1062    }
1063}