text2num/lang/es/
mod.rs

1//! Spanish number interpreter
2use crate::digit_string::DigitString;
3use crate::error::Error;
4
5mod vocabulary;
6
7use super::{LangInterpreter, MorphologicalMarker};
8use vocabulary::INSIGNIFICANT;
9
10fn lemmatize(word: &str) -> &str {
11    // brute, blind removal of 's' ending is enough here
12    if word.ends_with("os") && word != "dos" || word.ends_with("as") {
13        word.trim_end_matches('s')
14    } else if word.ends_with("es") && word != "tres" {
15        word.trim_end_matches("es")
16    } else {
17        word
18    }
19}
20
21#[derive(Default)]
22pub struct Spanish {}
23
24impl Spanish {
25    pub fn new() -> Self {
26        Default::default()
27    }
28}
29
30impl LangInterpreter for Spanish {
31    fn apply(&self, num_func: &str, b: &mut DigitString) -> Result<(), Error> {
32        let num_marker = self.get_morph_marker(num_func);
33        if !b.is_empty() && num_marker != b.marker && !num_marker.is_fraction() {
34            return Err(Error::Overlap);
35        }
36        let status = match lemmatize(num_func) {
37            "cero" => b.put(b"0"),
38            "un" | "uno" | "una" if b.peek(2) != b"10" && b.peek(2) != b"20" => b.put(b"1"),
39            "primer" | "primero" | "primera" => b.put(b"1"),
40            "dos" if b.peek(2) != b"10" && b.peek(2) != b"20" => b.put(b"2"),
41            "segundo" if b.marker.is_ordinal() => b.put(b"2"),
42            "segunda" => b.put(b"2"),
43            "tres" if b.peek(2) != b"10" && b.peek(2) != b"20" => b.put(b"3"),
44            "tercer" | "tercero" | "tercera" => b.put(b"3"),
45            "cuatro" if b.peek(2) != b"10" && b.peek(2) != b"20" => b.put(b"4"),
46            "cuarto" | "cuarta" => b.put(b"4"),
47            "cinco" if b.peek(2) != b"10" && b.peek(2) != b"20" => b.put(b"5"),
48            "quinto" | "quinta" => b.put(b"5"),
49            "seis" if b.peek(2) != b"10" && b.peek(2) != b"20" => b.put(b"6"),
50            "sexto" | "sexta" => b.put(b"6"),
51            "siete" if b.peek(2) != b"10" && b.peek(2) != b"20" => b.put(b"7"),
52            "séptimo" | "séptima" => b.put(b"7"),
53            "ocho" if b.peek(2) != b"10" && b.peek(2) != b"20" => b.put(b"8"),
54            "octavo" | "octava" => b.put(b"8"),
55            "nueve" if b.peek(2) != b"10" && b.peek(2) != b"20" => b.put(b"9"),
56            "noveno" | "novena" => b.put(b"9"),
57            "diez" | "décimo" | "décima" => b.put(b"10"),
58            "once" | "undécimo" | "undécima" | "decimoprimero" | "decimoprimera" | "onceavo" => {
59                b.put(b"11")
60            }
61            "doce" | "duodécimo" | "duodécima" | "decimosegundo" | "decimosegunda" | "doceavo" => {
62                b.put(b"12")
63            }
64            "trece" | "decimotercero" | "decimotercera" | "treceavo" => b.put(b"13"),
65            "catorce" | "decimocuarto" | "decimocuarta" | "catorceavo" => b.put(b"14"),
66            "quince" | "decimoquinto" | "decimoquinta" | "quinceavo" => b.put(b"15"),
67            "dieciseis" | "dieciséis" | "decimosexto" | "decimosexta" | "deciseisavo" => {
68                b.put(b"16")
69            }
70            "diecisiete" | "decimoséptimo" | "decimoséptima" | "diecisieteavo" => b.put(b"17"),
71            "dieciocho" | "decimoctavo" | "decimoctava" | "dieciochoavo" => b.put(b"18"),
72            "diecinueve" | "decimonoveno" | "decimonovena" | "decinueveavo" => b.put(b"19"),
73            "veinte" | "vigésimo" | "vigésima" | "veintavo" | "veinteavo" => b.put(b"20"),
74            "veintiuno" | "veintiuna" | "veintiunoavo" => b.put(b"21"),
75            "veintidós" | "veintidos" | "veintidosavo" => b.put(b"22"),
76            "veintitrés" | "veintitres" | "veintitresavo" => b.put(b"23"),
77            "veinticuatro" | "veinticuatroavo" => b.put(b"24"),
78            "veinticinco" | "veinticincoavo" => b.put(b"25"),
79            "veintiseis" | "veintiséis" | "veintiseisavo" => b.put(b"26"),
80            "veintisiete" | "veintisieteavo" => b.put(b"27"),
81            "veintiocho" | "veintiochoavo" => b.put(b"28"),
82            "veintinueve" | "veintinueveavo" => b.put(b"29"),
83            "treinta" | "trigésimo" | "trigésima" | "treintavo" => b.put(b"30"),
84            "cuarenta" | "cuadragésimo" | "cuadragésima" | "cuarentavo" => b.put(b"40"),
85            "cincuenta" | "quincuagésimo" | "quincuagésima" | "cincuentavo" => b.put(b"50"),
86            "sesenta" | "sexagésimo" | "sexagésima" | "sesentavo" => b.put(b"60"),
87            "setenta" | "septuagésimo" | "septuagésima" | "setentavo" => b.put(b"70"),
88            "ochenta" | "octogésimo" | "octogésima" | "ochentavo" => b.put(b"80"),
89            "noventa" | "nonagésimo" | "nonagésima" | "noventavo" => b.put(b"90"),
90            "cien" | "ciento" | "centésimo" | "centésima" | "centavo" => b.put(b"100"),
91            "dosciento" | "doscienta" | "ducentésimo" | "ducentésima" => b.put(b"200"),
92            "tresciento" | "trescienta" | "tricentésimo" | "tricentésima" => b.put(b"300"),
93            "cuatrociento" | "cuatrocienta" | "quadringentésimo" | "quadringentésima" => {
94                b.put(b"400")
95            }
96            "quiniento" | "quinienta" | "quingentésimo" | "quingentésima" => b.put(b"500"),
97            "seisciento" | "seiscienta" | "sexcentésimo" | "sexcentésima" => b.put(b"600"),
98            "seteciento" | "setecienta" | "septingentésimo" | "septingentésima" => b.put(b"700"),
99            "ochociento" | "ochocienta" | "octingentésimo" | "octingentésima" => b.put(b"800"),
100            "noveciento" | "novecienta" | "noningentésimo" | "noningentésima" => b.put(b"900"),
101            "mil" | "milésimo" | "milésima" if b.is_range_free(3, 5) => {
102                let peek = b.peek(2);
103                if peek == b"1" {
104                    Err(Error::Overlap)
105                } else {
106                    b.shift(3)
107                }
108            }
109            "millon" | "millón" | "millonésimo" | "millonésima" if b.is_range_free(6, 8) => {
110                b.shift(6)
111            }
112            "y" if b.len() >= 2 => Err(Error::Incomplete),
113
114            _ => Err(Error::NaN),
115        };
116        if status.is_ok() {
117            b.marker = num_marker;
118            if b.marker.is_fraction() {
119                b.freeze()
120            }
121        }
122        status
123    }
124
125    fn apply_decimal(&self, decimal_func: &str, b: &mut DigitString) -> Result<(), Error> {
126        self.apply(decimal_func, b)
127    }
128
129    fn check_decimal_separator(&self, word: &str) -> Option<char> {
130        match word {
131            "coma" => Some(','),
132            "punto" => Some('.'),
133            _ => None,
134        }
135    }
136
137    fn format_and_value(&self, b: &DigitString) -> (String, f64) {
138        let repr = b.to_string();
139        let val: f64 = repr.parse().unwrap();
140        match b.marker {
141            MorphologicalMarker::Fraction(_) => (format!("1/{repr}"), val.recip()),
142            MorphologicalMarker::Ordinal(marker) => (format!("{repr}{marker}"), val),
143            MorphologicalMarker::None => (repr, val),
144        }
145    }
146
147    fn format_decimal_and_value(
148        &self,
149        int: &DigitString,
150        dec: &DigitString,
151        sep: char,
152    ) -> (String, f64) {
153        let sint = int.to_string();
154        let sdec = dec.to_string();
155        let val = format!("{sint}.{sdec}").parse().unwrap();
156        (format!("{sint}{sep}{sdec}"), val)
157    }
158
159    fn get_morph_marker(&self, word: &str) -> MorphologicalMarker {
160        let sing = lemmatize(word).trim_start_matches("decimo");
161        let is_plur = word.ends_with('s');
162        match sing {
163            "primer" => MorphologicalMarker::Ordinal(".ᵉʳ"),
164            "primero" | "segundo" | "tercero" | "cuarto" | "quinto" | "sexto" | "séptimo"
165            | "octavo" | "ctavo" | "noveno" => {
166                MorphologicalMarker::Ordinal(if is_plur { "ᵒˢ" } else { "º" })
167            }
168            "primera" | "segunda" | "tercera" | "cuarta" | "quinta" | "sexta" | "séptima"
169            | "octava" | "ctava" | "novena" => {
170                MorphologicalMarker::Ordinal(if is_plur { "ᵃˢ" } else { "ª" })
171            }
172            ord if ord.ends_with("imo") => {
173                MorphologicalMarker::Ordinal(if is_plur { "ᵒˢ" } else { "º" })
174            }
175            ord if ord.ends_with("ima") => {
176                MorphologicalMarker::Ordinal(if is_plur { "ᵃˢ" } else { "ª" })
177            }
178            ord if ord.ends_with("avo") => MorphologicalMarker::Fraction("avo"),
179            _ => MorphologicalMarker::None,
180        }
181    }
182
183    fn is_linking(&self, word: &str) -> bool {
184        INSIGNIFICANT.contains(word)
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use crate::word_to_digit::{replace_numbers_in_text, text2digits};
192
193    macro_rules! assert_text2digits {
194        ($text:expr, $res:expr) => {
195            let f = Spanish {};
196            let res = text2digits($text, &f);
197            dbg!(&res);
198            assert!(res.is_ok());
199            assert_eq!(res.unwrap(), $res)
200        };
201    }
202
203    macro_rules! assert_replace_numbers {
204        ($text:expr, $res:expr) => {
205            let f = Spanish {};
206            assert_eq!(replace_numbers_in_text($text, &f, 10.0), $res)
207        };
208    }
209
210    macro_rules! assert_replace_all_numbers {
211        ($text:expr, $res:expr) => {
212            let f = Spanish {};
213            assert_eq!(replace_numbers_in_text($text, &f, 0.0), $res)
214        };
215    }
216
217    macro_rules! assert_invalid {
218        ($text:expr) => {
219            let f = Spanish {};
220            let res = text2digits($text, &f);
221            assert!(res.is_err());
222        };
223    }
224
225    #[test]
226    fn test_apply_steps() {
227        let f = Spanish {};
228        let mut b = DigitString::new();
229        assert!(f.apply("treinta", &mut b).is_ok());
230        assert!(f.apply("cuatro", &mut b).is_ok());
231        assert!(f.apply("veinte", &mut b).is_err());
232    }
233
234    #[test]
235    fn test_apply() {
236        assert_text2digits!("cero", "0");
237        assert_text2digits!("uno", "1");
238        assert_text2digits!("nueve", "9");
239        assert_text2digits!("diez", "10");
240        assert_text2digits!("once", "11");
241        assert_text2digits!("quince", "15");
242        assert_text2digits!("diecinueve", "19");
243        assert_text2digits!("veinte", "20");
244        assert_text2digits!("veintiuno", "21");
245        assert_text2digits!("treinta", "30");
246        assert_text2digits!("treinta y uno", "31");
247        assert_text2digits!("treinta y dos", "32");
248        assert_text2digits!("treinta y nueve", "39");
249        assert_text2digits!("noventa y nueve", "99");
250        assert_text2digits!("ochenta y cinco", "85");
251        assert_text2digits!("ochenta y uno", "81");
252        assert_text2digits!("cien", "100");
253        assert_text2digits!("ciento uno", "101");
254        assert_text2digits!("ciento quince", "115");
255        assert_text2digits!("doscientos", "200");
256        assert_text2digits!("doscientos uno", "201");
257        assert_text2digits!("mil", "1000");
258        assert_text2digits!("mil uno", "1001");
259        assert_text2digits!("dos mil", "2000");
260        assert_text2digits!("dos mil noventa y nueve", "2099");
261        assert_text2digits!("setenta y cinco mil", "75000");
262        assert_text2digits!("mil novecientos veinte", "1920");
263
264        assert_text2digits!("nueve mil novecientos noventa y nueve", "9999");
265        assert_text2digits!(
266            "novecientos noventa y nueve mil novecientos noventa y nueve",
267            "999999"
268        );
269        assert_text2digits!(
270            "novecientos noventa y nueve mil novecientos noventa y nueve millones novecientos noventa y nueve mil novecientos noventa y nueve",
271            "999999999999"
272        );
273        assert_text2digits!(
274            "cincuenta y tres mil veinte millones doscientos cuarenta y tres mil setecientos veinticuatro",
275            "53020243724"
276        );
277        assert_text2digits!(
278            "cincuenta y un millones quinientos setenta y ocho mil trescientos dos",
279            "51578302"
280        );
281    }
282
283    #[test]
284    fn test_variants() {
285        assert_text2digits!("un millon", "1000000");
286        assert_text2digits!("un millón", "1000000");
287        assert_text2digits!("décimo primero", "11º");
288        assert_text2digits!("decimoprimero", "11º");
289        assert_text2digits!("undécimo", "11º");
290        assert_text2digits!("décimo segundo", "12º");
291        assert_text2digits!("decimosegundo", "12º");
292        assert_text2digits!("duodécimo", "12º");
293    }
294
295    #[test]
296    fn test_ordinals() {
297        assert_text2digits!("vigésimo cuarto", "24º");
298        assert_text2digits!("vigésimo primero", "21º");
299        assert_text2digits!("centésimo primero", "101º");
300        assert_text2digits!("decimosexta", "16ª");
301        assert_text2digits!("decimosextas", "16ᵃˢ");
302        assert_text2digits!("decimosextos", "16ᵒˢ");
303    }
304
305    #[test]
306    fn test_fractions() {
307        assert_text2digits!("doceavo", "1/12");
308        assert_text2digits!("centavo", "1/100");
309        assert_text2digits!("ciento veintiochoavos", "1/128");
310    }
311
312    #[test]
313    fn test_zeroes() {
314        assert_text2digits!("cero", "0");
315        assert_text2digits!("cero uno", "01");
316        assert_text2digits!("cero ocho", "08");
317        assert_text2digits!("cero cero ciento veinticinco", "00125");
318        assert_invalid!("cinco cero");
319        assert_invalid!("cincuenta cero tres");
320        assert_invalid!("cincuenta y tres cero");
321        assert_invalid!("diez cero");
322    }
323
324    #[test]
325    fn test_invalid() {
326        assert_invalid!("mil mil doscientos");
327        assert_invalid!("sesenta quince");
328        assert_invalid!("sesenta cien");
329        assert_invalid!("quince cientos");
330        assert_invalid!("veinte cuarto");
331        assert_invalid!("vigésimo decimocuarto");
332        assert_invalid!("diez cuarto");
333        assert_invalid!("uno mil");
334    }
335
336    #[test]
337    fn test_replace_numbers_integers() {
338        assert_replace_numbers!(
339            "Veinticinco vacas, doce gallinas y ciento veinticinco kg de patatas.",
340            "25 vacas, 12 gallinas y 125 kg de patatas."
341        );
342        assert_replace_numbers!(
343            "trescientos hombres y quinientas mujeres",
344            "300 hombres y 500 mujeres"
345        );
346        assert_replace_numbers!("Mil doscientos sesenta y seis dolares.", "1266 dolares.");
347        assert_replace_numbers!("un dos tres cuatro veinte quince.", "1 2 3 4 20 15.");
348        assert_replace_numbers!(
349            "un, dos, tres, cuatro, veinte, quince.",
350            "1, 2, 3, 4, 20, 15."
351        );
352        assert_replace_numbers!("Mil, doscientos, sesenta y seis.", "1000, 200, 66.");
353        assert_replace_numbers!("Veintiuno, treinta y uno.", "21, 31.");
354        assert_replace_numbers!("treinta y cuatro = treinta cuatro", "34 = 34");
355    }
356
357    #[test]
358    fn test_replace_numbers_formal() {
359        assert_replace_numbers!(
360            "dos setenta y cinco cuarenta y nueve cero dos",
361            "2 75 49 02"
362        );
363    }
364
365    #[test]
366    fn test_and() {
367        assert_replace_numbers!("cincuenta sesenta treinta y once", "50 60 30 y 11");
368    }
369
370    #[test]
371    fn test_replace_numbers_zero() {
372        assert_replace_numbers!("trece mil cero noventa", "13000 090");
373        assert_replace_numbers!("cero", "cero");
374        assert_replace_numbers!("cero cinco", "05");
375        assert_replace_numbers!("cero uno ochenta y cinco", "01 85");
376        assert_replace_numbers!("cero, cinco", "0, 5");
377    }
378
379    #[test]
380    fn test_replace_numbers_ordinals() {
381        assert_replace_numbers!(
382            "Cuarto quinto segundo tercero vigésimo primero centésimo milésimo ducentésimo trigésimo.",
383            "4º 5º segundo 3º 21º 100230º."
384        );
385        assert_replace_numbers!("centésimo trigésimo segundo", "132º");
386        assert_replace_numbers!("centésimo, trigésimo, segundo", "100º, 30º, segundo");
387        assert_replace_numbers!(
388            "Un segundo por favor! Vigésimo segundo es diferente que veinte segundos.",
389            "Un segundo por favor! 22º es diferente que 20 segundos."
390        );
391        assert_replace_numbers!(
392            "Un segundo por favor! Vigésimos segundos es diferente que veinte segundos.",
393            "Un segundo por favor! 22ᵒˢ es diferente que 20 segundos."
394        );
395        assert_replace_all_numbers!("Él ha quedado tercero", "Él ha quedado 3º");
396        assert_replace_all_numbers!("Ella ha quedado tercera", "Ella ha quedado 3ª");
397        assert_replace_all_numbers!("Ellos han quedado terceros", "Ellos han quedado 3ᵒˢ");
398        assert_replace_all_numbers!("Ellas han quedado terceras", "Ellas han quedado 3ᵃˢ");
399    }
400
401    #[test]
402    fn test_replace_numbers_decimals() {
403        assert_replace_numbers!(
404            "doce coma noventa y nueve, ciento veinte coma cero cinco, uno coma doscientos treinta y seis, uno coma dos tres y seis.",
405            "12,99, 120,05, 1,236, 1,2 3 y 6."
406        );
407        assert_replace_numbers!("cero coma quince", "0,15");
408        assert_replace_numbers!("uno coma uno", "1,1");
409        assert_replace_numbers!("uno punto uno", "1.1");
410        assert_replace_numbers!("uno coma cuatrocientos uno", "1,401");
411        assert_replace_numbers!("cero coma cuatrocientos uno", "0,401");
412    }
413
414    #[test]
415    fn test_isolates() {
416        assert_replace_numbers!(
417            "Un momento por favor! treinta y un gatos. Uno dos tres cuatro!",
418            "Un momento por favor! 31 gatos. 1 2 3 4!"
419        );
420        assert_replace_numbers!("Ni uno. Uno uno. Treinta y uno", "Ni uno. 1 1. 31");
421    }
422
423    #[test]
424    fn test_isolates_with_noise() {
425        assert_replace_numbers!(
426            "Entonces dos con tres con siete y ocho mas cuatro menos cinco son nueve exacto",
427            "Entonces 2 con 3 con 7 y 8 mas 4 menos 5 son 9 exacto"
428        );
429    }
430}