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 EnHourExpandTask;

impl ExpandTask for EnHourExpandTask {
    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(':')) = queue.get(1) {
                    return Some(ExpandResult::Maybe);
                }
            }
            return None;
        }

        if let (
            Some(ExpandUnit::Number(hh)),
            Some(ExpandUnit::Mark(':')),
            Some(ExpandUnit::Number(mm)),
        ) = (queue.front(), queue.get(1), queue.get(2))
        {
            let h_val = hh.parse::<u32>().unwrap_or(25);
            let m_val = mm.parse::<u32>().unwrap_or(61);

            // Only treat as time if minutes part has exactly 2 digits (e.g. "09", "30").
            // A single-digit minutes like "9" indicates a ratio (e.g. "16:9"), not a time.
            if h_val < 24 && m_val < 60 && mm.len() == 2 {
                let mut replacement = vec![];
                replacement.push(ExpandUnit::Number(hh.clone()));
                if m_val == 0 {
                    replacement.push(ExpandUnit::Word("o'clock".into()));
                } else {
                    if m_val < 10 {
                        replacement.push(ExpandUnit::Word("oh".into()));
                    }
                    replacement.push(ExpandUnit::Number(mm.clone()));
                }
                return Some(ExpandResult::Replace(3, replacement));
            }
        }
        None
    }
}

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

    #[test]
    fn test_en_hour_expansion() {
        let task = EnHourExpandTask;
        let mut queue = VecDeque::new();
        queue.push_back(ExpandUnit::Number("12".into()));
        queue.push_back(ExpandUnit::Mark(':'));
        queue.push_back(ExpandUnit::Number("05".into()));

        let res = task.expand(&queue).unwrap();
        if let ExpandResult::Replace(n, units) = res {
            assert_eq!(n, 3);
            assert_eq!(
                units,
                vec![
                    ExpandUnit::Number("12".into()),
                    ExpandUnit::Word("oh".into()),
                    ExpandUnit::Number("05".into()),
                ]
            );
        } else {
            panic!("Expected Replace result");
        }
    }

    #[test]
    fn test_en_hour_no_expand_ratio() {
        // "16:9" is an aspect ratio, not a time — minutes must have exactly 2 digits
        let task = EnHourExpandTask;
        let mut queue = VecDeque::new();
        queue.push_back(ExpandUnit::Number("16".into()));
        queue.push_back(ExpandUnit::Mark(':'));
        queue.push_back(ExpandUnit::Number("9".into()));

        let res = task.expand(&queue);
        assert!(
            res.is_none(),
            "16:9 should not be expanded as a time (it's an aspect ratio)"
        );
    }

    #[test]
    fn test_en_hour_midnight() {
        // "00:00" should expand as "0 o'clock"
        let task = EnHourExpandTask;
        let mut queue = VecDeque::new();
        queue.push_back(ExpandUnit::Number("00".into()));
        queue.push_back(ExpandUnit::Mark(':'));
        queue.push_back(ExpandUnit::Number("00".into()));

        let res = task.expand(&queue).unwrap();
        if let ExpandResult::Replace(n, units) = res {
            assert_eq!(n, 3);
            assert_eq!(
                units,
                vec![
                    ExpandUnit::Number("00".into()),
                    ExpandUnit::Word("o'clock".into()),
                ]
            );
        } else {
            panic!("Expected Replace result for midnight");
        }
    }

    #[test]
    fn test_en_hour_invalid_hour() {
        // "25:30" — hour out of range, should NOT expand
        let task = EnHourExpandTask;
        let mut queue = VecDeque::new();
        queue.push_back(ExpandUnit::Number("25".into()));
        queue.push_back(ExpandUnit::Mark(':'));
        queue.push_back(ExpandUnit::Number("30".into()));

        let res = task.expand(&queue);
        assert!(
            res.is_none(),
            "25:30 has invalid hour and should not expand"
        );
    }

    #[test]
    fn test_en_hour_invalid_minute() {
        // "12:61" — minute out of range, should NOT expand
        let task = EnHourExpandTask;
        let mut queue = VecDeque::new();
        queue.push_back(ExpandUnit::Number("12".into()));
        queue.push_back(ExpandUnit::Mark(':'));
        queue.push_back(ExpandUnit::Number("61".into()));

        let res = task.expand(&queue);
        assert!(
            res.is_none(),
            "12:61 has invalid minute and should not expand"
        );
    }

    #[test]
    fn test_en_hour_maybe_single_number() {
        // Queue has only a Number — should return Maybe
        let task = EnHourExpandTask;
        let mut queue = VecDeque::new();
        queue.push_back(ExpandUnit::Number("12".into()));

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

    #[test]
    fn test_en_hour_maybe_number_colon() {
        // Queue has Number + ':' but no minutes yet — should return Maybe
        let task = EnHourExpandTask;
        let mut queue = VecDeque::new();
        queue.push_back(ExpandUnit::Number("12".into()));
        queue.push_back(ExpandUnit::Mark(':'));

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

    #[test]
    fn test_en_hour_thirty_minutes() {
        // "12:30" should expand as [Number("12"), Number("30")]
        let task = EnHourExpandTask;
        let mut queue = VecDeque::new();
        queue.push_back(ExpandUnit::Number("12".into()));
        queue.push_back(ExpandUnit::Mark(':'));
        queue.push_back(ExpandUnit::Number("30".into()));

        let res = task.expand(&queue).unwrap();
        if let ExpandResult::Replace(n, units) = res {
            assert_eq!(n, 3);
            assert_eq!(
                units,
                vec![
                    ExpandUnit::Number("12".into()),
                    ExpandUnit::Number("30".into()),
                ]
            );
        } else {
            panic!("Expected Replace result");
        }
    }
}