talw-timecode 0.1.0

SMPTE timecode arithmetic — parse, format, convert, drop-frame
Documentation
use crate::error::TimecodeError;
use crate::framerate::FrameRate;

pub fn parse_timecode(s: &str, rate: FrameRate) -> Result<(u8, u8, u8, u8), TimecodeError> {
    let bytes = s.as_bytes();

    if bytes.len() != 11 {
        return Err(TimecodeError::InvalidFormat);
    }

    let h = parse_two_digits(bytes[0], bytes[1])?;
    if bytes[2] != b':' {
        return Err(TimecodeError::InvalidFormat);
    }
    let m = parse_two_digits(bytes[3], bytes[4])?;
    if bytes[5] != b':' {
        return Err(TimecodeError::InvalidFormat);
    }
    let s_val = parse_two_digits(bytes[6], bytes[7])?;

    let sep = bytes[8];
    if sep != b':' && sep != b';' {
        return Err(TimecodeError::InvalidFormat);
    }

    if sep == b';' && !rate.is_drop_frame() {
        return Err(TimecodeError::InvalidDropFrameRate);
    }

    let f = parse_two_digits(bytes[9], bytes[10])?;

    validate_components(h, m, s_val, f, rate)?;

    Ok((h, m, s_val, f))
}

pub fn validate_str(s: &str, rate: FrameRate) -> bool {
    parse_timecode(s, rate).is_ok()
}

fn validate_components(h: u8, m: u8, s: u8, f: u8, rate: FrameRate) -> Result<(), TimecodeError> {
    if h > 23 {
        return Err(TimecodeError::InvalidHours(h));
    }
    if m > 59 {
        return Err(TimecodeError::InvalidMinutes(m));
    }
    if s > 59 {
        return Err(TimecodeError::InvalidSeconds(s));
    }
    let max_frames = rate.nominal() as u8;
    if f >= max_frames {
        return Err(TimecodeError::InvalidFrames(f, max_frames));
    }
    Ok(())
}

fn parse_two_digits(tens: u8, ones: u8) -> Result<u8, TimecodeError> {
    if !tens.is_ascii_digit() || !ones.is_ascii_digit() {
        return Err(TimecodeError::InvalidFormat);
    }
    Ok((tens - b'0') * 10 + (ones - b'0'))
}

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

    #[test]
    fn valid_timecode() {
        assert_eq!(
            parse_timecode("01:23:45:12", FrameRate::Fps24),
            Ok((1, 23, 45, 12))
        );
    }

    #[test]
    fn drop_frame_separator() {
        assert_eq!(
            parse_timecode("01:23:45;12", FrameRate::Fps29_97Df),
            Ok((1, 23, 45, 12))
        );
    }

    #[test]
    fn semicolon_on_non_drop_frame() {
        assert!(parse_timecode("01:23:45;12", FrameRate::Fps24).is_err());
    }

    #[test]
    fn too_short() {
        assert!(parse_timecode("01:23:45", FrameRate::Fps24).is_err());
    }

    #[test]
    fn invalid_hours() {
        assert!(parse_timecode("25:00:00:00", FrameRate::Fps24).is_err());
    }

    #[test]
    fn invalid_frames_for_rate() {
        assert!(parse_timecode("00:00:00:24", FrameRate::Fps24).is_err());
        assert!(parse_timecode("00:00:00:30", FrameRate::Fps30).is_err());
        assert!(parse_timecode("00:00:00:23", FrameRate::Fps24).is_ok());
        assert!(parse_timecode("00:00:00:29", FrameRate::Fps30).is_ok());
    }

    #[test]
    fn non_numeric() {
        assert!(parse_timecode("ab:cd:ef:gh", FrameRate::Fps24).is_err());
    }
}