duckling 0.4.0

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

use super::{TemperatureData, TemperatureUnit};

fn normalize_decimal_digits(s: &str) -> String {
    s.chars()
        .map(|c| match c {
            '٠' => '0',
            '١' => '1',
            '٢' => '2',
            '٣' => '3',
            '٤' => '4',
            '٥' => '5',
            '٦' => '6',
            '٧' => '7',
            '٨' => '8',
            '٩' => '9',
            '۰' => '0',
            '۱' => '1',
            '۲' => '2',
            '۳' => '3',
            '۴' => '4',
            '۵' => '5',
            '۶' => '6',
            '۷' => '7',
            '۸' => '8',
            '۹' => '9',
            _ => c,
        })
        .collect()
}

fn parse_number(s: &str) -> Option<f64> {
    let normalized = normalize_decimal_digits(s)
        .replace(' ', "")
        .replace(',', ".");
    normalized.parse().ok()
}

pub fn rules() -> Vec<Rule> {
    vec![
        Rule {
            name: "temperature numeric with optional unit (ar)".to_string(),
            pattern: vec![regex("(-?\\s*[0-9٠-٩]+(?:[\\.,][0-9٠-٩]+)?)\\s*(?:°|درج(?:ة|ه|ات)(?:\\s*مئوي(?:ة|ه))?)?\\s*(سي?لي?[سز]ي?وس|ف(?:ا|ي)?هرنها?يت)?")],
            production: Box::new(|nodes| {
                let m = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => m,
                    _ => return None,
                };
                let value = parse_number(m.group(1)?)?;
                let unit = match m.group(2).map(|u| u.to_lowercase()) {
                    Some(u) if u.contains("فهر") => TemperatureUnit::Fahrenheit,
                    Some(u) if u.contains('س') || u.contains('ز') => TemperatureUnit::Celsius,
                    _ => TemperatureUnit::Degree,
                };
                Some(TokenData::Temperature(
                    TemperatureData::new(value).with_unit(unit),
                ))
            }),
        },
        Rule {
            name: "temperature below zero numeric (ar)".to_string(),
            pattern: vec![regex("([0-9٠-٩]+)\\s*تحت الصفر")],
            production: Box::new(|nodes| {
                let v = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => parse_number(m.group(1)?)?,
                    _ => return None,
                };
                Some(TokenData::Temperature(
                    TemperatureData::new(-v).with_unit(TemperatureUnit::Degree),
                ))
            }),
        },
        Rule {
            name: "two degrees below zero (ar)".to_string(),
            pattern: vec![regex("درجت(ين|ان)\\s*تحت الصفر")],
            production: Box::new(|_| {
                Some(TokenData::Temperature(
                    TemperatureData::new(-2.0).with_unit(TemperatureUnit::Degree),
                ))
            }),
        },
        Rule {
            name: "spelled celsius 37 (ar)".to_string(),
            pattern: vec![regex("سبع وثلاثون\\s*(?:سي?لي?[سز]ي?وس)")],
            production: Box::new(|_| {
                Some(TokenData::Temperature(
                    TemperatureData::new(37.0).with_unit(TemperatureUnit::Celsius),
                ))
            }),
        },
        Rule {
            name: "spelled fahrenheit 70 (ar)".to_string(),
            pattern: vec![regex("سبعون\\s*(?:ف(?:ا|ي)?هرنها?يت)")],
            production: Box::new(|_| {
                Some(TokenData::Temperature(
                    TemperatureData::new(70.0).with_unit(TemperatureUnit::Fahrenheit),
                ))
            }),
        },
    ]
}