Skip to main content

sudachi/analysis/
node.rs

1/*
2 *  Copyright (c) 2021-2026 Works Applications Co., Ltd.
3 *
4 *  Licensed under the Apache License, Version 2.0 (the "License");
5 *  you may not use this file except in compliance with the License.
6 *  You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 *   Unless required by applicable law or agreed to in writing, software
11 *  distributed under the License is distributed on an "AS IS" BASIS,
12 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 *  See the License for the specific language governing permissions and
14 *  limitations under the License.
15 */
16
17use std::fmt;
18use std::iter::FusedIterator;
19use std::ops::Range;
20
21use crate::analysis::inner::Node;
22use crate::analysis::lattice::Lattice;
23use crate::dic::lexicon_set::LexiconSet;
24use crate::dic::subset::InfoSubset;
25use crate::dic::word_id::WordId;
26use crate::dic::word_info::{WordInfo, WordInfoResolver};
27use crate::input_text::InputBuffer;
28use crate::prelude::*;
29
30/// Accessor trait for right connection id
31pub trait RightId {
32    fn right_id(&self) -> u16;
33}
34
35/// Accessor trait for the full path cost
36pub trait PathCost {
37    fn total_cost(&self) -> i32;
38
39    #[inline]
40    fn is_connected_to_bos(&self) -> bool {
41        self.total_cost() != i32::MAX
42    }
43}
44
45pub trait LatticeNode: RightId {
46    fn begin(&self) -> usize;
47    fn end(&self) -> usize;
48    fn cost(&self) -> i16;
49    fn word_id(&self) -> WordId;
50    fn left_id(&self) -> u16;
51
52    /// Is true when the word does not come from the dictionary.
53    /// BOS and EOS are also treated as OOV.
54    #[inline]
55    fn is_oov(&self) -> bool {
56        self.word_id().is_oov()
57    }
58
59    /// If a node is a special system node like BOS or EOS.
60    /// Java name isSystem (which is similar to a regular node coming from the system dictionary)
61    #[inline]
62    fn is_special_node(&self) -> bool {
63        self.word_id().is_special()
64    }
65
66    /// Returns number of codepoints in the current node
67    #[inline]
68    fn num_codepts(&self) -> usize {
69        self.end() - self.begin()
70    }
71
72    /// Utility method for extracting [begin, end) codepoint range.
73    #[inline]
74    fn char_range(&self) -> Range<usize> {
75        self.begin()..self.end()
76    }
77}
78
79#[derive(Clone)]
80/// Full lattice node, as the result of analysis.
81/// All indices (including inner) are in the modified sentence space
82/// Indices are converted to original sentence space when user request them.
83pub struct ResultNode {
84    inner: Node,
85    total_cost: i32,
86    begin_bytes: u16,
87    end_bytes: u16,
88
89    word_info: WordInfo,
90}
91
92impl ResultNode {
93    pub fn new(
94        inner: Node,
95        total_cost: i32,
96        begin_bytes: u16,
97        end_bytes: u16,
98        word_info: WordInfo,
99    ) -> ResultNode {
100        ResultNode {
101            inner,
102            total_cost,
103            begin_bytes,
104            end_bytes,
105            word_info,
106        }
107    }
108}
109
110impl RightId for ResultNode {
111    fn right_id(&self) -> u16 {
112        self.inner.right_id()
113    }
114}
115
116impl PathCost for ResultNode {
117    fn total_cost(&self) -> i32 {
118        self.total_cost
119    }
120}
121
122impl LatticeNode for ResultNode {
123    fn begin(&self) -> usize {
124        self.inner.begin()
125    }
126
127    fn end(&self) -> usize {
128        self.inner.end()
129    }
130
131    fn cost(&self) -> i16 {
132        self.inner.cost()
133    }
134
135    fn word_id(&self) -> WordId {
136        self.inner.word_id()
137    }
138
139    fn left_id(&self) -> u16 {
140        self.inner.left_id()
141    }
142}
143
144impl ResultNode {
145    pub fn word_info(&self) -> &WordInfo {
146        &self.word_info
147    }
148
149    /// Returns begin offset in bytes of node surface in a sentence
150    pub fn begin_bytes(&self) -> usize {
151        self.begin_bytes as usize
152    }
153
154    /// Returns end offset in bytes of node surface in a sentence
155    pub fn end_bytes(&self) -> usize {
156        self.end_bytes as usize
157    }
158
159    /// Returns range in bytes (for easy string slicing)
160    pub fn bytes_range(&self) -> Range<usize> {
161        self.begin_bytes()..self.end_bytes()
162    }
163
164    pub fn set_bytes_range(&mut self, begin: u16, end: u16) {
165        self.begin_bytes = begin;
166        self.end_bytes = end;
167    }
168
169    pub fn set_char_range(&mut self, begin: u16, end: u16) {
170        self.inner.set_range(begin, end)
171    }
172
173    /// Returns number of splits in a specified mode
174    pub fn num_splits(&self, mode: Mode) -> usize {
175        match mode {
176            Mode::A => self.word_info.a_unit_split().len(),
177            Mode::B => self.word_info.b_unit_split().len(),
178            Mode::C => 0,
179        }
180    }
181
182    /// Split the node with a specified mode using the dictionary data
183    pub fn split<'a>(
184        &'a self,
185        mode: Mode,
186        lexicon_set: &'a LexiconSet<'a>,
187        subset: InfoSubset,
188        text: &'a InputBuffer,
189    ) -> NodeSplitIterator<'a> {
190        let splits: &[WordId] = match mode {
191            Mode::A => self.word_info.a_unit_split(),
192            Mode::B => self.word_info.b_unit_split(),
193            Mode::C => panic!("splitting Node with Mode::C is not supported"),
194        };
195
196        NodeSplitIterator {
197            splits,
198            index: 0,
199            lexicon_set,
200            subset,
201            text,
202            byte_offset: self.begin_bytes,
203            byte_end: self.end_bytes,
204            char_offset: self.begin() as u16,
205            char_end: self.end() as u16,
206        }
207    }
208}
209
210impl fmt::Display for ResultNode {
211    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
212        write!(
213            f,
214            "{} {} {} {} {} {} {}",
215            self.begin(),
216            self.end(),
217            self.word_id(),
218            self.word_info().pos_id(),
219            self.left_id(),
220            self.right_id(),
221            self.cost()
222        )
223    }
224}
225
226pub struct NodeSplitIterator<'a> {
227    splits: &'a [WordId],
228    lexicon_set: &'a LexiconSet<'a>,
229    index: usize,
230    subset: InfoSubset,
231    text: &'a InputBuffer,
232    char_offset: u16,
233    byte_offset: u16,
234    char_end: u16,
235    byte_end: u16,
236}
237
238impl<'a> Iterator for NodeSplitIterator<'a> {
239    type Item = ResultNode;
240
241    #[inline]
242    fn next(&mut self) -> Option<Self::Item> {
243        let idx = self.index;
244        if idx >= self.splits.len() {
245            return None;
246        }
247
248        let char_start = self.char_offset;
249        let byte_start = self.byte_offset;
250
251        let word_id = self.splits[idx];
252        // data comes from dictionary, panicking here is OK
253        let word_info = self
254            .lexicon_set
255            .get_word_info_subset(word_id, self.subset)
256            .unwrap();
257
258        let (char_end, byte_end) = if idx + 1 == self.splits.len() {
259            (self.char_end, self.byte_end)
260        } else {
261            let byte_end = byte_start as usize + word_info.index_form_length();
262            let char_end = self.text.ch_idx(byte_end);
263            (char_end as u16, byte_end as u16)
264        };
265
266        self.char_offset = char_end;
267        self.byte_offset = byte_end;
268
269        let inner = Node::new(char_start, char_end, u16::MAX, u16::MAX, i16::MAX, word_id);
270
271        let node = ResultNode::new(inner, i32::MAX, byte_start, byte_end, word_info);
272
273        self.index += 1;
274        Some(node)
275    }
276
277    #[inline]
278    fn size_hint(&self) -> (usize, Option<usize>) {
279        (self.splits.len(), Some(self.splits.len()))
280    }
281}
282
283impl FusedIterator for NodeSplitIterator<'_> {}
284
285/// Concatenate the nodes in the range and replace normalized_form if given.
286pub fn concat_nodes(
287    mut path: Vec<ResultNode>,
288    begin: usize,
289    end: usize,
290    normalized_form: Option<String>,
291    resolver: &dyn WordInfoResolver,
292) -> SudachiResult<Vec<ResultNode>> {
293    if begin >= end {
294        return Err(SudachiError::InvalidRange(begin, end));
295    }
296
297    let end_bytes = path[end - 1].end_bytes();
298    let beg_bytes = path[begin].begin_bytes();
299
300    let mut headword = String::with_capacity(end_bytes - beg_bytes);
301    let mut reading_form = String::with_capacity(end_bytes - beg_bytes);
302    let mut dictionary_form = String::with_capacity(end_bytes - beg_bytes);
303    let mut index_form_length = 0;
304
305    for node in path[begin..end].iter() {
306        headword.push_str(node.word_info().headword(resolver));
307        reading_form.push_str(node.word_info().reading_form(resolver));
308        dictionary_form.push_str(node.word_info().dictionary_form(resolver));
309        index_form_length += node.word_info().index_form_length();
310    }
311
312    let normalized_form = normalized_form.unwrap_or_else(|| {
313        let mut norm = String::with_capacity(end_bytes - beg_bytes);
314        for node in path[begin..end].iter() {
315            norm.push_str(node.word_info().normalized_form(resolver));
316        }
317        norm
318    });
319
320    let pos_id = path[begin].word_info().pos_id() as i16;
321
322    let wid = WordId::oov(pos_id as u32);
323    let new_wi = WordInfo::new_with_strings(
324        pos_id,
325        index_form_length as i16,
326        wid,
327        headword,
328        reading_form,
329        normalized_form,
330        dictionary_form,
331    );
332
333    let inner = Node::new(
334        path[begin].begin() as u16,
335        path[end - 1].end() as u16,
336        u16::MAX,
337        u16::MAX,
338        i16::MAX,
339        wid,
340    );
341
342    let node = ResultNode::new(
343        inner,
344        path[end - 1].total_cost,
345        path[begin].begin_bytes,
346        path[end - 1].end_bytes,
347        new_wi,
348    );
349
350    path[begin] = node;
351    path.drain(begin + 1..end);
352    Ok(path)
353}
354
355/// Concatenate the nodes in the range and set pos_id.
356pub fn concat_oov_nodes(
357    mut path: Vec<ResultNode>,
358    begin: usize,
359    end: usize,
360    pos_id: u16,
361    text: &InputBuffer,
362    lattice: &Lattice,
363    resolver: &dyn WordInfoResolver,
364) -> SudachiResult<Vec<ResultNode>> {
365    if begin >= end {
366        return Err(SudachiError::InvalidRange(begin, end));
367    }
368
369    let byte_begin = path[begin].begin_bytes;
370    let byte_end = path[end - 1].end_bytes;
371    let node_begin = path[begin].begin();
372    let node_end = path[end - 1].end();
373
374    // Use node in the path if exists.
375    if let Some((existing_node, cost)) = lattice.get_minimum_node(node_begin, node_end) {
376        if !existing_node.is_special_node() {
377            let word_info = if existing_node.is_oov() {
378                let surface = text
379                    .curr_slice_c(existing_node.begin()..existing_node.end())
380                    .to_owned();
381                WordInfo::new_oov(
382                    existing_node.word_id().entry().as_raw() as u16,
383                    surface.len() as i16,
384                    existing_node.word_id(),
385                    surface,
386                )
387            } else {
388                resolver
389                    .lexicon()
390                    .get_word_info_subset(existing_node.word_id(), InfoSubset::all())?
391            };
392            let node =
393                ResultNode::new(existing_node.clone(), cost, byte_begin, byte_end, word_info);
394            path[begin] = node;
395            path.drain(begin + 1..end);
396            return Ok(path);
397        }
398    }
399
400    // concat nodes in the range to compose new oov node
401    let capa = path[end - 1].end_bytes() - path[begin].begin_bytes();
402
403    let mut headword = String::with_capacity(capa);
404    let mut index_form_length = 0;
405    for node in path[begin..end].iter() {
406        headword.push_str(node.word_info().headword(resolver));
407        index_form_length += node.word_info().index_form_length();
408    }
409
410    // Synthetic concatenation in Java's concatenateOov() is always marked as OOV
411    // when we are not reusing an existing lattice node.
412    let wid = WordId::oov(pos_id as u32);
413
414    let new_wi = WordInfo::new_oov(pos_id, index_form_length as i16, wid, headword);
415
416    let inner = Node::new(
417        path[begin].begin() as u16,
418        path[end - 1].end() as u16,
419        u16::MAX,
420        u16::MAX,
421        i16::MAX,
422        wid,
423    );
424
425    let node = ResultNode::new(
426        inner,
427        path[end - 1].total_cost,
428        byte_begin,
429        byte_end,
430        new_wi,
431    );
432
433    path[begin] = node;
434    path.drain(begin + 1..end);
435    Ok(path)
436}