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

impl ExpandTask for ViHourExpandTask {
    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![
                    ExpandUnit::Number(hh.clone()),
                    ExpandUnit::Word("giờ".into()),
                ];
                if m_val > 0 {
                    replacement.push(ExpandUnit::Number(mm.clone()));
                    replacement.push(ExpandUnit::Word("phút".into()));
                }
                return Some(ExpandResult::Replace(3, replacement));
            }
        }
        None
    }
}

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

    #[test]
    fn test_vi_hour_expansion() {
        let task = ViHourExpandTask;
        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::Word("giờ".into()),
                    ExpandUnit::Number("30".into()),
                    ExpandUnit::Word("phút".into()),
                ]
            );
        } else {
            panic!("Expected Replace result");
        }
    }

    #[test]
    fn test_vi_hour_no_expand_ratio() {
        // "16:9" is an aspect ratio, not a time — minutes must have exactly 2 digits
        let task = ViHourExpandTask;
        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_vi_hour_midnight() {
        // "00:00" — midnight, should expand as [Number("00"), Word("giờ")] with no phút
        let task = ViHourExpandTask;
        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("giờ".into()),
                ]
            );
        } else {
            panic!("Expected Replace result for midnight");
        }
    }

    #[test]
    fn test_vi_hour_maybe_single_number() {
        let task = ViHourExpandTask;
        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_vi_hour_maybe_number_colon() {
        let task = ViHourExpandTask;
        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"
        );
    }
}