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::{VolumeData, VolumeUnit};

fn zh_digit(ch: char) -> Option<f64> {
    match ch {
        '' => 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),
        _ => None,
    }
}

fn parse_zh_integer(s: &str) -> Option<f64> {
    if s.is_empty() {
        return None;
    }
    if let Ok(v) = s.parse::<f64>() {
        return Some(v);
    }
    let mut total = 0.0f64;
    let mut current = 0.0f64;
    for ch in s.chars() {
        match ch {
            '' => {
                let lhs = if current == 0.0 { 1.0 } else { current };
                total += lhs * 10.0;
                current = 0.0;
            }
            '' => {
                let lhs = if current == 0.0 { 1.0 } else { current };
                total += lhs * 100.0;
                current = 0.0;
            }
            '' => {
                let lhs = if current == 0.0 { 1.0 } else { current };
                total += lhs * 1000.0;
                current = 0.0;
            }
            _ => current = zh_digit(ch)?,
        }
    }
    Some(total + current)
}

fn parse_zh_number(s: &str) -> Option<f64> {
    if s == "" {
        return Some(0.5);
    }
    if let Ok(v) = s.parse::<f64>() {
        return Some(v);
    }
    if let Some((d, n)) = s.split_once("分之") {
        let denom = parse_zh_integer(d)?;
        let numer = parse_zh_integer(n)?;
        return Some(numer / denom);
    }
    if let Some((int_part, frac_part)) = s.split_once('').or_else(|| s.split_once('')) {
        let int_v = parse_zh_integer(int_part)?;
        let mut place = 0.1;
        let mut frac_v = 0.0;
        for ch in frac_part.chars() {
            let d = zh_digit(ch)?;
            frac_v += d * place;
            place /= 10.0;
        }
        return Some(int_v + frac_v);
    }
    parse_zh_integer(s)
}

fn parse_unit(s: &str) -> Option<VolumeUnit> {
    match s.to_lowercase().as_str() {
        "" | "公升" | "l" => Some(VolumeUnit::Litre),
        "毫升" | "ml" | "cc" => Some(VolumeUnit::Millilitre),
        "加侖" => Some(VolumeUnit::Gallon),
        _ => None,
    }
}

pub fn rules() -> Vec<Rule> {
    vec![
        Rule {
            name: "<number><zh volume unit>".to_string(),
            pattern: vec![regex("([0-9]+(?:\\.[0-9]+)?)\\s*(升|公升|l|毫升|ml|cc|加侖)")],
            production: Box::new(|nodes| {
                let (value, unit) = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => (m.group(1)?.parse::<f64>().ok()?, parse_unit(m.group(2)?)?),
                    _ => return None,
                };
                Some(TokenData::Volume(VolumeData::new(value, unit)))
            }),
        },
        Rule {
            name: "<zh numeral><zh volume unit>".to_string(),
            pattern: vec![regex("([零一二兩两三四五六七八九十百千點点半]+)\\s*(升|公升|毫升|加侖)")],
            production: Box::new(|nodes| {
                let (value, unit) = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => (parse_zh_number(m.group(1)?)?, parse_unit(m.group(2)?)?),
                    _ => return None,
                };
                Some(TokenData::Volume(VolumeData::new(value, unit)))
            }),
        },
        Rule {
            name: "fractional chinese volume".to_string(),
            pattern: vec![regex("([一二兩两三四五六七八九十百千]+分之一|半)\\s*(升|公升|加侖|湯匙|茶匙)")],
            production: Box::new(|nodes| {
                let (value, unit_text) = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => (parse_zh_number(m.group(1)?)?, m.group(2)?),
                    _ => return None,
                };
                let (unit, scaled) = match unit_text {
                    "湯匙" => (VolumeUnit::Millilitre, value * 15.0),
                    "茶匙" => (VolumeUnit::Millilitre, value * 5.0),
                    _ => (parse_unit(unit_text)?, value),
                };
                Some(TokenData::Volume(VolumeData::new(scaled, unit)))
            }),
        },
        Rule {
            name: "<zh numeral> teaspoon".to_string(),
            pattern: vec![regex("([零一二兩两三四五六七八九十百千]+)茶匙")],
            production: Box::new(|nodes| {
                let value = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => parse_zh_number(m.group(1)?)?,
                    _ => return None,
                };
                Some(TokenData::Volume(VolumeData::new(
                    value * 5.0,
                    VolumeUnit::Millilitre,
                )))
            }),
        },
        Rule {
            name: "corpus one-third tablespoon".to_string(),
            pattern: vec![regex("三分一湯匙")],
            production: Box::new(|_| {
                Some(TokenData::Volume(VolumeData::new(
                    5.0,
                    VolumeUnit::Millilitre,
                )))
            }),
        },
        Rule {
            name: "between volumes (zh)".to_string(),
            pattern: vec![regex("([0-9零一二兩两三四五六七八九十百千]+)(升|公升|l)?\\s*(-|~|至|到)\\s*([0-9零一二兩两三四五六七八九十百千]+)\\s*(升|公升|l)?")],
            production: Box::new(|nodes| {
                let (from, to, u1, u2) = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => {
                        let f = parse_zh_number(m.group(1)?)?;
                        let t = parse_zh_number(m.group(4)?)?;
                        (f, t, m.group(2), m.group(5))
                    }
                    _ => return None,
                };
                if from >= to {
                    return None;
                }
                let unit = parse_unit(u2.or(u1).unwrap_or(""))?;
                Some(TokenData::Volume(
                    VolumeData::unit_only(unit).with_interval(from, to),
                ))
            }),
        },
        Rule {
            name: "at most volume (zh)".to_string(),
            pattern: vec![regex("(最多\\s*([0-9零一二兩两三四五六七八九十百千]+)\\s*(個)?\\s*(加侖|升|公升|l|毫升|ml|cc)|([0-9零一二兩两三四五六七八九十百千]+)\\s*(加侖|升|公升|l|毫升|ml|cc)\\s*(或)?以下)")],
            production: Box::new(|nodes| {
                let (value_text, unit_text) = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => {
                        if let (Some(v), Some(u)) = (m.group(2), m.group(4)) {
                            (v, u)
                        } else {
                            (m.group(5)?, m.group(6)?)
                        }
                    }
                    _ => return None,
                };
                let value = parse_zh_number(value_text)?;
                let unit = parse_unit(unit_text)?;
                Some(TokenData::Volume(
                    VolumeData::unit_only(unit).with_max(value),
                ))
            }),
        },
        Rule {
            name: "at least volume (zh)".to_string(),
            pattern: vec![regex("(至少|最少|起碼)\\s*([0-9零一二兩两三四五六七八九十百千]+)\\s*(毫升|ml|cc|升|公升|l)|([0-9零一二兩两三四五六七八九十百千]+)\\s*(毫升|ml|cc|升|公升|l)\\s*(或)?以上")],
            production: Box::new(|nodes| {
                let (value_text, unit_text) = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => {
                        if let (Some(v), Some(u)) = (m.group(2), m.group(3)) {
                            (v, u)
                        } else {
                            (m.group(4)?, m.group(5)?)
                        }
                    }
                    _ => return None,
                };
                let value = parse_zh_number(value_text)?;
                let unit = parse_unit(unit_text)?;
                Some(TokenData::Volume(
                    VolumeData::unit_only(unit).with_min(value),
                ))
            }),
        },
    ]
}