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;
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 quantity_value_with_unit_transform(unit_text: &str, value: f64) -> f64 {
    match unit_text.to_lowercase().as_str() {
        "milligram" | "mg" => value / 1000.0,
        "kilo" | "kilogram" | "kg" => value * 1000.0,
        "pond" => value * 500.0,
        "ons" => value * 100.0,
        _ => value,
    }
}

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()
    )
}

pub fn rules() -> Vec<Rule> {
    vec![
        Rule {
            name: "<quantity> kopje".to_string(),
            pattern: vec![predicate(is_positive), regex("(kopjes?)")],
            production: Box::new(|nodes| {
                let v = match &nodes[0].token_data {
                    TokenData::Numeral(n) => n.value,
                    _ => return None,
                };
                Some(TokenData::Quantity(QuantityData::new(v, QuantityUnit::Cup)))
            }),
        },
        Rule {
            name: "<quantity> grams".to_string(),
            pattern: vec![predicate(is_positive), regex("(g((r)?(am)?)?)")],
            production: Box::new(|nodes| {
                let v = match &nodes[0].token_data {
                    TokenData::Numeral(n) => n.value,
                    _ => return None,
                };
                let matched = match &nodes[1].token_data {
                    TokenData::RegexMatch(m) => m.group(1)?,
                    _ => return None,
                };
                Some(TokenData::Quantity(QuantityData::new(
                    quantity_value_with_unit_transform(matched, v),
                    QuantityUnit::Gram,
                )))
            }),
        },
        Rule {
            name: "<quantity> milligrams".to_string(),
            pattern: vec![predicate(is_positive), regex("((m(illi)?)(g(ram)?))")],
            production: Box::new(|nodes| {
                let v = match &nodes[0].token_data {
                    TokenData::Numeral(n) => n.value,
                    _ => return None,
                };
                let matched = match &nodes[1].token_data {
                    TokenData::RegexMatch(m) => m.group(1)?,
                    _ => return None,
                };
                Some(TokenData::Quantity(QuantityData::new(
                    quantity_value_with_unit_transform(matched, v),
                    QuantityUnit::Gram,
                )))
            }),
        },
        Rule {
            name: "<quantity> kilograms".to_string(),
            pattern: vec![predicate(is_positive), regex("((k(ilo)?)(g(ram)?)?)")],
            production: Box::new(|nodes| {
                let v = match &nodes[0].token_data {
                    TokenData::Numeral(n) => n.value,
                    _ => return None,
                };
                let matched = match &nodes[1].token_data {
                    TokenData::RegexMatch(m) => m.group(1)?,
                    _ => return None,
                };
                Some(TokenData::Quantity(QuantityData::new(
                    quantity_value_with_unit_transform(matched, v),
                    QuantityUnit::Gram,
                )))
            }),
        },
        Rule {
            name: "<quantity> pond".to_string(),
            pattern: vec![predicate(is_positive), regex("(pond(je(s)?)?)")],
            production: Box::new(|nodes| {
                let v = match &nodes[0].token_data {
                    TokenData::Numeral(n) => n.value,
                    _ => return None,
                };
                Some(TokenData::Quantity(QuantityData::new(
                    v * 500.0,
                    QuantityUnit::Gram,
                )))
            }),
        },
        Rule {
            name: "<quantity> ons".to_string(),
            pattern: vec![predicate(is_positive), regex("(ons(je(s)?)?)")],
            production: Box::new(|nodes| {
                let v = match &nodes[0].token_data {
                    TokenData::Numeral(n) => n.value,
                    _ => return None,
                };
                Some(TokenData::Quantity(QuantityData::new(
                    v * 100.0,
                    QuantityUnit::Gram,
                )))
            }),
        },
        Rule {
            name: "<quantity> product".to_string(),
            pattern: vec![dim(DimensionKind::Quantity), regex("(\\w+)")],
            production: Box::new(|nodes| {
                let mut q = quantity_data(&nodes[0].token_data)?.clone();
                let p = match &nodes[1].token_data {
                    TokenData::RegexMatch(m) => m.group(1)?,
                    _ => return None,
                };
                q.product = Some(p.to_lowercase());
                Some(TokenData::Quantity(q))
            }),
        },
        Rule {
            name: "ongeveer|plm|plusminus <quantity>".to_string(),
            pattern: vec![
                regex("\\~|precies|exact|ongeveer|bijna|ongeveer"),
                dim(DimensionKind::Quantity),
            ],
            production: Box::new(|nodes| Some(nodes[1].token_data.clone())),
        },
        Rule {
            name: "tussen|van <numeral> en|tot <quantity>".to_string(),
            pattern: vec![
                regex("tussen|van"),
                predicate(is_positive),
                regex("tot|en"),
                predicate(is_simple_quantity),
            ],
            production: Box::new(|nodes| {
                let from = match &nodes[1].token_data {
                    TokenData::Numeral(n) => n.value,
                    _ => return None,
                };
                let d = quantity_data(&nodes[3].token_data)?;
                let to = d.value?;
                let u = d.unit?;
                if from >= to {
                    return None;
                }
                Some(TokenData::Quantity(
                    QuantityData::unit_only(u).with_interval(from, to),
                ))
            }),
        },
        Rule {
            name: "tussen|van <quantity> en|tot <quantity>".to_string(),
            pattern: vec![
                regex("tussen|van"),
                predicate(is_simple_quantity),
                regex("en|tot"),
                predicate(is_simple_quantity),
            ],
            production: Box::new(|nodes| {
                let d1 = quantity_data(&nodes[1].token_data)?;
                let d2 = quantity_data(&nodes[3].token_data)?;
                let from = d1.value?;
                let to = d2.value?;
                if from >= to || d1.unit != d2.unit {
                    return None;
                }
                Some(TokenData::Quantity(
                    QuantityData::unit_only(d1.unit?).with_interval(from, to),
                ))
            }),
        },
        Rule {
            name: "<numeral> - <quantity>".to_string(),
            pattern: vec![
                predicate(is_positive),
                regex("\\-"),
                predicate(is_simple_quantity),
            ],
            production: Box::new(|nodes| {
                let from = match &nodes[0].token_data {
                    TokenData::Numeral(n) => n.value,
                    _ => return None,
                };
                let d = quantity_data(&nodes[2].token_data)?;
                let to = d.value?;
                if from >= to {
                    return None;
                }
                Some(TokenData::Quantity(
                    QuantityData::unit_only(d.unit?).with_interval(from, to),
                ))
            }),
        },
        Rule {
            name: "<quantity> - <quantity>".to_string(),
            pattern: vec![
                predicate(is_simple_quantity),
                regex("\\-"),
                predicate(is_simple_quantity),
            ],
            production: Box::new(|nodes| {
                let d1 = quantity_data(&nodes[0].token_data)?;
                let d2 = quantity_data(&nodes[2].token_data)?;
                let from = d1.value?;
                let to = d2.value?;
                if from >= to || d1.unit != d2.unit {
                    return None;
                }
                Some(TokenData::Quantity(
                    QuantityData::unit_only(d1.unit?).with_interval(from, to),
                ))
            }),
        },
        Rule {
            name: "minder dan/hoogstens/op zijn hoogst/maximaal/hooguit <quantity>".to_string(),
            pattern: vec![
                regex("minder dan|hoogstens|hooguit|maximaal|op zijn hoogst"),
                predicate(is_simple_quantity),
            ],
            production: Box::new(|nodes| {
                let d = quantity_data(&nodes[1].token_data)?;
                Some(TokenData::Quantity(
                    QuantityData::unit_only(d.unit?).with_max(d.value?),
                ))
            }),
        },
        Rule {
            name: "meer dan/minstens/op zijn minst <quantity>".to_string(),
            pattern: vec![
                regex("meer dan|minstens|minimaal|op zijn minst|minder dan"),
                predicate(is_simple_quantity),
            ],
            production: Box::new(|nodes| {
                let d = quantity_data(&nodes[1].token_data)?;
                Some(TokenData::Quantity(
                    QuantityData::unit_only(d.unit?).with_min(d.value?),
                ))
            }),
        },
        Rule {
            name: "een <quantity> kopje".to_string(),
            pattern: vec![regex("een? (kopjes?)")],
            production: Box::new(|_| {
                Some(TokenData::Quantity(QuantityData::new(
                    1.0,
                    QuantityUnit::Cup,
                )))
            }),
        },
        Rule {
            name: "een <quantity> grams".to_string(),
            pattern: vec![regex("een? (g((r)?(am)?)?)")],
            production: Box::new(|nodes| {
                let m = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => m.group(1)?,
                    _ => return None,
                };
                Some(TokenData::Quantity(QuantityData::new(
                    quantity_value_with_unit_transform(m, 1.0),
                    QuantityUnit::Gram,
                )))
            }),
        },
        Rule {
            name: "een <quantity> milligrams".to_string(),
            pattern: vec![regex("een? ((m(illi)?)(g(ram)?))")],
            production: Box::new(|nodes| {
                let m = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => m.group(1)?,
                    _ => return None,
                };
                Some(TokenData::Quantity(QuantityData::new(
                    quantity_value_with_unit_transform(m, 1.0),
                    QuantityUnit::Gram,
                )))
            }),
        },
        Rule {
            name: "een <quantity> kilograms".to_string(),
            pattern: vec![regex("een? ((k(ilo)?)(g(ram)?)?)")],
            production: Box::new(|nodes| {
                let m = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => m.group(1)?,
                    _ => return None,
                };
                Some(TokenData::Quantity(QuantityData::new(
                    quantity_value_with_unit_transform(m, 1.0),
                    QuantityUnit::Gram,
                )))
            }),
        },
        Rule {
            name: "een <quantity> pond".to_string(),
            pattern: vec![regex("een? (pond(je(s)?)?)")],
            production: Box::new(|_| {
                Some(TokenData::Quantity(QuantityData::new(
                    500.0,
                    QuantityUnit::Gram,
                )))
            }),
        },
        Rule {
            name: "een <quantity> ons".to_string(),
            pattern: vec![regex("een? (ons(je(s)?)?)")],
            production: Box::new(|_| {
                Some(TokenData::Quantity(QuantityData::new(
                    100.0,
                    QuantityUnit::Gram,
                )))
            }),
        },
    ]
}