piper-phoneme-streaming 0.1.1

A high-performance Rust library for streaming Text-to-Phoneme (G2P) conversion.
Documentation
use crate::text_expand::{ExpandResult, ExpandTask, ExpandUnit};
use std::collections::VecDeque;

pub struct ViDateExpandTask;

impl ExpandTask for ViDateExpandTask {
    fn expand(&self, queue: &VecDeque<ExpandUnit>) -> Option<ExpandResult> {
        if queue.len() < 3 {
            if let Some(ExpandUnit::Number(_)) = queue.front() {
                if queue.len() == 1 {
                    return Some(ExpandResult::Maybe);
                }
                if let Some(ExpandUnit::Mark('/') | ExpandUnit::Mark('-')) = queue.get(1) {
                    return Some(ExpandResult::Maybe);
                }
            }
            return None;
        }

        // Support DD/MM/YYYY or DD/MM
        let mut units_to_consume = 0;
        let mut day = None;
        let mut month = None;
        let mut year = None;

        if let (
            Some(ExpandUnit::Number(d)),
            Some(ExpandUnit::Mark('/' | '-')),
            Some(ExpandUnit::Number(m)),
        ) = (queue.front(), queue.get(1), queue.get(2))
        {
            day = Some(d.clone());
            month = Some(m.clone());
            units_to_consume = 3;

            if queue.len() >= 5 {
                if let (Some(ExpandUnit::Mark('/' | '-')), Some(ExpandUnit::Number(y))) =
                    (queue.get(3), queue.get(4))
                {
                    year = Some(y.clone());
                    units_to_consume = 5;
                }
            } else if queue.len() == 4
                && let Some(ExpandUnit::Mark('/' | '-')) = queue.get(3)
            {
                return Some(ExpandResult::Maybe);
            }
        }

        if let (Some(d), Some(m)) = (day, month) {
            let mut replacement = vec![
                ExpandUnit::Word("ngày".into()),
                ExpandUnit::Number(d),
                ExpandUnit::Word("tháng".into()),
                ExpandUnit::Number(m),
            ];
            if let Some(y) = year {
                replacement.push(ExpandUnit::Word("năm".into()));
                replacement.push(ExpandUnit::Number(y));
            }
            return Some(ExpandResult::Replace(units_to_consume, replacement));
        }

        None
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_vi_date_expansion() {
        let task = ViDateExpandTask;
        let mut queue = VecDeque::new();
        queue.push_back(ExpandUnit::Number("24".into()));
        queue.push_back(ExpandUnit::Mark('/'));
        queue.push_back(ExpandUnit::Number("03".into()));
        queue.push_back(ExpandUnit::Mark('/'));
        queue.push_back(ExpandUnit::Number("2026".into()));

        let res = task.expand(&queue).unwrap();
        if let ExpandResult::Replace(n, units) = res {
            assert_eq!(n, 5);
            assert_eq!(units[0], ExpandUnit::Word("ngày".into()));
            assert_eq!(units[1], ExpandUnit::Number("24".into()));
            assert_eq!(units[2], ExpandUnit::Word("tháng".into()));
            assert_eq!(units[3], ExpandUnit::Number("03".into()));
            assert_eq!(units[4], ExpandUnit::Word("năm".into()));
            assert_eq!(units[5], ExpandUnit::Number("2026".into()));
        } else {
            panic!("Expected Replace result");
        }
    }

    #[test]
    fn test_vi_date_expansion_without_year() {
        // DD/MM without year
        let task = ViDateExpandTask;
        let mut queue = VecDeque::new();
        queue.push_back(ExpandUnit::Number("15".into()));
        queue.push_back(ExpandUnit::Mark('/'));
        queue.push_back(ExpandUnit::Number("08".into()));

        let res = task.expand(&queue).unwrap();
        if let ExpandResult::Replace(n, units) = res {
            assert_eq!(n, 3);
            assert_eq!(units[0], ExpandUnit::Word("ngày".into()));
            assert_eq!(units[1], ExpandUnit::Number("15".into()));
            assert_eq!(units[2], ExpandUnit::Word("tháng".into()));
            assert_eq!(units[3], ExpandUnit::Number("08".into()));
            // No "năm" element since no year
            assert_eq!(units.len(), 4);
        } else {
            panic!("Expected Replace result for date without year");
        }
    }

    #[test]
    fn test_vi_date_dash_separator() {
        // Support dash separator
        let task = ViDateExpandTask;
        let mut queue = VecDeque::new();
        queue.push_back(ExpandUnit::Number("01".into()));
        queue.push_back(ExpandUnit::Mark('-'));
        queue.push_back(ExpandUnit::Number("01".into()));
        queue.push_back(ExpandUnit::Mark('-'));
        queue.push_back(ExpandUnit::Number("2025".into()));

        let res = task.expand(&queue).unwrap();
        if let ExpandResult::Replace(n, units) = res {
            assert_eq!(n, 5);
            assert_eq!(units[0], ExpandUnit::Word("ngày".into()));
        } else {
            panic!("Expected Replace result for dash-separated date");
        }
    }

    #[test]
    fn test_vi_date_maybe_single_number() {
        let task = ViDateExpandTask;
        let mut queue = VecDeque::new();
        queue.push_back(ExpandUnit::Number("24".into()));

        let res = task.expand(&queue);
        assert!(
            matches!(res, Some(ExpandResult::Maybe)),
            "Single number should return Maybe"
        );
    }

    #[test]
    fn test_vi_date_maybe_number_separator() {
        let task = ViDateExpandTask;
        let mut queue = VecDeque::new();
        queue.push_back(ExpandUnit::Number("24".into()));
        queue.push_back(ExpandUnit::Mark('/'));

        let res = task.expand(&queue);
        assert!(
            matches!(res, Some(ExpandResult::Maybe)),
            "Number + '/' should return Maybe"
        );
    }

    #[test]
    fn test_vi_date_non_date_returns_none() {
        let task = ViDateExpandTask;
        let mut queue = VecDeque::new();
        queue.push_back(ExpandUnit::Word("xin".into()));

        let res = task.expand(&queue);
        assert!(res.is_none(), "Non-date input should return None");
    }
}