use core::fmt;
use core::ops::{Add, Sub};
use crate::convert::convert_frames;
use crate::dropframe::{components_to_frames, frames_to_components};
use crate::error::TimecodeError;
use crate::format::format_timecode;
use crate::framerate::{FrameRate, Rational};
use crate::parse::parse_timecode;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Timecode {
total_frames: i64,
rate: FrameRate,
}
impl Timecode {
pub fn new(h: u8, m: u8, s: u8, f: u8, rate: FrameRate) -> Result<Self, TimecodeError> {
let max_frames = rate.nominal() as u8;
if h > 23 {
return Err(TimecodeError::InvalidHours(h));
}
if m > 59 {
return Err(TimecodeError::InvalidMinutes(m));
}
if s > 59 {
return Err(TimecodeError::InvalidSeconds(s));
}
if f >= max_frames {
return Err(TimecodeError::InvalidFrames(f, max_frames));
}
Ok(Self {
total_frames: components_to_frames(h, m, s, f, rate),
rate,
})
}
pub const fn from_frames(total_frames: i64, rate: FrameRate) -> Self {
Self { total_frames, rate }
}
pub fn from_seconds(seconds: f64, rate: FrameRate) -> Self {
let r = rate.rational();
let frames = (seconds * r.num as f64 / r.den as f64).round() as i64;
Self {
total_frames: frames,
rate,
}
}
pub fn from_milliseconds(ms: f64, rate: FrameRate) -> Self {
Self::from_seconds(ms / 1000.0, rate)
}
pub fn parse(s: &str, rate: FrameRate) -> Result<Self, TimecodeError> {
let (h, m, s_val, f) = parse_timecode(s, rate)?;
Ok(Self {
total_frames: components_to_frames(h, m, s_val, f, rate),
rate,
})
}
pub fn validate(s: &str, rate: FrameRate) -> bool {
parse_timecode(s, rate).is_ok()
}
pub fn hours(&self) -> u8 {
frames_to_components(self.total_frames, self.rate).0
}
pub fn minutes(&self) -> u8 {
frames_to_components(self.total_frames, self.rate).1
}
pub fn seconds(&self) -> u8 {
frames_to_components(self.total_frames, self.rate).2
}
pub fn frames(&self) -> u8 {
frames_to_components(self.total_frames, self.rate).3
}
pub fn components(&self) -> (u8, u8, u8, u8) {
frames_to_components(self.total_frames, self.rate)
}
pub const fn total_frames(&self) -> i64 {
self.total_frames
}
pub const fn rate(&self) -> FrameRate {
self.rate
}
pub fn to_seconds(&self) -> f64 {
let r = self.rate.rational();
self.total_frames as f64 * r.den as f64 / r.num as f64
}
pub fn to_milliseconds(&self) -> f64 {
self.to_seconds() * 1000.0
}
pub fn to_rational(&self) -> (i64, Rational) {
(self.total_frames, self.rate.rational())
}
pub fn convert_to(&self, target_rate: FrameRate) -> Self {
Self {
total_frames: convert_frames(self.total_frames, self.rate, target_rate),
rate: target_rate,
}
}
pub fn frame_diff(&self, other: &Self) -> Result<i64, TimecodeError> {
if self.rate != other.rate {
return Err(TimecodeError::MismatchedRates);
}
Ok(self.total_frames - other.total_frames)
}
}
impl fmt::Display for Timecode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let bytes = format_timecode(self.total_frames, self.rate);
let s = core::str::from_utf8(&bytes).unwrap();
f.write_str(s)
}
}
impl PartialOrd for Timecode {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
if self.rate != other.rate {
return None;
}
Some(self.total_frames.cmp(&other.total_frames))
}
}
impl Add<i64> for Timecode {
type Output = Self;
fn add(self, frames: i64) -> Self {
Self {
total_frames: self.total_frames + frames,
rate: self.rate,
}
}
}
impl Sub<i64> for Timecode {
type Output = Self;
fn sub(self, frames: i64) -> Self {
Self {
total_frames: self.total_frames - frames,
rate: self.rate,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_and_display() {
let tc = Timecode::new(1, 23, 45, 12, FrameRate::Fps24).unwrap();
assert_eq!(tc.to_string(), "01:23:45:12");
}
#[test]
fn from_frames() {
let tc = Timecode::from_frames(86400, FrameRate::Fps24);
assert_eq!(tc.to_string(), "01:00:00:00");
}
#[test]
fn from_seconds() {
let tc = Timecode::from_seconds(0.5, FrameRate::Fps24);
assert_eq!(tc.to_string(), "00:00:00:12");
}
#[test]
fn from_milliseconds() {
let tc = Timecode::from_milliseconds(500.0, FrameRate::Fps24);
assert_eq!(tc.to_string(), "00:00:00:12");
}
#[test]
fn parse_roundtrip() {
let tc = Timecode::parse("01:23:45:12", FrameRate::Fps24).unwrap();
assert_eq!(tc.to_string(), "01:23:45:12");
}
#[test]
fn to_seconds() {
let tc = Timecode::from_frames(24, FrameRate::Fps24);
assert!((tc.to_seconds() - 1.0).abs() < 0.001);
}
#[test]
fn to_milliseconds() {
let tc = Timecode::from_frames(12, FrameRate::Fps24);
assert!((tc.to_milliseconds() - 500.0).abs() < 1.0);
}
#[test]
fn add_frames() {
let tc = Timecode::from_frames(0, FrameRate::Fps24);
let tc2 = tc + 48;
assert_eq!(tc2.to_string(), "00:00:02:00");
}
#[test]
fn sub_frames() {
let tc = Timecode::from_frames(48, FrameRate::Fps24);
let tc2 = tc - 24;
assert_eq!(tc2.to_string(), "00:00:01:00");
}
#[test]
fn frame_diff() {
let a = Timecode::from_frames(100, FrameRate::Fps24);
let b = Timecode::from_frames(50, FrameRate::Fps24);
assert_eq!(a.frame_diff(&b).unwrap(), 50);
}
#[test]
fn frame_diff_mismatched_rates() {
let a = Timecode::from_frames(100, FrameRate::Fps24);
let b = Timecode::from_frames(100, FrameRate::Fps30);
assert!(a.frame_diff(&b).is_err());
}
#[test]
fn convert_24_to_30() {
let tc = Timecode::from_frames(24, FrameRate::Fps24);
let converted = tc.convert_to(FrameRate::Fps30);
assert_eq!(converted.total_frames(), 30);
}
#[test]
fn ordering() {
let a = Timecode::from_frames(10, FrameRate::Fps24);
let b = Timecode::from_frames(20, FrameRate::Fps24);
assert!(a < b);
}
#[test]
fn ordering_different_rates_is_none() {
let a = Timecode::from_frames(10, FrameRate::Fps24);
let b = Timecode::from_frames(10, FrameRate::Fps30);
assert!(a.partial_cmp(&b).is_none());
}
#[test]
fn drop_frame_display() {
let tc = Timecode::from_frames(1800, FrameRate::Fps29_97Df);
assert_eq!(tc.to_string(), "00:01:00;02");
}
#[test]
fn components() {
let tc = Timecode::new(1, 23, 45, 12, FrameRate::Fps24).unwrap();
assert_eq!(tc.hours(), 1);
assert_eq!(tc.minutes(), 23);
assert_eq!(tc.seconds(), 45);
assert_eq!(tc.frames(), 12);
}
#[test]
fn ms_roundtrip_matches_python() {
let tc = Timecode::from_milliseconds(5025000.0, FrameRate::Fps24);
let back = tc.to_milliseconds();
assert!((back - 5025000.0).abs() < 50.0);
}
#[test]
fn validate() {
assert!(Timecode::validate("01:23:45:12", FrameRate::Fps24));
assert!(!Timecode::validate("01:23:45", FrameRate::Fps24));
assert!(!Timecode::validate("25:00:00:00", FrameRate::Fps24));
}
#[test]
fn rust_nexus_ms_to_smpte_basic() {
let tc = Timecode::from_milliseconds(3723000.0, FrameRate::Fps30);
assert_eq!(tc.to_string(), "01:02:03:00");
}
#[test]
fn rust_nexus_ms_to_smpte_with_frames() {
let tc = Timecode::from_milliseconds(500.0, FrameRate::Fps30);
assert_eq!(tc.to_string(), "00:00:00:15");
}
#[test]
fn rust_nexus_ms_to_smpte_24fps() {
let tc = Timecode::from_milliseconds(500.0, FrameRate::Fps24);
assert_eq!(tc.to_string(), "00:00:00:12");
}
}