use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq)]
#[allow(dead_code)]
pub struct TimecodeValue {
pub hh: u8,
pub mm: u8,
pub ss: u8,
pub ff: u8,
pub fps: f32,
pub drop_frame: bool,
}
impl TimecodeValue {
#[must_use]
pub fn new(hh: u8, mm: u8, ss: u8, ff: u8, fps: f32, drop_frame: bool) -> Self {
Self {
hh,
mm,
ss,
ff,
fps,
drop_frame,
}
}
#[must_use]
fn fps_int(&self) -> u64 {
self.fps.ceil() as u64
}
#[must_use]
fn frames_per_day(&self) -> u64 {
self.fps_int() * 3600 * 24
}
#[must_use]
pub fn to_frame_count(&self) -> u64 {
let fps = self.fps_int();
let hh = u64::from(self.hh);
let mm = u64::from(self.mm);
let ss = u64::from(self.ss);
let ff = u64::from(self.ff);
let raw = hh * 3600 * fps + mm * 60 * fps + ss * fps + ff;
if self.drop_frame {
let total_minutes = hh * 60 + mm;
let dropped = 2 * (total_minutes - total_minutes / 10);
raw - dropped
} else {
raw
}
}
#[must_use]
pub fn from_frame_count(frames: u64, fps: f32, drop_frame: bool) -> Self {
let fps_int = fps.ceil() as u64;
let mut remaining = frames;
if drop_frame {
let frames_per_min = fps_int * 60 - 2;
let frames_per_10_min = frames_per_min * 9 + fps_int * 60;
let ten_min_blocks = remaining / frames_per_10_min;
remaining += ten_min_blocks * 18;
let remaining_in_block = remaining % frames_per_10_min;
if remaining_in_block >= fps_int * 60 {
let extra_minutes = (remaining_in_block - fps_int * 60) / frames_per_min;
remaining += (extra_minutes + 1) * 2;
}
}
let hh = ((remaining / (fps_int * 3600)) % 24) as u8;
remaining %= fps_int * 3600;
let mm = (remaining / (fps_int * 60)) as u8;
remaining %= fps_int * 60;
let ss = (remaining / fps_int) as u8;
let ff = (remaining % fps_int) as u8;
Self::new(hh, mm, ss, ff, fps, drop_frame)
}
#[must_use]
pub fn add_frames(&self, frames: i64) -> Self {
let total = self.to_frame_count() as i64;
let frames_per_day = self.frames_per_day() as i64;
let new_total = ((total + frames) % frames_per_day + frames_per_day) % frames_per_day;
Self::from_frame_count(new_total as u64, self.fps, self.drop_frame)
}
#[must_use]
pub fn subtract(&self, other: &Self) -> i64 {
self.to_frame_count() as i64 - other.to_frame_count() as i64
}
#[must_use]
pub fn to_string_formatted(&self) -> String {
let sep = if self.drop_frame { ';' } else { ':' };
format!(
"{:02}:{:02}:{:02}{}{:02}",
self.hh, self.mm, self.ss, sep, self.ff
)
}
#[must_use]
pub fn parse(s: &str, fps: f32) -> Option<Self> {
let drop_frame = s.contains(';');
let normalized = s.replace(';', ":");
let parts: Vec<&str> = normalized.split(':').collect();
if parts.len() != 4 {
return None;
}
let hh: u8 = parts[0].parse().ok()?;
let mm: u8 = parts[1].parse().ok()?;
let ss: u8 = parts[2].parse().ok()?;
let ff: u8 = parts[3].parse().ok()?;
if hh > 23 || mm > 59 || ss > 59 {
return None;
}
Some(Self::new(hh, mm, ss, ff, fps, drop_frame))
}
}
impl fmt::Display for TimecodeValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_string_formatted())
}
}
#[allow(dead_code)]
pub struct Duration;
impl Duration {
#[must_use]
pub fn from_timecode(tc: &TimecodeValue) -> f64 {
let frame_count = tc.to_frame_count();
f64::from(frame_count as u32) / f64::from(tc.fps)
}
#[must_use]
pub fn to_timecode(seconds: f64, fps: f32, drop_frame: bool) -> TimecodeValue {
let total_frames = (seconds * f64::from(fps)).round() as u64;
TimecodeValue::from_frame_count(total_frames, fps, drop_frame)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_timecode_value_new() {
let tc = TimecodeValue::new(1, 2, 3, 4, 25.0, false);
assert_eq!(tc.hh, 1);
assert_eq!(tc.mm, 2);
assert_eq!(tc.ss, 3);
assert_eq!(tc.ff, 4);
assert!((tc.fps - 25.0).abs() < f32::EPSILON);
assert!(!tc.drop_frame);
}
#[test]
fn test_to_frame_count_ndf() {
let tc = TimecodeValue::new(0, 0, 1, 0, 25.0, false);
assert_eq!(tc.to_frame_count(), 25);
}
#[test]
fn test_to_frame_count_one_hour() {
let tc = TimecodeValue::new(1, 0, 0, 0, 30.0, false);
assert_eq!(tc.to_frame_count(), 3600 * 30);
}
#[test]
fn test_from_frame_count_ndf() {
let tc = TimecodeValue::from_frame_count(25, 25.0, false);
assert_eq!(tc.hh, 0);
assert_eq!(tc.mm, 0);
assert_eq!(tc.ss, 1);
assert_eq!(tc.ff, 0);
}
#[test]
fn test_frame_count_roundtrip_ndf() {
let original = TimecodeValue::new(1, 30, 45, 12, 25.0, false);
let frames = original.to_frame_count();
let recovered = TimecodeValue::from_frame_count(frames, 25.0, false);
assert_eq!(original, recovered);
}
#[test]
fn test_add_frames_forward() {
let tc = TimecodeValue::new(0, 0, 0, 0, 25.0, false);
let tc2 = tc.add_frames(25);
assert_eq!(tc2.ss, 1);
assert_eq!(tc2.ff, 0);
}
#[test]
fn test_add_frames_backward() {
let tc = TimecodeValue::new(0, 0, 1, 0, 25.0, false);
let tc2 = tc.add_frames(-25);
assert_eq!(tc2.hh, 0);
assert_eq!(tc2.mm, 0);
assert_eq!(tc2.ss, 0);
assert_eq!(tc2.ff, 0);
}
#[test]
fn test_add_frames_wrap_at_24h() {
let tc = TimecodeValue::new(23, 59, 59, 24, 25.0, false);
let tc2 = tc.add_frames(1); assert_eq!(tc2.hh, 0);
assert_eq!(tc2.mm, 0);
assert_eq!(tc2.ss, 0);
assert_eq!(tc2.ff, 0);
}
#[test]
fn test_subtract() {
let tc1 = TimecodeValue::new(0, 0, 1, 0, 25.0, false);
let tc2 = TimecodeValue::new(0, 0, 0, 0, 25.0, false);
assert_eq!(tc1.subtract(&tc2), 25);
assert_eq!(tc2.subtract(&tc1), -25);
}
#[test]
fn test_display_ndf() {
let tc = TimecodeValue::new(1, 2, 3, 4, 25.0, false);
assert_eq!(tc.to_string(), "01:02:03:04");
}
#[test]
fn test_display_df() {
let tc = TimecodeValue::new(1, 2, 3, 4, 29.97, true);
assert_eq!(tc.to_string(), "01:02:03;04");
}
#[test]
fn test_parse_ndf() {
let tc = TimecodeValue::parse("01:02:03:04", 25.0).expect("valid timecode value");
assert_eq!(tc.hh, 1);
assert_eq!(tc.mm, 2);
assert_eq!(tc.ss, 3);
assert_eq!(tc.ff, 4);
assert!(!tc.drop_frame);
}
#[test]
fn test_parse_df() {
let tc = TimecodeValue::parse("01:02:03;04", 29.97).expect("valid timecode value");
assert_eq!(tc.hh, 1);
assert_eq!(tc.mm, 2);
assert_eq!(tc.ss, 3);
assert_eq!(tc.ff, 4);
assert!(tc.drop_frame);
}
#[test]
fn test_parse_invalid() {
assert!(TimecodeValue::parse("invalid", 25.0).is_none());
assert!(TimecodeValue::parse("25:00:00:00", 25.0).is_none()); assert!(TimecodeValue::parse("", 25.0).is_none());
}
#[test]
fn test_duration_from_timecode() {
let tc = TimecodeValue::new(0, 0, 1, 0, 25.0, false);
let secs = Duration::from_timecode(&tc);
assert!((secs - 1.0).abs() < 0.01);
}
#[test]
fn test_duration_to_timecode() {
let tc = Duration::to_timecode(1.0, 25.0, false);
assert_eq!(tc.ss, 1);
assert_eq!(tc.ff, 0);
}
#[test]
fn test_parse_display_roundtrip() {
let original = "01:30:45:12";
let tc = TimecodeValue::parse(original, 25.0).expect("valid timecode value");
assert_eq!(tc.to_string(), original);
}
}