duckling 0.4.0

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

use super::OrdinalData;

fn ord_lookup(s: &str) -> Option<i64> {
    match s.to_lowercase().as_str() {
        "første" => Some(1),
        "anden" => Some(2),
        "tredje" => Some(3),
        "fjerde" => Some(4),
        "femte" => Some(5),
        "sjette" => Some(6),
        "syvende" => Some(7),
        "ottende" => Some(8),
        "niende" => Some(9),
        "tiende" => Some(10),
        "elfte" => Some(11),
        "tolvte" => Some(12),
        "trettende" => Some(13),
        "fjortende" => Some(14),
        "femtende" => Some(15),
        "sekstende" => Some(16),
        "syttende" => Some(17),
        "attende" => Some(18),
        "nittende" => Some(19),
        "tyvende" => Some(20),
        "tenogtyvende" => Some(21),
        "toogtyvende" => Some(22),
        "treogtyvende" => Some(23),
        "fireogtyvende" => Some(24),
        "femogtyvende" => Some(25),
        "seksogtyvende" => Some(26),
        "syvogtyvende" => Some(27),
        "otteogtyvende" => Some(28),
        "niogtyvende" => Some(29),
        "tredivte" => Some(30),
        "enogtredivte" => Some(31),
        _ => None,
    }
}

fn tens_ord(s: &str) -> Option<i64> {
    match s.to_lowercase().as_str() {
        "tyvende" => Some(20),
        "tredivte" => Some(30),
        "fyrrende" | "fyrretyvende" => Some(40),
        "halvtredsende" | "halvtredsindstyvende" => Some(50),
        "tressende" | "tresindstyvende" => Some(60),
        "halvfjerdsende" | "halvfjerdsindstyvende" => Some(70),
        "firsende" | "firsindsstyvende" => Some(80),
        "halvfemsende" | "halvfemsindstyvende" => Some(90),
        _ => None,
    }
}

fn ones_prefix(s: &str) -> Option<i64> {
    match s.to_lowercase().as_str() {
        "" => Some(0),
        "enog" => Some(1),
        "toog" => Some(2),
        "treog" => Some(3),
        "fireog" => Some(4),
        "femog" => Some(5),
        "seksog" => Some(6),
        "syvog" => Some(7),
        "otteog" => Some(8),
        "niog" => Some(9),
        _ => None,
    }
}

pub fn rules() -> Vec<Rule> {
    vec![
        Rule {
            name: "ordinal (digits)".to_string(),
            pattern: vec![regex("0*(\\d+)(\\.|ste?)")],
            production: Box::new(|nodes| {
                let v: i64 = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => m.group(1)?.parse().ok()?,
                    _ => return None,
                };
                Some(TokenData::Ordinal(OrdinalData::new(v)))
            }),
        },
        Rule {
            name: "ordinals (first..19st)".to_string(),
            pattern: vec![regex("(første|anden|tredje|fjerde|femte|sjette|syvende|ottende|niende|tiende|elfte|tolvte|trettende|fjortende|femtende|sekstende|syttende|attende|nittende)")],
            production: Box::new(|nodes| {
                let s = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => m.group(1)?,
                    _ => return None,
                };
                Some(TokenData::Ordinal(OrdinalData::new(ord_lookup(s)?)))
            }),
        },
        Rule {
            name: "ordinals, 20 to 99, spelled-out".to_string(),
            pattern: vec![regex("((?:en|to|tre|fire|fem|seks|syv|otte|ni)og)?(tyvende|tredivte|fyrr(?:etyv)?ende|halvtreds(?:indstyv)?ende|tres(?:indstyv|s)?ende|halvfjerds(?:indstyv)?ende|firs(?:indstyv)?ende|halvfems(?:indstyv)?ende)")],
            production: Box::new(|nodes| {
                let m = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => m,
                    _ => return None,
                };
                let ones = ones_prefix(m.group(1).unwrap_or_default())?;
                let tens = tens_ord(m.group(2)?)?;
                Some(TokenData::Ordinal(OrdinalData::new(ones.checked_add(tens)?)))
            }),
        },
        Rule {
            name: "ordinals, above 99, spelled out".to_string(),
            pattern: vec![
                predicate(|td| matches!(td, TokenData::Numeral(n) if n.value > 99.0 && n.value.fract()==0.0)),
                regex("og"),
                dim(DimensionKind::Ordinal),
            ],
            production: Box::new(|nodes| {
                let n = match &nodes[0].token_data {
                    TokenData::Numeral(n) => n.value as i64,
                    _ => return None,
                };
                let o = match &nodes[2].token_data {
                    TokenData::Ordinal(o) => o.value,
                    _ => return None,
                };
                Some(TokenData::Ordinal(OrdinalData::new(n.checked_add(o)?)))
            }),
        },
    ]
}