1#![allow(dead_code)]
8
9use crate::{FrameRate, Timecode, TimecodeError};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum TcOperation {
16 AddFrames(u64),
18 SubtractFrames(u64),
20 AddSeconds(u32),
22 SubtractSeconds(u32),
24}
25
26impl std::fmt::Display for TcOperation {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 match self {
29 Self::AddFrames(n) => write!(f, "+{n} frames"),
30 Self::SubtractFrames(n) => write!(f, "-{n} frames"),
31 Self::AddSeconds(s) => write!(f, "+{s} seconds"),
32 Self::SubtractSeconds(s) => write!(f, "-{s} seconds"),
33 }
34 }
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct TcResult {
42 pub timecode: Timecode,
44 pub wrapped: bool,
46 pub days_wrapped: u32,
48}
49
50impl TcResult {
51 pub fn tc(&self) -> &Timecode {
53 &self.timecode
54 }
55}
56
57#[derive(Debug, Clone, Copy)]
75pub struct TcCalculator {
76 frame_rate: FrameRate,
77}
78
79impl TcCalculator {
80 pub fn new(frame_rate: FrameRate) -> Self {
82 Self { frame_rate }
83 }
84
85 pub fn frames_per_day(&self) -> u64 {
87 let fps = self.frame_rate.frames_per_second() as u64;
88 if self.frame_rate.is_drop_frame() {
89 let total_minutes = 24u64 * 60;
92 let non_tenth_minutes = total_minutes - total_minutes / 10;
93 fps * 3600 * 24 - 2 * non_tenth_minutes
94 } else {
95 fps * 3600 * 24
96 }
97 }
98
99 pub fn apply(&self, tc: &Timecode, op: TcOperation) -> Result<TcResult, TimecodeError> {
101 let fpd = self.frames_per_day();
102 let current = tc.to_frames();
103
104 let (raw_target, wrapped, days_wrapped) = match op {
105 TcOperation::AddFrames(n) => {
106 let total = current + n;
107 let days = (total / fpd) as u32;
108 let pos = total % fpd;
109 (pos, days > 0, days)
110 }
111 TcOperation::SubtractFrames(n) => {
112 if n <= current {
113 (current - n, false, 0)
114 } else {
115 let deficit = n - current;
117 let days = deficit.div_ceil(fpd) as u32;
118 let pos = fpd - (deficit % fpd);
119 let pos = if pos == fpd { 0 } else { pos };
120 (pos, true, days)
121 }
122 }
123 TcOperation::AddSeconds(s) => {
124 let fps = self.frame_rate.frames_per_second() as u64;
125 self.apply(tc, TcOperation::AddFrames(s as u64 * fps))?;
126 return self.apply(tc, TcOperation::AddFrames(s as u64 * fps));
128 }
129 TcOperation::SubtractSeconds(s) => {
130 let fps = self.frame_rate.frames_per_second() as u64;
131 return self.apply(tc, TcOperation::SubtractFrames(s as u64 * fps));
132 }
133 };
134
135 let result_tc = Timecode::from_frames(raw_target, self.frame_rate)?;
136 Ok(TcResult {
137 timecode: result_tc,
138 wrapped,
139 days_wrapped,
140 })
141 }
142
143 pub fn difference(&self, a: &Timecode, b: &Timecode) -> i64 {
146 b.to_frames() as i64 - a.to_frames() as i64
147 }
148
149 pub fn max_tc<'a>(&self, a: &'a Timecode, b: &'a Timecode) -> &'a Timecode {
151 if a.to_frames() >= b.to_frames() {
152 a
153 } else {
154 b
155 }
156 }
157
158 pub fn min_tc<'a>(&self, a: &'a Timecode, b: &'a Timecode) -> &'a Timecode {
160 if a.to_frames() <= b.to_frames() {
161 a
162 } else {
163 b
164 }
165 }
166}
167
168#[cfg(test)]
171mod tests {
172 use super::*;
173
174 fn tc(h: u8, m: u8, s: u8, f: u8) -> Timecode {
175 Timecode::new(h, m, s, f, FrameRate::Fps25).expect("valid timecode")
176 }
177
178 fn calc() -> TcCalculator {
179 TcCalculator::new(FrameRate::Fps25)
180 }
181
182 #[test]
183 fn test_add_frames_basic() {
184 let result = calc()
185 .apply(&tc(0, 0, 0, 0), TcOperation::AddFrames(25))
186 .expect("should succeed");
187 assert_eq!(result.timecode.seconds, 1);
188 assert_eq!(result.timecode.frames, 0);
189 assert!(!result.wrapped);
190 }
191
192 #[test]
193 fn test_add_frames_wraps_hour() {
194 let result = calc()
195 .apply(&tc(23, 59, 59, 24), TcOperation::AddFrames(1))
196 .expect("should succeed");
197 assert_eq!(result.timecode.hours, 0);
199 assert!(result.wrapped);
200 assert_eq!(result.days_wrapped, 1);
201 }
202
203 #[test]
204 fn test_subtract_frames_basic() {
205 let result = calc()
206 .apply(&tc(0, 0, 2, 0), TcOperation::SubtractFrames(25))
207 .expect("should succeed");
208 assert_eq!(result.timecode.seconds, 1);
209 assert!(!result.wrapped);
210 }
211
212 #[test]
213 fn test_subtract_frames_wraps_backwards() {
214 let result = calc()
215 .apply(&tc(0, 0, 0, 0), TcOperation::SubtractFrames(25))
216 .expect("should succeed");
217 assert_eq!(result.timecode.hours, 23);
219 assert!(result.wrapped);
220 }
221
222 #[test]
223 fn test_add_seconds() {
224 let result = calc()
225 .apply(&tc(0, 0, 0, 0), TcOperation::AddSeconds(3))
226 .expect("should succeed");
227 assert_eq!(result.timecode.seconds, 3);
228 }
229
230 #[test]
231 fn test_subtract_seconds() {
232 let result = calc()
233 .apply(&tc(0, 0, 5, 0), TcOperation::SubtractSeconds(3))
234 .expect("should succeed");
235 assert_eq!(result.timecode.seconds, 2);
236 }
237
238 #[test]
239 fn test_difference_positive() {
240 let a = tc(0, 0, 0, 0);
241 let b = tc(0, 0, 1, 0);
242 assert_eq!(calc().difference(&a, &b), 25);
243 }
244
245 #[test]
246 fn test_difference_negative() {
247 let a = tc(0, 0, 1, 0);
248 let b = tc(0, 0, 0, 0);
249 assert_eq!(calc().difference(&a, &b), -25);
250 }
251
252 #[test]
253 fn test_difference_zero() {
254 let a = tc(1, 2, 3, 4);
255 let b = tc(1, 2, 3, 4);
256 assert_eq!(calc().difference(&a, &b), 0);
257 }
258
259 #[test]
260 fn test_max_tc() {
261 let a = tc(0, 0, 0, 0);
262 let b = tc(0, 0, 1, 0);
263 let m = calc().max_tc(&a, &b);
264 assert_eq!(m.seconds, 1);
265 }
266
267 #[test]
268 fn test_min_tc() {
269 let a = tc(0, 0, 0, 0);
270 let b = tc(0, 0, 1, 0);
271 let m = calc().min_tc(&a, &b);
272 assert_eq!(m.seconds, 0);
273 }
274
275 #[test]
276 fn test_frames_per_day_25fps() {
277 let c = TcCalculator::new(FrameRate::Fps25);
278 assert_eq!(c.frames_per_day(), 25 * 3600 * 24);
279 }
280
281 #[test]
282 fn test_frames_per_day_30fps() {
283 let c = TcCalculator::new(FrameRate::Fps30);
284 assert_eq!(c.frames_per_day(), 30 * 3600 * 24);
285 }
286
287 #[test]
288 fn test_tc_result_tc_accessor() {
289 let result = calc()
290 .apply(&tc(0, 0, 1, 0), TcOperation::AddFrames(0))
291 .expect("should succeed");
292 assert_eq!(result.tc().seconds, 1);
293 }
294
295 #[test]
296 fn test_operation_display() {
297 assert_eq!(TcOperation::AddFrames(10).to_string(), "+10 frames");
298 assert_eq!(TcOperation::SubtractSeconds(5).to_string(), "-5 seconds");
299 }
300
301 #[test]
302 fn test_add_zero_frames() {
303 let original = tc(1, 2, 3, 4);
304 let result = calc()
305 .apply(&original, TcOperation::AddFrames(0))
306 .expect("operation should succeed");
307 assert_eq!(result.timecode, original);
308 assert!(!result.wrapped);
309 }
310}