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

use super::NumeralData;

fn thai_digit(tok: &str) -> Option<f64> {
    match tok {
        "ศูนย์" => 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 tokenize_thai_number(mut s: &str) -> Option<Vec<&str>> {
    let mut out = Vec::new();
    while !s.is_empty() {
        let candidates = [
            "ศูนย์",
            "หนึ่ง",
            "เอ็ด",
            "สอง",
            "ยี่",
            "สาม",
            "สี่",
            "ห้า",
            "หก",
            "เจ็ด",
            "แปด",
            "เก้า",
            "สิบ",
            "ร้อย",
            "พัน",
            "หมื่น",
            "แสน",
            "ล้าน",
        ];
        let mut matched = None;
        for c in candidates {
            if s.starts_with(c) {
                matched = Some(c);
                break;
            }
        }
        let m = matched?;
        out.push(m);
        s = &s[m.len()..];
    }
    Some(out)
}

fn parse_thai_integer(s: &str) -> Option<f64> {
    let tokens = tokenize_thai_number(s)?;
    let mut total = 0.0;
    let mut section = 0.0;
    let mut number = 0.0;

    for t in tokens {
        if let Some(d) = thai_digit(t) {
            number = d;
            continue;
        }
        let unit = match t {
            "สิบ" => Some(10.0),
            "ร้อย" => Some(100.0),
            "พัน" => Some(1000.0),
            "หมื่น" => Some(10000.0),
            "แสน" => Some(100000.0),
            _ => None,
        };
        if let Some(u) = unit {
            if number == 0.0 {
                number = 1.0;
            }
            section += number * u;
            number = 0.0;
            continue;
        }
        if t == "ล้าน" {
            section += number;
            if section == 0.0 {
                section = 1.0;
            }
            total += section * 1e6;
            section = 0.0;
            number = 0.0;
            continue;
        }
        return None;
    }

    Some(total + section + number)
}

fn power_word(s: &str) -> Option<NumeralData> {
    match s {
        "สิบ" => Some(NumeralData::new(1e1).with_grain(1).with_multipliable(true)),
        "ร้อย" => Some(NumeralData::new(1e2).with_grain(2).with_multipliable(true)),
        "พัน" => Some(NumeralData::new(1e3).with_grain(3).with_multipliable(true)),
        "หมื่น" => Some(NumeralData::new(1e4).with_grain(4).with_multipliable(true)),
        "แสน" => Some(NumeralData::new(1e5).with_grain(5).with_multipliable(true)),
        "ล้าน" => Some(NumeralData::new(1e6).with_grain(6).with_multipliable(true)),
        "โหล" => Some(
            NumeralData::new(12.0)
                .with_grain(1)
                .with_multipliable(true)
                .not_ok_for_any_time(),
        ),
        _ => None,
    }
}

pub fn rules() -> Vec<Rule> {
    vec![
        Rule {
            name: "thai none".to_string(),
            pattern: vec![regex("ไม่มี")],
            production: Box::new(|_| Some(TokenData::Numeral(NumeralData::new(0.0)))),
        },
        Rule {
            name: "thai word number".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(parse_thai_integer(s)?)))
            }),
        },
        Rule {
            name: "powers".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(power_word(s)?))
            }),
        },
        Rule {
            name: "compose multiply".to_string(),
            pattern: vec![
                dim(DimensionKind::Numeral),
                predicate(|td| matches!(td, TokenData::Numeral(d) if d.multipliable)),
            ],
            production: Box::new(|nodes| {
                let a = numeral_data(&nodes[0].token_data)?;
                let b = numeral_data(&nodes[1].token_data)?;
                if b.value > a.value || b.value == 12.0 {
                    let mut out = NumeralData::new(a.value * b.value);
                    if let Some(g) = b.grain {
                        out = out.with_grain(g);
                    }
                    if !b.ok_for_any_time {
                        out = out.not_ok_for_any_time();
                    }
                    Some(TokenData::Numeral(out))
                } else {
                    None
                }
            }),
        },
        Rule {
            name: "intersect".to_string(),
            pattern: vec![
                predicate(|td| matches!(td, TokenData::Numeral(d) if d.grain.is_some())),
                predicate(
                    |td| matches!(td, TokenData::Numeral(d) if !d.multipliable && d.value >= 0.0),
                ),
            ],
            production: Box::new(|nodes| {
                let a = numeral_data(&nodes[0].token_data)?;
                let b = numeral_data(&nodes[1].token_data)?;
                let g = a.grain? as i32;
                if 10f64.powi(g) > b.value {
                    Some(TokenData::Numeral(NumeralData::new(a.value + b.value)))
                } else {
                    None
                }
            }),
        },
        Rule {
            name: "dot word decimal".to_string(),
            pattern: vec![
                dim(DimensionKind::Numeral),
                regex("จุด"),
                predicate(|td| matches!(td, TokenData::Numeral(d) if d.grain.is_none())),
            ],
            production: Box::new(|nodes| {
                let a = numeral_data(&nodes[0].token_data)?.value;
                let b = numeral_data(&nodes[2].token_data)?.value;
                Some(TokenData::Numeral(NumeralData::new(
                    a + decimals_to_double(b),
                )))
            }),
        },
        Rule {
            name: "dot word decimal compact".to_string(),
            pattern: vec![regex(
                "([ศูนย์หนึ่งเอ็ดสองยี่สามสี่ห้าหกเจ็ดแปดเก้า]+)จุด([ศูนย์หนึ่งเอ็ดสองยี่สามสี่ห้าหกเจ็ดแปดเก้า]+)",
            )],
            production: Box::new(|nodes| {
                let (a, b) = match &nodes[0].token_data {
                    TokenData::RegexMatch(m) => (m.group(1)?, m.group(2)?),
                    _ => return None,
                };
                let lhs = parse_thai_integer(a)?;
                let rhs = parse_thai_integer(b)?;
                Some(TokenData::Numeral(NumeralData::new(
                    lhs + decimals_to_double(rhs),
                )))
            }),
        },
        Rule {
            name: "negative".to_string(),
            pattern: vec![
                regex("-|ลบ"),
                predicate(|td| matches!(td, TokenData::Numeral(d) if d.value >= 0.0)),
            ],
            production: Box::new(|nodes| {
                let v = numeral_data(&nodes[1].token_data)?.value;
                Some(TokenData::Numeral(NumeralData::new(-v)))
            }),
        },
    ]
}