Skip to main content

msime_engine/
multilingual.rs

1//! 按原始键位置连接已确认的中英日词条。这里只选择完整路径,不查询词库或推断未知词。
2
3use std::ops::Range;
4
5mod dictionaries;
6pub use dictionaries::DictionaryComposer;
7
8/// 词条所属的语言;不等同于键盘布局或输入方案。
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
10pub enum Language {
11    Chinese,
12    English,
13    Japanese,
14}
15
16/// 提供者已确认的词条。范围按原始输入的 UTF-8 字节计数,名次仅来自同读音候选。
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Lexeme {
19    pub input: Range<usize>,
20    pub text: String,
21    pub language: Language,
22    pub rank: u16,
23}
24
25/// 完整路径里相邻同语言词条合成的片段,供后续转换与上屏保留语言边界。
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct Segment {
28    pub input: Range<usize>,
29    pub text: String,
30    pub language: Language,
31}
32
33/// 混合转换及中文歧义证据。中文路径完整时,调用方必须把中文留在混合候选之前。
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct Conversion {
36    pub segments: Vec<Segment>,
37    pub chinese_complete: bool,
38}
39
40/// 每次解码的原始输入字节上限;超出后调用方保留原候选。
41pub const MAX_INPUT_BYTES: usize = 96;
42/// 每次解码的词条数上限,包含无效词条,防止无效数据绕过预算。
43pub const MAX_LEXEMES: usize = 4096;
44
45/// 选择完整混合路径。词条数、切换数、同读音名次依次更少者优先;完整单语言
46/// 路径的词条更少时不拼凑混合候选,同数时保留混合备选。未选语言、无效范围和空文字不参与搜索。
47///
48/// 返回的片段覆盖全部输入并至少涉及两种语言。没有路径或超出预算时返回 `None`,
49/// 不提供部分转换,也不把缺失的尾部当成原文拼接。
50pub fn decode(input: &str, lexemes: &[Lexeme], enabled: &[Language]) -> Option<Conversion> {
51    if input.is_empty() || input.len() > MAX_INPUT_BYTES || lexemes.len() > MAX_LEXEMES {
52        return None;
53    }
54    let mut outgoing = vec![Vec::new(); input.len() + 1];
55    for (index, lexeme) in lexemes.iter().enumerate() {
56        if lexeme.input.start < lexeme.input.end
57            && lexeme.input.end <= input.len()
58            && input.is_char_boundary(lexeme.input.start)
59            && input.is_char_boundary(lexeme.input.end)
60            && !lexeme.text.is_empty()
61            && enabled.contains(&lexeme.language)
62        {
63            outgoing[lexeme.input.start].push(index);
64        }
65    }
66
67    // 语言集合有八种,每个集合再保留三种末尾语言;空路径占用第零格。
68    let mut states: Vec<[Option<Path>; 24]> = vec![std::array::from_fn(|_| None); input.len() + 1];
69    states[0][0] = Some(Path::default());
70    for position in 0..input.len() {
71        for slot in 0..24 {
72            let Some(previous) = states[position][slot].clone() else {
73                continue;
74            };
75            let used = slot / 3;
76            let last = slot % 3;
77            for &index in &outgoing[position] {
78                let lexeme = &lexemes[index];
79                let language = lexeme.language as usize;
80                let target_slot = (used | (1 << language)) * 3 + language;
81                let switches = previous.switches + usize::from(used != 0 && last != language);
82                let rank = previous.rank + usize::from(lexeme.rank);
83                let target = &mut states[lexeme.input.end][target_slot];
84                // 成本已经更差时不复制整条前缀,同成本仍按原有稳定序比较。
85                if target.as_ref().is_some_and(|old| {
86                    (previous.lexemes.len() + 1, switches, rank)
87                        > (old.lexemes.len(), old.switches, old.rank)
88                }) {
89                    continue;
90                }
91                let mut path = previous.clone();
92                path.lexemes.push(index);
93                path.switches = switches;
94                path.rank = rank;
95                if target
96                    .as_ref()
97                    .is_none_or(|old| path.precedes(old, lexemes))
98                {
99                    *target = Some(path);
100                }
101            }
102        }
103    }
104
105    let mut best: Option<&Path> = None;
106    for (slot, path) in states[input.len()].iter().enumerate() {
107        if (slot / 3).count_ones() < 2 {
108            continue;
109        }
110        if let Some(path) = path {
111            if best.is_none_or(|old| path.precedes(old, lexemes)) {
112                best = Some(path);
113            }
114        }
115    }
116    let best = best?;
117    // 完整单语言词条能用更少片段回答输入时,不强行把它拆成跨语言碎片。
118    if states[input.len()].iter().enumerate().any(|(slot, path)| {
119        (slot / 3).count_ones() == 1
120            && path
121                .as_ref()
122                .is_some_and(|single| single.lexemes.len() < best.lexemes.len())
123    }) {
124        return None;
125    }
126    let chinese_complete = states[input.len()]
127        .iter()
128        .enumerate()
129        .any(|(slot, path)| slot / 3 == (1 << Language::Chinese as usize) && path.is_some());
130    let mut segments: Vec<Segment> = Vec::new();
131    for &index in &best.lexemes {
132        let lexeme = &lexemes[index];
133        if let Some(last) = segments
134            .last_mut()
135            .filter(|segment| segment.language == lexeme.language)
136        {
137            last.input.end = lexeme.input.end;
138            last.text.push_str(&lexeme.text);
139        } else {
140            segments.push(Segment {
141                input: lexeme.input.clone(),
142                text: lexeme.text.clone(),
143                language: lexeme.language,
144            });
145        }
146    }
147    Some(Conversion {
148        segments,
149        chinese_complete,
150    })
151}
152
153#[derive(Clone, Default)]
154struct Path {
155    lexemes: Vec<usize>,
156    switches: usize,
157    rank: usize,
158}
159
160impl Path {
161    fn precedes(&self, other: &Self, lexemes: &[Lexeme]) -> bool {
162        let cost = |path: &Self| (path.lexemes.len(), path.switches, path.rank);
163        let order = cost(self).cmp(&cost(other));
164        if !order.is_eq() {
165            return order.is_lt();
166        }
167        // 词库枚举顺序不能改变同分路径,固定按语言、词面与范围比较。
168        let key = |index: &usize| {
169            let lexeme = &lexemes[*index];
170            (
171                lexeme.language,
172                lexeme.text.as_str(),
173                lexeme.input.start,
174                lexeme.input.end,
175            )
176        };
177        self.lexemes
178            .iter()
179            .map(key)
180            .cmp(other.lexemes.iter().map(key))
181            .is_lt()
182    }
183}
184
185#[cfg(test)]
186#[path = "multilingual/tests.rs"]
187mod tests;
188
189#[cfg(test)]
190#[path = "multilingual/reference_tests.rs"]
191mod reference_tests;