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;

use super::ordinal_words_en;

pub struct EnDateExpandTask;

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

        match (
            queue.front(),
            queue.get(1),
            queue.get(2),
            queue.get(3),
            queue.get(4),
        ) {
            (
                Some(ExpandUnit::Number(day)),
                Some(ExpandUnit::Mark('/')),
                Some(ExpandUnit::Number(month)),
                Some(ExpandUnit::Mark('/')),
                Some(ExpandUnit::Number(year)),
            ) => {
                let month_name = match month.parse::<u32>().unwrap_or(0) {
                    1 => "January",
                    2 => "February",
                    3 => "March",
                    4 => "April",
                    5 => "May",
                    6 => "June",
                    7 => "July",
                    8 => "August",
                    9 => "September",
                    10 => "October",
                    11 => "November",
                    12 => "December",
                    _ => return None,
                };

                let day_val = day.parse::<u32>().unwrap_or(0);
                if day_val == 0 || day_val > 31 {
                    return None;
                }

                let ordinal = ordinal_words_en(day_val);
                if ordinal.is_empty() {
                    return None;
                }

                let mut units = vec![ExpandUnit::Word(month_name.into())];
                units.extend(ordinal.into_iter().map(|w| ExpandUnit::Word(w.into())));
                units.push(ExpandUnit::Mark(','));
                units.push(ExpandUnit::Number(year.clone()));

                Some(ExpandResult::Replace(5, units))
            }
            _ => None,
        }
    }
}

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

    #[test]
    fn test_en_date_expansion() {
        let task = EnDateExpandTask;
        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("March".into()));
            assert_eq!(units[1], ExpandUnit::Word("twenty".into()));
            assert_eq!(units[2], ExpandUnit::Word("fourth".into()));
            assert_eq!(units[3], ExpandUnit::Mark(','));
        } else {
            panic!("Expected Replace result");
        }
    }

    #[test]
    fn test_en_date_invalid_month() {
        // Month 13 is invalid — should return None
        let task = EnDateExpandTask;
        let mut queue = VecDeque::new();
        queue.push_back(ExpandUnit::Number("01".into()));
        queue.push_back(ExpandUnit::Mark('/'));
        queue.push_back(ExpandUnit::Number("13".into()));
        queue.push_back(ExpandUnit::Mark('/'));
        queue.push_back(ExpandUnit::Number("2026".into()));

        let res = task.expand(&queue);
        assert!(res.is_none(), "Month 13 should return None");
    }

    #[test]
    fn test_en_date_invalid_day_zero() {
        // Day 0 is invalid — should return None
        let task = EnDateExpandTask;
        let mut queue = VecDeque::new();
        queue.push_back(ExpandUnit::Number("0".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);
        assert!(res.is_none(), "Day 0 should return None");
    }

    #[test]
    fn test_en_date_invalid_day_32() {
        // Day 32 is invalid — should return None
        let task = EnDateExpandTask;
        let mut queue = VecDeque::new();
        queue.push_back(ExpandUnit::Number("32".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);
        assert!(res.is_none(), "Day 32 should return None");
    }

    #[test]
    fn test_en_date_maybe_single_number() {
        let task = EnDateExpandTask;
        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_en_date_maybe_number_slash() {
        let task = EnDateExpandTask;
        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_en_date_maybe_partial_date() {
        // Number / Number — incomplete, should return Maybe
        let task = EnDateExpandTask;
        let mut queue = VecDeque::new();
        queue.push_back(ExpandUnit::Number("24".into()));
        queue.push_back(ExpandUnit::Mark('/'));
        queue.push_back(ExpandUnit::Number("03".into()));

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

    #[test]
    fn test_en_date_non_date_returns_none() {
        // A word in the queue should return None
        let task = EnDateExpandTask;
        let mut queue = VecDeque::new();
        queue.push_back(ExpandUnit::Word("hello".into()));

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