duckling 0.4.0

A Rust port of Facebook's Duckling library for parsing natural language into structured data
Documentation
use crate::dimensions::numeral::helpers::{decimals_to_double, numeral_data};
use crate::pattern::{dim, predicate, regex};
use crate::types::{DimensionKind, Rule, TokenData};

use super::NumeralData;

fn arabic_digit_to_ascii(c: char) -> char {
    match c {
        '٠' => '0',
        '١' => '1',
        '٢' => '2',
        '٣' => '3',
        '٤' => '4',
        '٥' => '5',
        '٦' => '6',
        '٧' => '7',
        '٨' => '8',
        '٩' => '9',
        _ => c,
    }
}

fn parse_arabic_number(raw: &str) -> Option<f64> {
    let normalized: String = raw
        .chars()
        .map(arabic_digit_to_ascii)
        .filter(|c| *c != '٬' && *c != ',')
        .map(|c| if c == '٫' { '.' } else { c })
        .collect();
    normalized.parse::<f64>().ok()
}

fn lex_unit(s: &str) -> Option<f64> {
    match s {
        "صفر" => Some(0.0),
        "واحد" | "واحدة" => Some(1.0),
        "إثنين" | "اثنين" | "إثنتين" | "اثنتين" | "اتنين" => Some(2.0),
        "ثلاثة" | "ثلاث" | "تلاتة" => Some(3.0),
        "أربعة" | "اربعة" | "أربع" | "اربع" | "اربعه" => Some(4.0),
        "خمسة" | "خمس" => Some(5.0),
        "ستة" | "ست" | "سته" => Some(6.0),
        "سبعة" | "سبع" => Some(7.0),
        "ثمانية" | "ثمان" | "تمنية" | "تمنة" => Some(8.0),
        "تسعة" | "تسع" => Some(9.0),
        "عشرة" | "عشره" | "عشر" => Some(10.0),
        _ => None,
    }
}

fn tens_word(s: &str) -> Option<f64> {
    match s {
        "عشرون" | "عشرين" => Some(20.0),
        "ثلاثون" | "ثلاثين" | "تلاتين" => Some(30.0),
        "أربعون" | "اربعون" | "أربعين" | "اربعين" => Some(40.0),
        "خمسون" | "خمسين" => Some(50.0),
        "ستون" | "ستين" => Some(60.0),
        "سبعون" | "سبعين" => Some(70.0),
        "ثمانون" | "تمانين" | "ثمانين" => Some(80.0),
        "تسعون" | "تسعين" => Some(90.0),
        _ => None,
    }
}

fn hundred_word(s: &str) -> Option<f64> {
    match s {
        "مائة" | "مائه" | "مئة" | "مئه" => Some(100.0),
        "مائتين" | "مائتان" | "متين" => Some(200.0),
        "ثلاثمائة" | "ثلاثماية" | "تلتمية" => Some(300.0),
        "أربعمائة" | "اربعمائة" | "أربعمية" | "اربعمية" => {
            Some(400.0)
        }
        "خمسمائة" | "خمسمية" => Some(500.0),
        "ستمائة" | "ستمية" => Some(600.0),
        "سبعمائة" | "سبعمية" => Some(700.0),
        "ثمانمائة" | "ثمانمية" => Some(800.0),
        "تسعمائة" | "تسعمية" => Some(900.0),
        _ => None,
    }
}

fn teens_special(s: &str) -> Option<f64> {
    match s {
        "إحدى عشرة" | "إحدى عشر" | "احد عشر" | "حداشر" => Some(11.0),
        "إثنتى عشر" | "إثنى عشر" | "اتناشر" | "إتناشر" => Some(12.0),
        "اربعتاشر" | "أربعتاشر" => Some(14.0),
        "ستاشر" => Some(16.0),
        "سبعتاشر" => Some(17.0),
        "تمنتاشر" => Some(18.0),
        _ => None,
    }
}

fn power_word(s: &str) -> Option<NumeralData> {
    match s {
        "ألف" | "الف" | "آلاف" | "الاف" => {
            Some(NumeralData::new(1e3).with_grain(3).with_multipliable(true))
        }
        "الفين" | "الفان" => Some(NumeralData::new(2e3).with_grain(3)),
        "مليون" | "ملايين" => {
            Some(NumeralData::new(1e6).with_grain(6).with_multipliable(true))
        }
        "مليونين" | "مليونان" => Some(NumeralData::new(2e6).with_grain(6)),
        _ => None,
    }
}

fn is_positive(td: &TokenData) -> bool {
    matches!(td, TokenData::Numeral(d) if d.value >= 0.0)
}

pub fn rules() -> Vec<Rule> {
    vec![
        Rule {
            name: "Arabic numeric (with separators)".to_string(),
            pattern: vec![regex("([٠-٩]+(?:[٬,][٠-٩]{3})*(?:[٫.][٠-٩]+)?)")],
            production: Box::new(|nodes| {
                let s = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => m.group(1)?,
                    _ => return None,
                };
                Some(TokenData::Numeral(NumeralData::new(parse_arabic_number(s)?)))
            }),
        },
        Rule {
            name: "Arabic fractional number numeric".to_string(),
            pattern: vec![regex("([٠-٩]+)/([٠-٩]+)")],
            production: Box::new(|nodes| {
                let (n, d) = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => (m.group(1)?, m.group(2)?),
                    _ => return None,
                };
                let nn = parse_arabic_number(n)?;
                let dd = parse_arabic_number(d)?;
                if dd == 0.0 {
                    None
                } else {
                    Some(TokenData::Numeral(NumeralData::new(nn / dd)))
                }
            }),
        },
        Rule {
            name: "integer 0..10".to_string(),
            pattern: vec![regex("(صفر|واحد[ةه]?|[إا]ثن(ين|تين)|اتنين|ثلاث[ةه]?|[أا]ربع[ةه]?|خمس[ةه]?|ست[ةه]?|سبع[ةه]?|ثما?ني?[ةه]?|عشر[ةه]?)")],
            production: Box::new(|nodes| {
                let s = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => m.group(1)?.to_lowercase(),
                    _ => return None,
                };
                Some(TokenData::Numeral(NumeralData::new(lex_unit(&s)?)))
            }),
        },
        Rule {
            name: "integer 11..18 special".to_string(),
            pattern: vec![regex("(حداشر|[إاأ]حد[يى]? عشر[ةه]?|إتناشر|اتناشر|[إا]?ت?ث?ن(ت)?[يىا] ?عشر[ةه]?|ا?ربعتاشر|ستاشر|سبعتاشر|تمنتاشر)")],
            production: Box::new(|nodes| {
                let s = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => m.group(1)?.to_lowercase(),
                    _ => return None,
                };
                Some(TokenData::Numeral(NumeralData::new(teens_special(&s)?)))
            }),
        },
        Rule {
            name: "integer (13..19)".to_string(),
            pattern: vec![
                predicate(|td| matches!(td, TokenData::Numeral(d) if (3.0..10.0).contains(&d.value))),
                predicate(|td| matches!(td, TokenData::Numeral(d) if d.value == 10.0)),
            ],
            production: Box::new(|nodes| {
                let v = numeral_data(&nodes[0].token_data)?.value;
                Some(TokenData::Numeral(NumeralData::new(v + 10.0)))
            }),
        },
        Rule {
            name: "integer (20..90)".to_string(),
            pattern: vec![regex("((عشر|ثلاث|[أا]ربع|خمس|ست|سبع|ثمان|تسع)(ون|ين)|تلاتين)")],
            production: Box::new(|nodes| {
                let s = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => m.group(1)?.to_lowercase(),
                    _ => return None,
                };
                Some(TokenData::Numeral(NumeralData::new(tens_word(&s)?)))
            }),
        },
        Rule {
            name: "integer 21..99".to_string(),
            pattern: vec![
                predicate(|td| matches!(td, TokenData::Numeral(d) if (1.0..10.0).contains(&d.value))),
                regex("و"),
                predicate(|td| matches!(td, TokenData::Numeral(d) if [20.0,30.0,40.0,50.0,60.0,70.0,80.0,90.0].contains(&d.value))),
            ],
            production: Box::new(|nodes| {
                let a = numeral_data(&nodes[0].token_data)?.value;
                let b = numeral_data(&nodes[2].token_data)?.value;
                Some(TokenData::Numeral(NumeralData::new(a + b)))
            }),
        },
        Rule {
            name: "hundreds lex".to_string(),
            pattern: vec![regex("(مائت(ين|ان)|متين|ثلاثمائة|ثلاثماية|تلتمية|[أا]ربعم(ائة|ية)|خمسم(ائة|ية)|ستم(ائة|ية)|سبعم(ائة|ية)|ثمانم(ائة|ية)|تسعم(ائة|ية)|م[ئا]?[يئ][ةه])")],
            production: Box::new(|nodes| {
                let s = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => m.group(1)?.to_lowercase(),
                    _ => return None,
                };
                Some(TokenData::Numeral(
                    NumeralData::new(hundred_word(&s)?).with_grain(2),
                ))
            }),
        },
        Rule {
            name: "integer 101..999".to_string(),
            pattern: vec![
                predicate(|td| matches!(td, TokenData::Numeral(d) if [100.0,200.0,300.0,400.0,500.0,600.0,700.0,800.0,900.0].contains(&d.value))),
                regex("و"),
                predicate(|td| matches!(td, TokenData::Numeral(d) if (1.0..100.0).contains(&d.value))),
            ],
            production: Box::new(|nodes| {
                let a = numeral_data(&nodes[0].token_data)?.value;
                let b = numeral_data(&nodes[2].token_data)?.value;
                Some(TokenData::Numeral(NumeralData::new(a + b)))
            }),
        },
        Rule {
            name: "powers of tens".to_string(),
            pattern: vec![regex("(م[ئا]?[يئ][ةه]|مئت(ان|ين)|مئات|[أا]لف(ان|ين)?|[آا]لاف|ملايين|مليون(ان|ين)?)")],
            production: Box::new(|nodes| {
                let s = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => m.group(1)?.to_lowercase(),
                    _ => return None,
                };
                if matches!(s.as_str(), "مئة" | "مئه" | "مائة" | "مائه" | "مئات") {
                    return Some(TokenData::Numeral(
                        NumeralData::new(1e2).with_grain(2).with_multipliable(true),
                    ));
                }
                Some(TokenData::Numeral(power_word(&s)?))
            }),
        },
        Rule {
            name: "compose by multiplication".to_string(),
            pattern: vec![dim(DimensionKind::Numeral), predicate(|td| matches!(td, TokenData::Numeral(d) if d.multipliable))],
            production: Box::new(|nodes| {
                let a = numeral_data(&nodes[0].token_data)?;
                let b = numeral_data(&nodes[1].token_data)?;
                if b.value > a.value {
                    let mut out = NumeralData::new(a.value * b.value);
                    if let Some(g) = b.grain {
                        out = out.with_grain(g);
                    }
                    Some(TokenData::Numeral(out))
                } else {
                    None
                }
            }),
        },
        Rule {
            name: "number dot number".to_string(),
            pattern: vec![dim(DimensionKind::Numeral), regex("فاصل[ةه]|فاصلة"), predicate(|td| matches!(td, TokenData::Numeral(d) if d.grain.is_none()))],
            production: Box::new(|nodes| {
                let a = numeral_data(&nodes[0].token_data)?.value;
                let b = numeral_data(&nodes[2].token_data)?.value;
                Some(TokenData::Numeral(NumeralData::new(a + decimals_to_double(b))))
            }),
        },
        Rule {
            name: "numbers prefix with -, minus".to_string(),
            pattern: vec![regex("-"), predicate(is_positive)],
            production: Box::new(|nodes| {
                let v = numeral_data(&nodes[1].token_data)?.value;
                Some(TokenData::Numeral(NumeralData::new(-v)))
            }),
        },
    ]
}