Skip to main content

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}