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::{is_natural, numeral_data};
use crate::dimensions::time_grain::Grain;
use crate::pattern::{dim, predicate, regex};
use crate::types::{DimensionKind, Rule, TokenData};

use super::DurationData;

fn n_plus_one_half(grain: Grain, n: i64) -> Option<DurationData> {
    match grain {
        Grain::Minute => Some(DurationData::new(
            60_i64.checked_mul(n)?.checked_add(30)?,
            Grain::Second,
        )),
        Grain::Hour => Some(DurationData::new(
            60_i64.checked_mul(n)?.checked_add(30)?,
            Grain::Minute,
        )),
        Grain::Day => Some(DurationData::new(
            24_i64.checked_mul(n)?.checked_add(12)?,
            Grain::Hour,
        )),
        Grain::Month => Some(DurationData::new(
            30_i64.checked_mul(n)?.checked_add(15)?,
            Grain::Day,
        )),
        Grain::Year => Some(DurationData::new(
            12_i64.checked_mul(n)?.checked_add(6)?,
            Grain::Month,
        )),
        _ => None,
    }
}

pub fn rules() -> Vec<Rule> {
    vec![
        Rule {
            name: "<integer> <unit-of-duration>".to_string(),
            pattern: vec![predicate(is_natural), dim(DimensionKind::TimeGrain)],
            production: Box::new(|nodes| {
                let n = numeral_data(&nodes[0].token_data)?.value as i64;
                let grain = match &nodes[1].token_data {
                    TokenData::TimeGrain(g) => *g,
                    _ => return None,
                };
                Some(TokenData::Duration(DurationData::new(n, grain)))
            }),
        },
        Rule {
            name: "<unit-of-duration> as duration".to_string(),
            pattern: vec![dim(DimensionKind::TimeGrain)],
            production: Box::new(|nodes| match &nodes[0].token_data {
                TokenData::TimeGrain(g) => Some(TokenData::Duration(DurationData::new(1, *g))),
                _ => None,
            }),
        },
        Rule {
            name: "half an hour".to_string(),
            pattern: vec![regex("(1/2\\s?|пів\\s?)години?")],
            production: Box::new(|_| {
                Some(TokenData::Duration(DurationData::new(30, Grain::Minute)))
            }),
        },
        Rule {
            name: "number.number hours".to_string(),
            pattern: vec![regex("(\\d+)\\.(\\d+)\\s*години?")],
            production: Box::new(|nodes| {
                let rm = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => m,
                    _ => return None,
                };
                let h: i64 = rm.group(1)?.parse().ok()?;
                let frac = rm.group(2)?;
                let num: i64 = frac.parse().ok()?;
                let den: i64 = 10_i64.pow(frac.len() as u32);
                let total_minutes = 60_i64
                    .checked_mul(h)?
                    .checked_add(num.checked_mul(60)?.checked_div(den)?)?;
                Some(TokenData::Duration(DurationData::new(
                    total_minutes,
                    Grain::Minute,
                )))
            }),
        },
        Rule {
            name: "<integer> and a half hour".to_string(),
            pattern: vec![predicate(is_natural), regex("з\\s+половиною\\s+години")],
            production: Box::new(|nodes| {
                let n = numeral_data(&nodes[0].token_data)?.value as i64;
                Some(TokenData::Duration(DurationData::new(
                    60_i64.checked_mul(n)?.checked_add(30)?,
                    Grain::Minute,
                )))
            }),
        },
        Rule {
            name: "about <duration>".to_string(),
            pattern: vec![regex("близько"), dim(DimensionKind::Duration)],
            production: Box::new(|nodes| Some(nodes[1].token_data.clone())),
        },
        Rule {
            name: "exactly <duration>".to_string(),
            pattern: vec![regex("рівно"), dim(DimensionKind::Duration)],
            production: Box::new(|nodes| Some(nodes[1].token_data.clone())),
        },
        Rule {
            name: "half a <time-grain>".to_string(),
            pattern: vec![regex("(1/2|пів)"), dim(DimensionKind::TimeGrain)],
            production: Box::new(|nodes| {
                let grain = match &nodes[1].token_data {
                    TokenData::TimeGrain(g) => *g,
                    _ => return None,
                };
                Some(TokenData::Duration(n_plus_one_half(grain, 0)?))
            }),
        },
        Rule {
            name: "one and a half <unit-of-duration>".to_string(),
            pattern: vec![regex("півтори"), dim(DimensionKind::TimeGrain)],
            production: Box::new(|nodes| {
                let grain = match &nodes[1].token_data {
                    TokenData::TimeGrain(g) => *g,
                    _ => return None,
                };
                Some(TokenData::Duration(n_plus_one_half(grain, 1)?))
            }),
        },
        Rule {
            name: "composite <duration> (with ,/і)".to_string(),
            pattern: vec![
                predicate(is_natural),
                dim(DimensionKind::TimeGrain),
                regex(",|і"),
                dim(DimensionKind::Duration),
            ],
            production: Box::new(|nodes| {
                let n = numeral_data(&nodes[0].token_data)?.value as i64;
                let g = match &nodes[1].token_data {
                    TokenData::TimeGrain(grain) => *grain,
                    _ => return None,
                };
                let dd = match &nodes[3].token_data {
                    TokenData::Duration(d) => d,
                    _ => return None,
                };
                if g <= dd.grain {
                    return None;
                }
                let left = DurationData::new(n, g);
                Some(TokenData::Duration(left.combine(dd)?))
            }),
        },
    ]
}