afrim_translator/lib.rs
1#![deny(missing_docs)]
2//! This crate provides a range of language-related functionalities, including translation,
3//! auto-suggestions, auto-correction and more.
4//! It's designed to enhance the language processing tasks within in input method engine.
5//!
6//! **Note**: We use [`IndexMap`] instead of [`HashMap`](std::collections::HashMap) for better performance
7//! when dealing with big datasets.
8//!
9//! ### Feature flags
10//!
11//! To reduce the amount of compiled code in the crate, you can enable feature manually. This is
12//! done by adding `default-features = false` to your dependency specification. Below is a list of
13//! the features available in this crate.
14//!
15//! * `rhai`: Enables the usage of rhai script files.
16//! * `rhai-wasm`: Like rhai, but wasm compatible.
17//! * `strsim`: Enables the text similarity algorithm for better predictions.
18//! * `serde`: Enables serde feature.
19//!
20//! # Example
21//!
22//! ```
23//! use afrim_translator::{Predicate, Translator, IndexMap};
24//! #[cfg(feature = "rhai")]
25//! use afrim_translator::AST;
26//!
27//! // Prepares the dictionary.
28//! let mut dictionary = IndexMap::new();
29//! dictionary.insert("jump".to_string(), vec!["sauter".to_string()]);
30//! dictionary.insert("jumper".to_string(), vec!["sauteur".to_string()]);
31//! dictionary.insert("nihao".to_string(), vec!["hello".to_string()]);
32//!
33//! // Builds the translator.
34//! let mut translator = Translator::new(dictionary, true, 0.7);
35//!
36//! assert_eq!(
37//! translator.translate("jump"),
38//! vec![
39//! Predicate {
40//! code: "jump".to_owned(),
41//! remaining_code: "".to_owned(),
42//! texts: vec!["sauter".to_owned()],
43//! can_commit: true
44//! },
45//! // Auto-completion.
46//! Predicate {
47//! code: "jumper".to_owned(),
48//! remaining_code: "er".to_owned(),
49//! texts: vec!["sauteur".to_owned()],
50//! can_commit: false
51//! }
52//! ]
53//! );
54//! ```
55//!
56//! # Example with the strsim feature
57//!
58//! ```
59//! use afrim_translator::{Predicate, Translator, IndexMap};
60//!
61//! // Prepares the dictionary.
62//! let mut dictionary = IndexMap::new();
63//! dictionary.insert("jump".to_string(), vec!["sauter".to_string()]);
64//! dictionary.insert("jumper".to_string(), vec!["sauteur".to_string()]);
65//!
66//! // Builds the translator.
67//! let mut translator = Translator::new(dictionary, true, 0.7);
68//!
69//! // Auto-suggestion / Auto-correction.
70//! #[cfg(feature = "strsim")]
71//! assert_eq!(
72//! translator.translate("junp"),
73//! vec![Predicate {
74//! code: "jump".to_owned(),
75//! remaining_code: "".to_owned(),
76//! texts: vec!["sauter".to_owned()],
77//! can_commit: false
78//! }]
79//! );
80//! ```
81//!
82//! # Example with the rhai feature
83//!
84//! ```
85//! #[cfg(feature = "rhai")]
86//! use afrim_translator::{AST, Engine};
87//! use afrim_translator::{Predicate, Translator, IndexMap};
88//!
89//! // Prepares the dictionary.
90//! let mut dictionary = IndexMap::new();
91//! dictionary.insert("jump".to_string(), vec!["sauter".to_string()]);
92//! dictionary.insert("jumper".to_string(), vec!["sauteur".to_string()]);
93//!
94//! // Prepares the script.
95//! #[cfg(feature = "rhai")]
96//! let engine = Engine::new();
97//! #[cfg(feature = "rhai")]
98//! let jump_translator: AST = engine.compile(r#"
99//! // The main script function.
100//! fn translate(input) {
101//! if input == "jump" {
102//! [input, "", "\n", false]
103//! }
104//! }
105//! "#).unwrap();
106//!
107//! // Builds the translator.
108//! let mut translator = Translator::new(dictionary, true, 0.7);
109//!
110//! // Registers the jump translator.
111//! #[cfg(feature = "rhai")]
112//! translator.register("jump".to_string(), jump_translator);
113//!
114//! assert_eq!(
115//! translator.translate("jump"),
116//! vec![
117//! Predicate {
118//! code: "jump".to_owned(),
119//! remaining_code: "".to_owned(),
120//! texts: vec!["sauter".to_owned()],
121//! can_commit: true
122//! },
123//! #[cfg(feature = "rhai")]
124//! // Programmable translation.
125//! Predicate {
126//! code: "jump".to_owned(),
127//! remaining_code: "".to_owned(),
128//! texts: vec!["\n".to_owned()],
129//! can_commit: false
130//! },
131//! // Auto-completion.
132//! Predicate {
133//! code: "jumper".to_owned(),
134//! remaining_code: "er".to_owned(),
135//! texts: vec!["sauteur".to_owned()],
136//! can_commit: false
137//! }
138//! ]
139//! );
140//! ```
141
142pub use indexmap::IndexMap;
143#[cfg(feature = "rhai")]
144use rhai::{Array, Scope};
145#[cfg(feature = "rhai")]
146pub use rhai::{Engine, AST};
147use std::cmp::Ordering;
148#[cfg(feature = "strsim")]
149use strsim::{self};
150
151/// Struct representing the predicate.
152#[derive(Clone, Debug, Default, Eq, PartialEq)]
153#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
154pub struct Predicate {
155 /// The predicate code.
156 pub code: String,
157 /// The remaining code to match the predicate.
158 pub remaining_code: String,
159 /// The resulting predicate possible outputs.
160 pub texts: Vec<String>,
161 /// Whether the predicate can be commit.
162 pub can_commit: bool,
163}
164
165/// Core structure of the translator.
166pub struct Translator {
167 dictionary: IndexMap<String, Vec<String>>,
168 #[cfg(feature = "rhai")]
169 translators: IndexMap<String, AST>,
170 #[cfg(feature = "rhai")]
171 engine: Engine,
172 auto_commit: bool,
173 min_confidence: f64,
174}
175
176impl Translator {
177 /// Initiatializes a new translator.
178 ///
179 /// # Example
180 ///
181 /// ```
182 /// use afrim_translator::{Translator, IndexMap};
183 ///
184 /// let dictionary = IndexMap::new();
185 /// let translator = Translator::new(dictionary, false, 0.7);
186 /// ```
187 pub fn new(
188 dictionary: IndexMap<String, Vec<String>>,
189 auto_commit: bool,
190 min_confidence: f64,
191 ) -> Self {
192 Self {
193 dictionary,
194 auto_commit,
195 #[cfg(feature = "rhai")]
196 translators: IndexMap::default(),
197 #[cfg(feature = "rhai")]
198 engine: Engine::new(),
199 min_confidence,
200 }
201 }
202
203 #[cfg(feature = "rhai")]
204 /// Registers a translator.
205 ///
206 /// The provided name will be used for debugging in case of script error.
207 /// Note that the scripts are compiled using [`Engine`](crate::Engine::compile).
208 ///
209 /// # Example
210 ///
211 /// ```
212 /// use afrim_translator::{Engine, Predicate, Translator, IndexMap};
213 ///
214 /// // We prepare the script.
215 /// let date_translator = r#"
216 /// // Date converter.
217 ///
218 /// const MONTHS = [
219 /// "Jan", "Feb", "Mar", "Apr", "May", "Jun",
220 /// "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
221 /// ];
222 ///
223 /// fn parse_date(input) {
224 /// let data = input.split('/');
225 ///
226 /// if data.len() != 3 {
227 /// return [];
228 /// }
229 ///
230 /// let day = parse_int(data[0]);
231 /// let month = parse_int(data[1]);
232 /// let year = parse_int(data[2]);
233 ///
234 /// if day in 1..31 && month in 1..13 && year in 1..2100 {
235 /// return [day, month, year];
236 /// }
237 /// }
238 ///
239 /// // Script main function.
240 /// fn translate(input) {
241 /// let date = parse_date(input);
242 ///
243 /// if date.is_empty() { return }
244 ///
245 /// let month = global::MONTHS[date[1]-1];
246 ///
247 /// [input, "", [`${date[0]}, ${month} ${date[2]}`], true]
248 /// }
249 /// "#;
250 /// let mut engine = Engine::new();
251 /// let date_translator = engine.compile(date_translator).unwrap();
252 ///
253 /// // We build the translator.
254 /// let mut translator = Translator::new(IndexMap::new(), true, 0.7);
255 ///
256 /// // We register our date translator.
257 /// translator.register("date_translator".to_owned(), date_translator);
258 ///
259 /// assert_eq!(
260 /// translator.translate("09/02/2024"),
261 /// vec![
262 /// Predicate {
263 /// code: "09/02/2024".to_owned(),
264 /// remaining_code: "".to_owned(),
265 /// texts: vec!["9, Feb 2024".to_owned()],
266 /// can_commit: true
267 /// }
268 /// ]
269 /// );
270 /// ```
271 pub fn register(&mut self, name: String, ast: AST) {
272 self.translators.insert(name, ast);
273 }
274
275 #[cfg(feature = "rhai")]
276 /// Unregisters a translator.
277 ///
278 /// # Example
279 /// ```
280 /// use afrim_translator::{Engine, Predicate, Translator, IndexMap};
281 ///
282 /// // We prepare the script.
283 /// let engine = Engine::new();
284 /// let erase_translator = engine.compile("fn translate(input) { [input, \"\", [], true] }").unwrap();
285 ///
286 /// // We build the translator.
287 /// let mut translator = Translator::new(IndexMap::new(), false, 0.7);
288 ///
289 /// // We register the erase translator.
290 /// translator.register("erase".to_owned(), erase_translator);
291 /// assert_eq!(
292 /// translator.translate("hello"),
293 /// vec![
294 /// Predicate {
295 /// code: "hello".to_owned(),
296 /// remaining_code: "".to_owned(),
297 /// texts: vec![],
298 /// can_commit: true
299 /// }
300 /// ]
301 /// );
302 ///
303 /// // We unregister the erase translator.
304 /// translator.unregister("erase");
305 /// assert_eq!(translator.translate("hello"), vec![]);
306 /// ```
307 pub fn unregister(&mut self, name: &str) {
308 self.translators.shift_remove(name);
309 }
310
311 /// Generates a list of predicates based on the input.
312 ///
313 /// # Example
314 ///
315 /// ```
316 /// use afrim_translator::{IndexMap, Predicate, Translator};
317 ///
318 /// // We prepares the dictionary.
319 /// let mut dictionary = IndexMap::new();
320 /// dictionary.insert("salut!".to_owned(), vec!["hello!".to_owned(), "hi!".to_owned()]);
321 /// dictionary.insert("salade".to_owned(), vec!["vegetable".to_owned()]);
322 ///
323 /// // We build the translator.
324 /// let translator = Translator::new(dictionary, false, 0.7);
325 /// assert_eq!(
326 /// translator.translate("sal"),
327 /// vec![
328 /// Predicate {
329 /// code: "salut!".to_owned(),
330 /// remaining_code: "ut!".to_owned(),
331 /// texts: vec!["hello!".to_owned(), "hi!".to_owned()],
332 /// can_commit: false
333 /// },
334 /// Predicate {
335 /// code: "salade".to_owned(),
336 /// remaining_code: "ade".to_owned(),
337 /// texts: vec!["vegetable".to_owned()],
338 /// can_commit: false
339 /// }
340 /// ]
341 /// )
342 /// ```
343 pub fn translate(&self, input: &str) -> Vec<Predicate> {
344 // Short-circuit after the 2nd char rather than scanning the whole string
345 // with .chars().count() -> O(1) vs O(n).
346 {
347 let mut ch = input.chars();
348 if ch.next().is_none() || ch.next().is_none() {
349 return vec![];
350 }
351 }
352
353 // Cache both values once; they are reused on every loop iteration.
354 let input_len = input.len();
355 // We confirmed at least 2 chars exist above.
356 let input_first_char = input.chars().next().unwrap();
357
358 #[cfg(feature = "rhai")]
359 let mut scope = Scope::new();
360
361 let predicates = self.dictionary.iter().filter_map(|(key, values)| {
362 let key_len = key.len();
363
364 if input_len > key_len || !key.starts_with(input_first_char) {
365 return None;
366 }
367
368 // 1. Exact match
369 if key == input {
370 return Some((
371 1.0_f64,
372 Predicate {
373 code: key.to_owned(),
374 remaining_code: String::new(),
375 texts: values.to_owned(),
376 can_commit: self.auto_commit,
377 },
378 ));
379 }
380
381 // 2. Fuzzy correction via strsim (same byte-length)
382 #[cfg(feature = "strsim")]
383 if key_len == input_len {
384 let confidence = strsim::hamming(key.as_ref(), input)
385 .map(|n| 1.0 - (n as f64 / key_len as f64))
386 .unwrap_or(0.0);
387
388 if confidence > self.min_confidence {
389 return Some((
390 confidence,
391 Predicate {
392 code: key.to_owned(),
393 remaining_code: String::new(),
394 texts: values.to_owned(),
395 can_commit: false,
396 },
397 ));
398 }
399 }
400
401 // 3. Prefix completion
402 if key.starts_with(input) {
403 return Some((
404 0.5,
405 Predicate {
406 code: key.to_owned(),
407 // `starts_with` guarantees `input_len` is a valid UTF-8 char
408 // boundary in `key`, so the byte slice is correct and O(1).
409 remaining_code: key[input_len..].to_owned(),
410 texts: values.to_owned(),
411 can_commit: false,
412 },
413 ));
414 }
415
416 None
417 });
418
419 #[cfg(feature = "rhai")]
420 let predicates =
421 predicates.chain(self.translators.iter().filter_map(|(_name, translator)| {
422 let mut data = self
423 .engine
424 .call_fn::<Array>(&mut scope, translator, "translate", (input.to_owned(),))
425 .unwrap_or_default();
426
427 (data.len() == 4).then(|| {
428 let code = data.remove(0).into_string().unwrap();
429 let remaining_code = data.remove(0).into_string().unwrap();
430 let value = data.remove(0);
431 let values = if value.is_array() {
432 value.into_array().unwrap()
433 } else {
434 vec![value]
435 };
436 let values = values
437 .into_iter()
438 .map(|e| e.into_string().unwrap())
439 .collect();
440 let translated = data.remove(0).as_bool().unwrap();
441
442 (
443 1.0_f64,
444 Predicate {
445 code,
446 remaining_code,
447 texts: values,
448 can_commit: translated,
449 },
450 )
451 })
452 }));
453
454 let mut predicates: Vec<(f64, Predicate)> = predicates.collect();
455
456 // `sort_unstable_by` avoids the auxiliary allocation that the stable
457 // sort needs for its merge passes; equal-confidence predicates may be
458 // reordered, but their relative priority is identical so it's safe.
459 predicates.sort_unstable_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal));
460
461 predicates
462 .into_iter()
463 .map(|(_, predicate)| predicate)
464 .collect()
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 #[test]
471 fn test_translate() {
472 #[cfg(feature = "rhai")]
473 use crate::Engine;
474 use crate::{IndexMap, Predicate, Translator};
475
476 // We build the translation
477 let mut dictionary = IndexMap::new();
478 dictionary.insert("halo".to_string(), ["hello".to_string()].to_vec());
479
480 // We config the translator
481 #[cfg(not(feature = "rhai"))]
482 let translator = Translator::new(dictionary, true, 0.7);
483 #[cfg(feature = "rhai")]
484 let mut translator = Translator::new(dictionary, true, 0.7);
485
486 // Test the filtering
487 translator.translate("รน");
488 //
489 #[cfg(feature = "rhai")]
490 {
491 let engine = Engine::new();
492 let ast1 = engine.compile("fn translate(input) {}").unwrap();
493 let ast2 = engine
494 .compile(
495 r#"
496 fn translate(input) {
497 if input == "hi" {
498 ["hi", "", "hello", true]
499 }
500 }
501 "#,
502 )
503 .unwrap();
504 translator.register("none".to_string(), ast1);
505 translator.unregister("none");
506 translator.register("some".to_string(), ast2);
507 }
508
509 assert_eq!(translator.translate("h"), vec![]);
510 #[cfg(feature = "rhai")]
511 assert_eq!(
512 translator.translate("hi"),
513 vec![Predicate {
514 code: "hi".to_owned(),
515 remaining_code: "".to_owned(),
516 texts: vec!["hello".to_owned()],
517 can_commit: true
518 }]
519 );
520 assert_eq!(
521 translator.translate("ha"),
522 vec![Predicate {
523 code: "halo".to_owned(),
524 remaining_code: "lo".to_owned(),
525 texts: vec!["hello".to_owned()],
526 can_commit: false
527 }]
528 );
529 #[cfg(feature = "strsim")]
530 assert_eq!(
531 translator.translate("helo"),
532 vec![Predicate {
533 code: "halo".to_owned(),
534 remaining_code: "".to_owned(),
535 texts: vec!["hello".to_owned()],
536 can_commit: false
537 }]
538 );
539 }
540}