#![allow(dead_code)]
use crate::{FrameRate, Timecode, TimecodeError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TcOperation {
AddFrames(u64),
SubtractFrames(u64),
AddSeconds(u32),
SubtractSeconds(u32),
}
impl std::fmt::Display for TcOperation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AddFrames(n) => write!(f, "+{n} frames"),
Self::SubtractFrames(n) => write!(f, "-{n} frames"),
Self::AddSeconds(s) => write!(f, "+{s} seconds"),
Self::SubtractSeconds(s) => write!(f, "-{s} seconds"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TcResult {
pub timecode: Timecode,
pub wrapped: bool,
pub days_wrapped: u32,
}
impl TcResult {
pub fn tc(&self) -> &Timecode {
&self.timecode
}
}
#[derive(Debug, Clone, Copy)]
pub struct TcCalculator {
frame_rate: FrameRate,
}
impl TcCalculator {
pub fn new(frame_rate: FrameRate) -> Self {
Self { frame_rate }
}
pub fn frames_per_day(&self) -> u64 {
let fps = self.frame_rate.frames_per_second() as u64;
if self.frame_rate.is_drop_frame() {
let total_minutes = 24u64 * 60;
let non_tenth_minutes = total_minutes - total_minutes / 10;
fps * 3600 * 24 - 2 * non_tenth_minutes
} else {
fps * 3600 * 24
}
}
pub fn apply(&self, tc: &Timecode, op: TcOperation) -> Result<TcResult, TimecodeError> {
let fpd = self.frames_per_day();
let current = tc.to_frames();
let (raw_target, wrapped, days_wrapped) = match op {
TcOperation::AddFrames(n) => {
let total = current + n;
let days = (total / fpd) as u32;
let pos = total % fpd;
(pos, days > 0, days)
}
TcOperation::SubtractFrames(n) => {
if n <= current {
(current - n, false, 0)
} else {
let deficit = n - current;
let days = deficit.div_ceil(fpd) as u32;
let pos = fpd - (deficit % fpd);
let pos = if pos == fpd { 0 } else { pos };
(pos, true, days)
}
}
TcOperation::AddSeconds(s) => {
let fps = self.frame_rate.frames_per_second() as u64;
self.apply(tc, TcOperation::AddFrames(s as u64 * fps))?;
return self.apply(tc, TcOperation::AddFrames(s as u64 * fps));
}
TcOperation::SubtractSeconds(s) => {
let fps = self.frame_rate.frames_per_second() as u64;
return self.apply(tc, TcOperation::SubtractFrames(s as u64 * fps));
}
};
let result_tc = Timecode::from_frames(raw_target, self.frame_rate)?;
Ok(TcResult {
timecode: result_tc,
wrapped,
days_wrapped,
})
}
pub fn difference(&self, a: &Timecode, b: &Timecode) -> i64 {
b.to_frames() as i64 - a.to_frames() as i64
}
pub fn max_tc<'a>(&self, a: &'a Timecode, b: &'a Timecode) -> &'a Timecode {
if a.to_frames() >= b.to_frames() {
a
} else {
b
}
}
pub fn min_tc<'a>(&self, a: &'a Timecode, b: &'a Timecode) -> &'a Timecode {
if a.to_frames() <= b.to_frames() {
a
} else {
b
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tc(h: u8, m: u8, s: u8, f: u8) -> Timecode {
Timecode::new(h, m, s, f, FrameRate::Fps25).expect("valid timecode")
}
fn calc() -> TcCalculator {
TcCalculator::new(FrameRate::Fps25)
}
#[test]
fn test_add_frames_basic() {
let result = calc()
.apply(&tc(0, 0, 0, 0), TcOperation::AddFrames(25))
.expect("should succeed");
assert_eq!(result.timecode.seconds, 1);
assert_eq!(result.timecode.frames, 0);
assert!(!result.wrapped);
}
#[test]
fn test_add_frames_wraps_hour() {
let result = calc()
.apply(&tc(23, 59, 59, 24), TcOperation::AddFrames(1))
.expect("should succeed");
assert_eq!(result.timecode.hours, 0);
assert!(result.wrapped);
assert_eq!(result.days_wrapped, 1);
}
#[test]
fn test_subtract_frames_basic() {
let result = calc()
.apply(&tc(0, 0, 2, 0), TcOperation::SubtractFrames(25))
.expect("should succeed");
assert_eq!(result.timecode.seconds, 1);
assert!(!result.wrapped);
}
#[test]
fn test_subtract_frames_wraps_backwards() {
let result = calc()
.apply(&tc(0, 0, 0, 0), TcOperation::SubtractFrames(25))
.expect("should succeed");
assert_eq!(result.timecode.hours, 23);
assert!(result.wrapped);
}
#[test]
fn test_add_seconds() {
let result = calc()
.apply(&tc(0, 0, 0, 0), TcOperation::AddSeconds(3))
.expect("should succeed");
assert_eq!(result.timecode.seconds, 3);
}
#[test]
fn test_subtract_seconds() {
let result = calc()
.apply(&tc(0, 0, 5, 0), TcOperation::SubtractSeconds(3))
.expect("should succeed");
assert_eq!(result.timecode.seconds, 2);
}
#[test]
fn test_difference_positive() {
let a = tc(0, 0, 0, 0);
let b = tc(0, 0, 1, 0);
assert_eq!(calc().difference(&a, &b), 25);
}
#[test]
fn test_difference_negative() {
let a = tc(0, 0, 1, 0);
let b = tc(0, 0, 0, 0);
assert_eq!(calc().difference(&a, &b), -25);
}
#[test]
fn test_difference_zero() {
let a = tc(1, 2, 3, 4);
let b = tc(1, 2, 3, 4);
assert_eq!(calc().difference(&a, &b), 0);
}
#[test]
fn test_max_tc() {
let a = tc(0, 0, 0, 0);
let b = tc(0, 0, 1, 0);
let m = calc().max_tc(&a, &b);
assert_eq!(m.seconds, 1);
}
#[test]
fn test_min_tc() {
let a = tc(0, 0, 0, 0);
let b = tc(0, 0, 1, 0);
let m = calc().min_tc(&a, &b);
assert_eq!(m.seconds, 0);
}
#[test]
fn test_frames_per_day_25fps() {
let c = TcCalculator::new(FrameRate::Fps25);
assert_eq!(c.frames_per_day(), 25 * 3600 * 24);
}
#[test]
fn test_frames_per_day_30fps() {
let c = TcCalculator::new(FrameRate::Fps30);
assert_eq!(c.frames_per_day(), 30 * 3600 * 24);
}
#[test]
fn test_tc_result_tc_accessor() {
let result = calc()
.apply(&tc(0, 0, 1, 0), TcOperation::AddFrames(0))
.expect("should succeed");
assert_eq!(result.tc().seconds, 1);
}
#[test]
fn test_operation_display() {
assert_eq!(TcOperation::AddFrames(10).to_string(), "+10 frames");
assert_eq!(TcOperation::SubtractSeconds(5).to_string(), "-5 seconds");
}
#[test]
fn test_add_zero_frames() {
let original = tc(1, 2, 3, 4);
let result = calc()
.apply(&original, TcOperation::AddFrames(0))
.expect("operation should succeed");
assert_eq!(result.timecode, original);
assert!(!result.wrapped);
}
}