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

use super::{QuantityData, QuantityUnit};

fn quantity_data(td: &TokenData) -> Option<&QuantityData> {
    match td {
        TokenData::Quantity(d) => Some(d),
        _ => None,
    }
}

fn is_simple_quantity(td: &TokenData) -> bool {
    matches!(
        td,
        TokenData::Quantity(d)
            if d.value.is_some()
                && d.unit.is_some()
                && d.min_value.is_none()
                && d.max_value.is_none()
                && d.product.is_none()
    )
}

fn gram_value(matched: &str, value: f64) -> f64 {
    let m = matched.to_lowercase();
    if m.contains("mili") || m.contains("mg") {
        value / 1000.0
    } else if m.contains("kilo") || m.contains("kg") {
        value * 1000.0
    } else {
        value
    }
}

pub fn rules() -> Vec<Rule> {
    vec![
        Rule {
            name: "<quantity> cups".to_string(),
            pattern: vec![predicate(is_positive), regex("(tazas?)")],
            production: Box::new(|nodes| {
                let n = numeral_data(&nodes[0].token_data)?;
                Some(TokenData::Quantity(QuantityData::new(n.value, QuantityUnit::Cup)))
            }),
        },
        Rule {
            name: "<quantity> grams".to_string(),
            pattern: vec![predicate(is_positive), regex("(((m(ili)?)|(k(ilo)?))?g(ramo)?s?)")],
            production: Box::new(|nodes| {
                let n = numeral_data(&nodes[0].token_data)?;
                let unit_text = match &nodes[1].token_data {
                    TokenData::RegexMatch(m) => m.group(1)?,
                    _ => return None,
                };
                Some(TokenData::Quantity(QuantityData::new(
                    gram_value(unit_text, n.value),
                    QuantityUnit::Gram,
                )))
            }),
        },
        Rule {
            name: "<quantity> pounds".to_string(),
            pattern: vec![predicate(is_positive), regex("((lb|libra)s?)")],
            production: Box::new(|nodes| {
                let n = numeral_data(&nodes[0].token_data)?;
                Some(TokenData::Quantity(QuantityData::new(
                    n.value,
                    QuantityUnit::Pound,
                )))
            }),
        },
        Rule {
            name: "<quantity> ounces".to_string(),
            pattern: vec![predicate(is_positive), regex("((onzas?)|oz)")],
            production: Box::new(|nodes| {
                let n = numeral_data(&nodes[0].token_data)?;
                Some(TokenData::Quantity(QuantityData::new(
                    n.value,
                    QuantityUnit::Ounce,
                )))
            }),
        },
        Rule {
            name: "a <quantity> cups".to_string(),
            pattern: vec![regex("una? (tazas?)")],
            production: Box::new(|_| Some(TokenData::Quantity(QuantityData::new(1.0, QuantityUnit::Cup)))),
        },
        Rule {
            name: "a <quantity> grams".to_string(),
            pattern: vec![regex("una? (((m(ili)?)|(k(ilo)?))?g(ramo)?s?)")],
            production: Box::new(|nodes| {
                let unit_text = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => m.group(1)?,
                    _ => return None,
                };
                Some(TokenData::Quantity(QuantityData::new(
                    gram_value(unit_text, 1.0),
                    QuantityUnit::Gram,
                )))
            }),
        },
        Rule {
            name: "a <quantity> pounds".to_string(),
            pattern: vec![regex("una? ((lb|libra)s?)")],
            production: Box::new(|_| {
                Some(TokenData::Quantity(QuantityData::new(
                    1.0,
                    QuantityUnit::Pound,
                )))
            }),
        },
        Rule {
            name: "<quantity> de producto".to_string(),
            pattern: vec![dim(DimensionKind::Quantity), regex("de (\\w+)")],
            production: Box::new(|nodes| {
                let mut q = quantity_data(&nodes[0].token_data)?.clone();
                let product = match &nodes[1].token_data {
                    TokenData::RegexMatch(m) => m.group(1)?,
                    _ => return None,
                };
                q.product = Some(product.to_lowercase());
                Some(TokenData::Quantity(q))
            }),
        },
        Rule {
            name: "under <quantity>".to_string(),
            pattern: vec![
                regex("no m(á|a)s que|menos de|por debajo de|como mucho|como m(á|a)xim(o|a)|a lo sumo|menos (que|de)"),
                predicate(is_simple_quantity),
            ],
            production: Box::new(|nodes| {
                let q = quantity_data(&nodes[1].token_data)?;
                Some(TokenData::Quantity(
                    QuantityData::unit_only(q.unit?).with_max(q.value?),
                ))
            }),
        },
        Rule {
            name: "over <quantity>".to_string(),
            pattern: vec![
                regex("m(á|a)s( grande| pesado)? (de|que)|mayor de|por encima de|excesivo|fuera de|por lo menos|como m(í|i)nim(o|a)|al menos"),
                predicate(is_simple_quantity),
            ],
            production: Box::new(|nodes| {
                let q = quantity_data(&nodes[1].token_data)?;
                Some(TokenData::Quantity(
                    QuantityData::unit_only(q.unit?).with_min(q.value?),
                ))
            }),
        },
    ]
}