Skip to main content

text2num/lang/de/
mod.rs

1//! German number interpreter
2//!
3//! This interpreter is tolerant and accepts splitted words, that is "ein und zwanzig" is treated like "einundzwanzig", as
4//! the main application, Speech-to-text recognition, may introduce spurious spaces.
5
6use bitflags::bitflags;
7
8use crate::digit_string::DigitString;
9use crate::error::Error;
10use crate::tokenizer::WordSplitter;
11
12mod vocabulary;
13
14use super::{LangInterpreter, MorphologicalMarker};
15use vocabulary::INSIGNIFICANT;
16
17fn lemmatize(word: &str) -> &str {
18    // remove declination for ordinals
19    if word.ends_with("tes")
20        || word.ends_with("ter")
21        || word.ends_with("ten")
22        || word.ends_with("tem")
23    {
24        word.trim_end_matches(['s', 'n', 'm', 'r'])
25    } else {
26        word
27    }
28}
29
30bitflags! {
31    /// Words that can be temporarily blocked because of linguistic features.
32    ///(logical, numerical feature inconsistencies are already taken care of by DigitString)
33    struct Excludable: u64 {
34        const TENS = 1;
35    }
36}
37
38pub struct German {
39    word_splitter: WordSplitter,
40}
41
42impl Default for German {
43    fn default() -> Self {
44        Self {
45            word_splitter: WordSplitter::new([
46                "billion",
47                "billionste",
48                "milliarden",
49                "milliarde",
50                "milliardste",
51                "millionen",
52                "million",
53                "millionste",
54                "tausend",
55                "tausendste",
56                "hundert",
57                "hundertste",
58                "und",
59            ])
60            .unwrap(),
61        }
62    }
63}
64
65impl German {
66    pub fn new() -> Self {
67        Default::default()
68    }
69}
70
71impl LangInterpreter for German {
72    fn apply(&self, num_func: &str, b: &mut DigitString) -> Result<(), Error> {
73        // In German, numbers are compounded to form a group
74        let lemma = lemmatize(num_func);
75        if self.word_splitter.is_splittable(lemma) {
76            return match self.exec_group(self.word_splitter.split(lemma)) {
77                Ok(ds) => {
78                    if ds.len() > 3 && ds.len() <= 6 && !b.is_range_free(3, 5) {
79                        return Err(Error::Overlap);
80                    }
81                    b.put(&ds)?;
82                    if ds.marker.is_ordinal() {
83                        b.marker = ds.marker;
84                        b.freeze()
85                    }
86                    Ok(())
87                }
88                Err(err) => Err(err),
89            };
90        }
91        let blocked = Excludable::from_bits_truncate(b.flags);
92        let mut to_block = Excludable::empty();
93
94        let status = match lemma {
95            "null" => b.put(b"0"),
96            "ein" | "eins" | "erste" if b.is_free(2) => {
97                to_block = Excludable::TENS;
98                b.put(b"1")
99            }
100            "zwei" | "zwo" | "zweite" if b.is_free(2) => {
101                to_block = Excludable::TENS;
102                b.put(b"2")
103            }
104            "drei" | "dritte" if b.is_free(2) => {
105                to_block = Excludable::TENS;
106                b.put(b"3")
107            }
108            "vier" | "vierte" if b.is_free(2) => {
109                to_block = Excludable::TENS;
110                b.put(b"4")
111            }
112            "fünf" | "fünfte" if b.is_free(2) => {
113                to_block = Excludable::TENS;
114                b.put(b"5")
115            }
116            "sechs" | "sechste" if b.is_free(2) => {
117                to_block = Excludable::TENS;
118                b.put(b"6")
119            }
120            "sieben" | "siebte" if b.is_free(2) => {
121                to_block = Excludable::TENS;
122                b.put(b"7")
123            }
124            "acht" | "achte" if b.is_free(2) => {
125                to_block = Excludable::TENS;
126                b.put(b"8")
127            }
128            "neun" | "neunte" if b.is_free(2) => {
129                to_block = Excludable::TENS;
130                b.put(b"9")
131            }
132            "zehn" | "zehnte" => b.put(b"10"),
133            "elf" | "elfte" => b.put(b"11"),
134            "zwölf" | "zwölfte" => b.put(b"12"),
135            "dreizehn" | "dreizehnte" => b.put(b"13"),
136            "vierzehn" | "vierzehnte" => b.put(b"14"),
137            "fünfzehn" | "fünfzehnte" => b.put(b"15"),
138            "sechzehn" | "sechzehnte" => b.put(b"16"),
139            "siebzehn" | "siebzehnte" => b.put(b"17"),
140            "achtzehn" | "achtzehnte" => b.put(b"18"),
141            "neunzehn" | "neunzehnte" => b.put(b"19"),
142            "zwanzig" | "zwanzigste" if !blocked.contains(Excludable::TENS) => {
143                b.put_digit_at(b'2', 1)
144            }
145            "dreißig" | "dreissig" | "dreißigste" | "dreissigste"
146                if !blocked.contains(Excludable::TENS) =>
147            {
148                b.put_digit_at(b'3', 1)
149            }
150            "vierzig" | "vierzigste" if !blocked.contains(Excludable::TENS) => {
151                b.put_digit_at(b'4', 1)
152            }
153            "fünfzig" | "fünfzigste" if !blocked.contains(Excludable::TENS) => {
154                b.put_digit_at(b'5', 1)
155            }
156            "sechzig" | "sechzigste" if !blocked.contains(Excludable::TENS) => {
157                b.put_digit_at(b'6', 1)
158            }
159            "siebzig" | "siebzigste" if !blocked.contains(Excludable::TENS) => {
160                b.put_digit_at(b'7', 1)
161            }
162            "achtzig" | "achtzigste" if !blocked.contains(Excludable::TENS) => {
163                b.put_digit_at(b'8', 1)
164            }
165            "neunzig" | "neunzigste" if !blocked.contains(Excludable::TENS) => {
166                b.put_digit_at(b'9', 1)
167            }
168            "hundert" | "hundertste" => {
169                let peek = b.peek(2);
170                if peek.len() == 1 || peek < b"20" {
171                    b.shift(2)
172                } else {
173                    Err(Error::Overlap)
174                }
175            }
176            "tausend" | "tausendste" if b.is_range_free(3, 5) => b.shift(3),
177            "million" | "millionen" | "millionste" if b.is_range_free(6, 8) => b.shift(6),
178            "milliarde" | "milliarden" | "milliardste" => b.shift(9),
179            "billion" | "billionste" => b.shift(12),
180            "und" => Err(Error::Incomplete),
181
182            _ => Err(Error::NaN),
183        };
184        if status.is_ok() {
185            b.flags = to_block.bits();
186            if lemma.ends_with("te") {
187                b.marker = self.get_morph_marker(lemma);
188                b.freeze();
189            }
190            if lemma == "eins" {
191                b.freeze();
192            }
193        } else {
194            b.flags = 0;
195        }
196        status
197    }
198
199    fn apply_decimal(&self, decimal_func: &str, b: &mut DigitString) -> Result<(), Error> {
200        match decimal_func {
201            "null" => b.push(b"0"),
202            "eins" => b.push(b"1"),
203            "zwei" => b.push(b"2"),
204            "drei" => b.push(b"3"),
205            "vier" => b.push(b"4"),
206            "fünf" => b.push(b"5"),
207            "sechs" => b.push(b"6"),
208            "sieben" => b.push(b"7"),
209            "acht" => b.push(b"8"),
210            "neun" => b.push(b"9"),
211            _ => Err(Error::NaN),
212        }
213    }
214
215    fn check_decimal_separator(&self, word: &str) -> Option<char> {
216        match word {
217            "komma" => Some(','),
218            "punkt" => Some('.'),
219            _ => None,
220        }
221    }
222
223    fn format_and_value(&self, b: &DigitString) -> (String, f64) {
224        let repr = b.to_string();
225        let val: f64 = repr.parse().unwrap();
226        if let MorphologicalMarker::Ordinal(marker) = b.marker {
227            (format!("{}{}", b.to_string(), marker), val)
228        } else {
229            (repr, val)
230        }
231    }
232
233    fn format_decimal_and_value(
234        &self,
235        int: &DigitString,
236        dec: &DigitString,
237        sep: char,
238    ) -> (String, f64) {
239        let irepr = int.to_string();
240        let drepr = dec.to_string();
241        let frepr = format!("{irepr}{sep}{drepr}");
242        let val = format!("{irepr}.{drepr}").parse().unwrap();
243        (frepr, val)
244    }
245
246    fn get_morph_marker(&self, word: &str) -> MorphologicalMarker {
247        if word.ends_with("te") {
248            MorphologicalMarker::Ordinal(".")
249        } else {
250            MorphologicalMarker::None
251        }
252    }
253
254    fn is_linking(&self, word: &str) -> bool {
255        INSIGNIFICANT.contains(word)
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::German;
262    use crate::word_to_digit::{replace_numbers_in_text, text2digits};
263
264    macro_rules! assert_text2digits {
265        ($text:expr, $res:expr) => {
266            let f = German::new();
267            let res = text2digits($text, &f);
268            dbg!(&res);
269            assert!(res.is_ok());
270            assert_eq!(res.unwrap(), $res)
271        };
272    }
273
274    macro_rules! assert_replace_numbers {
275        ($text:expr, $res:expr) => {
276            let f = German::new();
277            assert_eq!(replace_numbers_in_text($text, &f, 10.0), $res)
278        };
279    }
280
281    macro_rules! assert_replace_all_numbers {
282        ($text:expr, $res:expr) => {
283            let f = German::new();
284            assert_eq!(replace_numbers_in_text($text, &f, 0.0), $res)
285        };
286    }
287
288    macro_rules! assert_invalid {
289        ($text:expr) => {
290            let f = German::new();
291            let res = text2digits($text, &f);
292            assert!(res.is_err());
293        };
294    }
295
296    #[test]
297    fn test_apply() {
298        assert_text2digits!("fünfundachtzig", "85");
299        assert_text2digits!("einundachtzig", "81");
300        assert_text2digits!("fünfzehn", "15");
301        assert_text2digits!("zwei und vierzig", "42");
302        assert_text2digits!("einhundertfünfzehn", "115");
303        assert_text2digits!("einhundert fünfzehn", "115");
304        assert_text2digits!("ein hundert fünfzehn", "115");
305        assert_text2digits!("fünfundsiebzigtausend", "75000");
306        assert_text2digits!("vierzehntausend", "14000");
307        assert_text2digits!("eintausendneunhundertzwanzig", "1920");
308        assert_text2digits!("neunzehnhundertdreiundsiebzig", "1973");
309        assert_text2digits!(
310            "dreiundfünfzig Milliarden zweihundertdreiundvierzigtausendsiebenhundertvierundzwanzig",
311            "53000243724"
312        );
313        assert_text2digits!(
314            "einundfünfzig Millionen fünfhundertachtundsiebzigtausenddreihundertzwei",
315            "51578302"
316        );
317    }
318
319    #[test]
320    fn test_ordinals() {
321        assert_text2digits!("einundzwanzigster", "21.");
322        assert_text2digits!("eintausendzweihundertdreißigster", "1230.");
323        assert_text2digits!("fünfzigster", "50.");
324        assert_text2digits!("neunundvierzigster", "49.");
325    }
326
327    #[test]
328    fn test_zeroes() {
329        assert_text2digits!("null", "0");
330        assert_text2digits!("null acht", "08");
331        assert_text2digits!("null null hundertfünfundzwanzig", "00125");
332        assert_invalid!("fünf null");
333        assert_invalid!("fünfzignullzwei");
334        assert_invalid!("fünfzigdreinull");
335    }
336
337    #[test]
338    fn test_invalid() {
339        assert_invalid!("tausendtausendzweihundert");
340        assert_invalid!("sechzigfünfzehn");
341        assert_invalid!("sechzighundert");
342        assert_invalid!("zwei und vierzig und");
343        assert_invalid!("dreißig und elf");
344        assert_invalid!("ein und zehn");
345        assert_invalid!("wei und neunzehn");
346        assert_invalid!("zwanzig zweitausend");
347        assert_invalid!("eine und zwanzig");
348        assert_invalid!("eins und zwanzig");
349        assert_invalid!("neun zwanzig");
350    }
351
352    #[test]
353    fn test_replace_intergers() {
354        assert_replace_numbers!(
355            "fünfundzwanzig Kühe, zwölf Hühner und einhundertfünfundzwanzig kg Kartoffeln.",
356            "25 Kühe, 12 Hühner und 125 kg Kartoffeln."
357        );
358        assert_replace_numbers!(
359            "Eintausendzweihundertsechsundsechzig Dollar.",
360            "1266 Dollar."
361        );
362        assert_replace_numbers!("einundzwanzig, einunddreißig.", "21, 31.");
363        assert_replace_numbers!("zweiundzwanzig zweitausendeinundzwanzig", "22 2021");
364        assert_replace_numbers!("zwei und zwanzig zwei tausend ein und zwanzig", "22 2021");
365        assert_replace_numbers!(
366            "tausend hundertzweitausend zweihunderttausend vierzehntausend",
367            "1000 102000 200000 14000"
368        );
369        assert_replace_numbers!("eins zwei drei vier zwanzig fünfzehn", "1 2 3 4 20 15");
370        assert_replace_numbers!("eins zwei drei vier fünf und zwanzig.", "1 2 3 4 25.");
371        assert_replace_numbers!("eins zwei drei vier fünfundzwanzig.", "1 2 3 4 25.");
372        assert_replace_numbers!("eins zwei drei vier fünf zwanzig.", "1 2 3 4 5 20.");
373        assert_replace_numbers!(
374            "achtundachtzig sieben hundert, acht und achtzig siebenhundert, achtundachtzig sieben hundert, acht und achtzig sieben hundert",
375            "88 700, 88 700, 88 700, 88 700"
376        );
377        assert_replace_numbers!(
378            "Zahlen wie vierzig fünfhundert Tausend zweiundzwanzig hundert sind gut.",
379            "Zahlen wie 40 500022 100 sind gut."
380        );
381    }
382
383    #[test]
384    fn test_replace_relaxed() {
385        assert_replace_numbers!("vier und dreißig = vierunddreißig", "34 = 34");
386        assert_replace_numbers!("Ein hundert ein und dreißig", "131");
387        assert_replace_numbers!("Einhundert und drei", "103");
388        assert_replace_numbers!(
389            "eins und zwanzig ist nicht einundzwanzig",
390            "1 und 20 ist nicht 21"
391        );
392        assert_replace_numbers!("Einhundert und Ende", "100 und Ende");
393        assert_replace_numbers!("Einhundert und und", "100 und und");
394        assert_replace_numbers!("neun zwanzig", "9 20");
395    }
396
397    #[test]
398    fn test_replace_formal() {
399        assert_replace_numbers!(
400            "plus dreiunddreißig neun sechzig null sechs zwölf einundzwanzig",
401            "plus 33 9 60 06 12 21"
402        );
403
404        assert_replace_numbers!("null null fünf", "005");
405        assert_replace_numbers!("fünf null null", "5 00");
406        assert_replace_numbers!("null", "null");
407        assert_replace_all_numbers!("null", "0");
408        assert_replace_numbers!(
409            "null neun sechzig null sechs zwölf einundzwanzig",
410            "09 60 06 12 21"
411        );
412        assert_replace_numbers!("fünfzig sechzig dreißig und elf", "50 60 30 und 11");
413        assert_replace_numbers!("dreizehntausend null neunzig", "13000 090");
414    }
415
416    #[test]
417    fn test_replace_ordinals() {
418        assert_replace_numbers!(
419            "erster, zweiter, dritter, vierter, fünfter, sechster, siebter, achter, neunter.",
420            "1., 2., 3., 4., 5., 6., 7., 8., 9.."
421        );
422        assert_replace_numbers!(
423            "zehnter, zwanzigster, einundzwanzigster, fünfundzwanzigster, achtunddreißigster, neunundvierzigster, hundertster, eintausendzweihundertdreißigster.",
424            "10., 20., 21., 25., 38., 49., 100., 1230.."
425        );
426        assert_replace_numbers!("zwanzig erste Versuche", "20 erste Versuche");
427        assert_replace_numbers!("zwei tausend zweite", "2002.");
428        assert_replace_numbers!("zweitausendzweite", "2002.");
429        assert_replace_numbers!(
430            "Dies ist eine Liste oder die Einkaufsliste.",
431            "Dies ist eine Liste oder die Einkaufsliste."
432        );
433        assert_replace_numbers!(
434            "In zehnten Jahrzehnten. Und einmal mit den Vereinten.",
435            "In 10. Jahrzehnten. Und einmal mit den Vereinten."
436        );
437        assert_replace_numbers!(
438            "der zweiundzwanzigste erste zweitausendzweiundzwanzig",
439            "der 22. 1. 2022"
440        );
441        assert_replace_numbers!(
442            "der zwei und zwanzigste erste zwei tausend zwei und zwanzig",
443            "der 22. 1. 2022"
444        );
445        assert_replace_all_numbers!(
446            "das erste lustigste hundertste dreißigste beste",
447            "das 1. lustigste 100. 30. beste"
448        );
449        assert_replace_all_numbers!("der dritte und dreißig", "der 3. und 30");
450        assert_replace_all_numbers!(
451            "Es ist ein Buch mit dreitausend Seiten aber nicht das erste.",
452            "Es ist 1 Buch mit 3000 Seiten aber nicht das 1.."
453        );
454        assert_replace_numbers!(
455            "Es ist ein Buch mit dreitausend Seiten aber nicht das erste.",
456            "Es ist ein Buch mit 3000 Seiten aber nicht das erste."
457        );
458    }
459
460    #[test]
461    fn test_replace_decimals() {
462        assert_replace_numbers!(
463            "Die Testreihe ist zwölf komma neunundneunzig, zwölf komma neun, einhundertzwanzig komma null fünf, eins komma zwei drei sechs.",
464            "Die Testreihe ist 12 komma 99, 12,9, 120,05, 1,236."
465        );
466        assert_replace_numbers!(
467            "null komma fünfzehn geht nicht, aber null komma eins fünf",
468            "0 komma 15 geht nicht, aber 0,15"
469        );
470        assert_replace_numbers!(
471            "Pi ist drei Komma eins vier und so weiter",
472            "Pi ist 3,14 und so weiter"
473        );
474        assert_replace_numbers!("drei Punkt eins vier", "3.14");
475        assert_replace_numbers!("komma eins vier", "komma 1 4");
476        assert_replace_all_numbers!("drei komma", "3 komma");
477        assert_replace_numbers!("drei komma", "drei komma");
478        assert_replace_all_numbers!("eins komma erste", "1 komma 1.");
479    }
480
481    #[test]
482    fn test_replace_signed() {
483        assert_replace_numbers!(
484            "Es ist drinnen plus zwanzig Grad und draußen minus fünfzehn Grad.",
485            "Es ist drinnen plus 20 Grad und draußen minus 15 Grad."
486        );
487    }
488
489    #[test]
490    fn test_uppercase() {
491        assert_replace_numbers!("FÜNFZEHN EINS ZEHN EINS", "15 1 10 1");
492    }
493
494    #[test]
495    fn test_isolates() {
496        assert_replace_all_numbers!(
497            "Ich nehme eins. Eins passt nicht!",
498            "Ich nehme 1. 1 passt nicht!"
499        );
500        assert_replace_numbers!(
501            "Ich nehme eins. Eins passt nicht!",
502            "Ich nehme eins. Eins passt nicht!"
503        );
504        assert_replace_all_numbers!("Velma hat eine Spur", "Velma hat eine Spur");
505
506        assert_replace_all_numbers!("Er sieht eine Zwei", "Er sieht eine 2");
507        assert_replace_numbers!("Er sieht eine Zwei", "Er sieht eine Zwei");
508        assert_replace_numbers!("Ich suche ein Buch", "Ich suche ein Buch");
509        assert_replace_numbers!("Er sieht es nicht ein", "Er sieht es nicht ein");
510        assert_replace_all_numbers!("Eine Eins und eine Zwei", "Eine 1 und eine 2");
511        // ambiguous?
512        // assert_replace_numbers!("Ein Millionen Deal", "Ein 1000000 Deal");
513    }
514
515    // #[test]
516    // fn test_isolates_with_noise() {
517    //     //TODO!
518    //     unimplemented!();
519    // }
520}