ebustl-parser 0.7.0

Parse subtitle files in the EBU-STL format with Rust.
Documentation
//! Time codes.

use std::fmt;

/// A time code: hours, minutes, seconds and frames.
///
/// Time codes are ordered by hours, minutes, seconds and frames.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Time {
    /// Hours, 0-23 in Time Code In and Out
    pub hours: u8,
    /// Minutes, 0-59
    pub minutes: u8,
    /// Seconds, 0-59
    pub seconds: u8,
    /// Frames, less than the number of frames per second
    pub frames: u8,
}

impl Time {
    /// Formats the time as `H:M:S,ms`, with the frames converted to milliseconds at `fps`
    /// frames per second.
    ///
    /// # Example
    ///
    /// ```rust
    /// use ebustl_parser::Time;
    ///
    /// let time = Time { hours: 1, minutes: 2, seconds: 3, frames: 12 };
    /// assert_eq!("1:2:3,480", time.format_fps(25));
    /// ```
    pub fn format_fps(&self, fps: usize) -> String {
        format!(
            "{}:{}:{},{}",
            self.hours,
            self.minutes,
            self.seconds,
            self.frames as usize * 1000 / fps
        )
    }
    pub(crate) fn serialize(&self) -> Vec<u8> {
        vec![self.hours, self.minutes, self.seconds, self.frames]
    }

    /// Formats the time as HH:MM:SS:FF, e.g. for messages.
    pub(crate) fn hhmmssff(&self) -> String {
        format!(
            "{:02}:{:02}:{:02}:{:02}",
            self.hours, self.minutes, self.seconds, self.frames
        )
    }

    /// Returns `true` if each element is in the range the spec gives for Time Code In and Out:
    /// hours 0-23, minutes and seconds 0-59 and frames less than `fps`.
    ///
    /// # Example
    ///
    /// ```rust
    /// use ebustl_parser::Time;
    ///
    /// let time = Time { hours: 10, minutes: 0, seconds: 0, frames: 29 };
    /// assert!(time.is_valid(30));
    /// assert!(!time.is_valid(25));
    /// ```
    pub fn is_valid(&self, fps: usize) -> bool {
        self.range_error(fps, true).is_none()
    }

    /// Describes the first element that is outside the ranges in the spec, see
    /// [`Time::is_valid`]. Hours are only checked if `check_hours` is set.
    pub(crate) fn range_error(&self, fps: usize, check_hours: bool) -> Option<String> {
        let formatted = self.hhmmssff();
        if check_hours && self.hours > 23 {
            Some(format!("{formatted} has hours {} above 23", self.hours))
        } else if self.minutes > 59 {
            Some(format!("{formatted} has minutes {} above 59", self.minutes))
        } else if self.seconds > 59 {
            Some(format!("{formatted} has seconds {} above 59", self.seconds))
        } else if usize::from(self.frames) >= fps {
            Some(format!(
                "{formatted} has frame {} above {} at {fps} frames per second",
                self.frames,
                fps.saturating_sub(1)
            ))
        } else {
            None
        }
    }
}

impl fmt::Display for Time {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "{}:{}:{}/{})",
            self.hours, self.minutes, self.seconds, self.frames
        )
    }
}