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::NumeralData;

fn numeral_0_10(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 one_to_nine(td: &TokenData) -> bool {
    matches!(td, TokenData::Numeral(d) if (1.0..10.0).contains(&d.value))
}

fn is_val(td: &TokenData, candidates: &[f64]) -> bool {
    matches!(td, TokenData::Numeral(d) if candidates.contains(&d.value))
}

pub fn rules() -> Vec<Rule> {
    vec![
        Rule {
            name: "decimal number".to_string(),
            pattern: vec![regex("(\\d*\\.\\d+)")],
            production: Box::new(|nodes| {
                let s = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => m.group(1)?,
                    _ => return None,
                };
                Some(TokenData::Numeral(NumeralData::new(s.parse().ok()?)))
            }),
        },
        Rule {
            name: "decimal with thousands separator".to_string(),
            pattern: vec![regex("(\\d+(,\\d\\d\\d)+\\.\\d+)")],
            production: Box::new(|nodes| {
                let s = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => m.group(1)?,
                    _ => return None,
                };
                Some(TokenData::Numeral(NumeralData::new(
                    s.replace(',', "").parse().ok()?,
                )))
            }),
        },
        Rule {
            name: "integer with thousands separator ,".to_string(),
            pattern: vec![regex("(\\d{1,3}(,\\d\\d\\d){1,5})")],
            production: Box::new(|nodes| {
                let s = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => m.group(1)?,
                    _ => return None,
                };
                Some(TokenData::Numeral(NumeralData::new(
                    s.replace(',', "").parse().ok()?,
                )))
            }),
        },
        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)?,
                    _ => return None,
                };
                Some(TokenData::Numeral(NumeralData::new(numeral_0_10(s)?)))
            }),
        },
        Rule {
            name: "integer (11..19)".to_string(),
            pattern: vec![regex(""), predicate(one_to_nine)],
            production: Box::new(|nodes| {
                let b = numeral_data(&nodes[1].token_data)?.value;
                Some(TokenData::Numeral(NumeralData::new(10.0 + b)))
            }),
        },
        Rule {
            name: "integer (20..90)".to_string(),
            pattern: vec![predicate(one_to_nine), regex("")],
            production: Box::new(|nodes| {
                let a = numeral_data(&nodes[0].token_data)?.value;
                Some(TokenData::Numeral(NumeralData::new(a * 10.0)))
            }),
        },
        Rule {
            name: "integer 21..99".to_string(),
            pattern: vec![
                predicate(|td| is_val(td, &[20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0])),
                predicate(one_to_nine),
            ],
            production: Box::new(|nodes| {
                let a = numeral_data(&nodes[0].token_data)?.value;
                let b = numeral_data(&nodes[1].token_data)?.value;
                Some(TokenData::Numeral(NumeralData::new(a + b)))
            }),
        },
        Rule {
            name: "integer (100)".to_string(),
            pattern: vec![regex("")],
            production: Box::new(|_| Some(TokenData::Numeral(NumeralData::new(100.0)))),
        },
        Rule {
            name: "integer (100..199)".to_string(),
            pattern: vec![
                regex(""),
                predicate(
                    |td| matches!(td, TokenData::Numeral(d) if (1.0..100.0).contains(&d.value)),
                ),
            ],
            production: Box::new(|nodes| {
                let b = numeral_data(&nodes[1].token_data)?.value;
                Some(TokenData::Numeral(NumeralData::new(100.0 + b)))
            }),
        },
        Rule {
            name: "integer (200..900)".to_string(),
            pattern: vec![predicate(one_to_nine), regex("")],
            production: Box::new(|nodes| {
                let a = numeral_data(&nodes[0].token_data)?.value;
                if a >= 2.0 {
                    Some(TokenData::Numeral(NumeralData::new(a * 100.0)))
                } else {
                    None
                }
            }),
        },
        Rule {
            name: "integer 201..999".to_string(),
            pattern: vec![
                predicate(|td| {
                    is_val(
                        td,
                        &[200.0, 300.0, 400.0, 500.0, 600.0, 700.0, 800.0, 900.0],
                    )
                }),
                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[1].token_data)?.value;
                Some(TokenData::Numeral(NumeralData::new(a + b)))
            }),
        },
        Rule {
            name: "integer (1000)".to_string(),
            pattern: vec![regex("")],
            production: Box::new(|_| Some(TokenData::Numeral(NumeralData::new(1000.0)))),
        },
        Rule {
            name: "integer (1000..1999)".to_string(),
            pattern: vec![
                regex(""),
                predicate(
                    |td| matches!(td, TokenData::Numeral(d) if (1.0..1000.0).contains(&d.value)),
                ),
            ],
            production: Box::new(|nodes| {
                let b = numeral_data(&nodes[1].token_data)?.value;
                Some(TokenData::Numeral(NumeralData::new(1000.0 + b)))
            }),
        },
        Rule {
            name: "integer (2000..9000)".to_string(),
            pattern: vec![predicate(one_to_nine), regex("")],
            production: Box::new(|nodes| {
                let a = numeral_data(&nodes[0].token_data)?.value;
                if a >= 2.0 {
                    Some(TokenData::Numeral(NumeralData::new(a * 1000.0)))
                } else {
                    None
                }
            }),
        },
        Rule {
            name: "integer 2001..9999".to_string(),
            pattern: vec![
                predicate(|td| {
                    is_val(
                        td,
                        &[
                            2000.0, 3000.0, 4000.0, 5000.0, 6000.0, 7000.0, 8000.0, 9000.0,
                        ],
                    )
                }),
                predicate(
                    |td| matches!(td, TokenData::Numeral(d) if (1.0..1000.0).contains(&d.value)),
                ),
            ],
            production: Box::new(|nodes| {
                let a = numeral_data(&nodes[0].token_data)?.value;
                let b = numeral_data(&nodes[1].token_data)?.value;
                Some(TokenData::Numeral(NumeralData::new(a + b)))
            }),
        },
        Rule {
            name: "integer (10000)".to_string(),
            pattern: vec![regex("")],
            production: Box::new(|_| Some(TokenData::Numeral(NumeralData::new(10000.0)))),
        },
        Rule {
            name: "integer (10000..19999)".to_string(),
            pattern: vec![
                regex(""),
                predicate(
                    |td| matches!(td, TokenData::Numeral(d) if (1.0..10000.0).contains(&d.value)),
                ),
            ],
            production: Box::new(|nodes| {
                let b = numeral_data(&nodes[1].token_data)?.value;
                Some(TokenData::Numeral(NumeralData::new(10000.0 + b)))
            }),
        },
        Rule {
            name: "integer (20000..90000)".to_string(),
            pattern: vec![predicate(one_to_nine), regex("")],
            production: Box::new(|nodes| {
                let a = numeral_data(&nodes[0].token_data)?.value;
                if a >= 2.0 {
                    Some(TokenData::Numeral(NumeralData::new(a * 10000.0)))
                } else {
                    None
                }
            }),
        },
        Rule {
            name: "integer 20001..99999".to_string(),
            pattern: vec![
                predicate(|td| {
                    is_val(
                        td,
                        &[
                            20000.0, 30000.0, 40000.0, 50000.0, 60000.0, 70000.0, 80000.0, 90000.0,
                        ],
                    )
                }),
                predicate(
                    |td| matches!(td, TokenData::Numeral(d) if (1.0..10000.0).contains(&d.value)),
                ),
            ],
            production: Box::new(|nodes| {
                let a = numeral_data(&nodes[0].token_data)?.value;
                let b = numeral_data(&nodes[1].token_data)?.value;
                Some(TokenData::Numeral(NumeralData::new(a + b)))
            }),
        },
        Rule {
            name: "<number>个".to_string(),
            pattern: vec![dim(DimensionKind::Numeral), regex("(个|個)")],
            production: Box::new(|nodes| Some(nodes[0].token_data.clone())),
        },
        Rule {
            name: "numbers suffixes (K, M, G, 千, 万)".to_string(),
            pattern: vec![dim(DimensionKind::Numeral), regex("(k|m|g|千|万)")],
            production: Box::new(|nodes| {
                let v = numeral_data(&nodes[0].token_data)?.value;
                let s = match &nodes[1].token_data {
                    TokenData::RegexMatch(m) => m.group(1)?.to_lowercase(),
                    _ => return None,
                };
                let out = match s.as_str() {
                    "k" | "" => v * 1e3,
                    "" => v * 1e4,
                    "m" => v * 1e6,
                    "g" => v * 1e9,
                    _ => return None,
                };
                Some(TokenData::Numeral(NumeralData::new(out)))
            }),
        },
        Rule {
            name: "numbers prefix with -, negative or minus".to_string(),
            pattern: vec![regex("-|マイナス\\s?|負\\s?"), predicate(is_positive)],
            production: Box::new(|nodes| {
                let v = numeral_data(&nodes[1].token_data)?.value;
                Some(TokenData::Numeral(NumeralData::new(-v)))
            }),
        },
    ]
}