msime_engine/
multilingual.rs1use std::ops::Range;
4
5mod dictionaries;
6pub use dictionaries::DictionaryComposer;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
10pub enum Language {
11 Chinese,
12 English,
13 Japanese,
14}
15
16#[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#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct Segment {
28 pub input: Range<usize>,
29 pub text: String,
30 pub language: Language,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct Conversion {
36 pub segments: Vec<Segment>,
37 pub chinese_complete: bool,
38}
39
40pub const MAX_INPUT_BYTES: usize = 96;
42pub const MAX_LEXEMES: usize = 4096;
44
45pub 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 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 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 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 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;