1#![allow(dead_code)]
8
9use crate::{FrameRate, Timecode, TimecodeError};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
18pub struct TcDuration {
19 pub frames: u64,
21 pub fps: u8,
23}
24
25impl TcDuration {
26 pub fn from_frames(frames: u64, fps: u8) -> Self {
28 Self { frames, fps }
29 }
30
31 pub fn from_hmsf(hours: u32, minutes: u32, seconds: u32, frames: u32, fps: u8) -> Self {
33 let total = hours as u64 * 3600 * fps as u64
34 + minutes as u64 * 60 * fps as u64
35 + seconds as u64 * fps as u64
36 + frames as u64;
37 Self { frames: total, fps }
38 }
39
40 pub fn to_hmsf(&self) -> (u32, u32, u32, u32) {
42 let fps = self.fps as u64;
43 let total = self.frames;
44 let hours = (total / (fps * 3600)) as u32;
45 let rem = total % (fps * 3600);
46 let minutes = (rem / (fps * 60)) as u32;
47 let rem = rem % (fps * 60);
48 let seconds = (rem / fps) as u32;
49 let frames = (rem % fps) as u32;
50 (hours, minutes, seconds, frames)
51 }
52
53 #[allow(clippy::cast_precision_loss)]
55 pub fn as_seconds(&self) -> f64 {
56 self.frames as f64 / self.fps as f64
57 }
58
59 pub fn multiply(&self, factor: u64) -> Self {
61 Self {
62 frames: self.frames * factor,
63 fps: self.fps,
64 }
65 }
66
67 pub fn divide(&self, divisor: u64) -> Option<Self> {
70 if divisor == 0 {
71 return None;
72 }
73 Some(Self {
74 frames: self.frames / divisor,
75 fps: self.fps,
76 })
77 }
78
79 #[allow(clippy::cast_precision_loss)]
81 #[allow(clippy::cast_possible_truncation)]
82 #[allow(clippy::cast_sign_loss)]
83 pub fn scale(&self, factor: f64) -> Self {
84 let scaled = (self.frames as f64 * factor).round() as u64;
85 Self {
86 frames: scaled,
87 fps: self.fps,
88 }
89 }
90
91 pub fn midpoint(&self) -> Self {
93 Self {
94 frames: self.frames / 2,
95 fps: self.fps,
96 }
97 }
98
99 pub fn add(&self, other: &TcDuration) -> Self {
101 Self {
102 frames: self.frames + other.frames,
103 fps: self.fps,
104 }
105 }
106
107 pub fn subtract(&self, other: &TcDuration) -> Self {
109 Self {
110 frames: self.frames.saturating_sub(other.frames),
111 fps: self.fps,
112 }
113 }
114
115 pub fn is_zero(&self) -> bool {
117 self.frames == 0
118 }
119}
120
121impl std::fmt::Display for TcDuration {
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 let (h, m, s, fr) = self.to_hmsf();
124 write!(f, "{h:02}:{m:02}:{s:02}:{fr:02}")
125 }
126}
127
128pub struct TcMath;
141
142impl TcMath {
143 pub fn duration_between(a: &Timecode, b: &Timecode) -> TcDuration {
145 let fa = a.to_frames();
146 let fb = b.to_frames();
147 let diff = fa.abs_diff(fb);
148 TcDuration::from_frames(diff, a.frame_rate.fps)
149 }
150
151 pub fn midpoint(
153 a: &Timecode,
154 b: &Timecode,
155 rate: FrameRate,
156 ) -> Result<Timecode, TimecodeError> {
157 let fa = a.to_frames();
158 let fb = b.to_frames();
159 let mid = (fa + fb) / 2;
160 Timecode::from_frames(mid, rate)
161 }
162
163 #[allow(clippy::cast_precision_loss)]
165 #[allow(clippy::cast_possible_truncation)]
166 #[allow(clippy::cast_sign_loss)]
167 pub fn offset_by_percentage(
168 tc: &Timecode,
169 duration: &TcDuration,
170 pct: f64,
171 rate: FrameRate,
172 ) -> Result<Timecode, TimecodeError> {
173 let offset_frames = (duration.frames as f64 * pct / 100.0).round() as u64;
174 let target = tc.to_frames() + offset_frames;
175 Timecode::from_frames(target, rate)
176 }
177
178 pub fn midpoint_between_durations(a: &TcDuration, b: &TcDuration) -> TcDuration {
180 TcDuration::from_frames((a.frames + b.frames) / 2, a.fps)
181 }
182
183 #[allow(clippy::cast_precision_loss)]
185 pub fn position_percentage(tc: &Timecode, start: &Timecode, end: &Timecode) -> f64 {
186 let pos = tc.to_frames();
187 let s = start.to_frames();
188 let e = end.to_frames();
189 if e <= s {
190 return 0.0;
191 }
192 ((pos - s) as f64 / (e - s) as f64) * 100.0
193 }
194
195 #[allow(clippy::cast_precision_loss)]
197 pub fn rate_conversion_factor(from: FrameRate, to: FrameRate) -> f64 {
198 to.as_float() / from.as_float()
199 }
200
201 #[allow(clippy::cast_precision_loss)]
203 #[allow(clippy::cast_possible_truncation)]
204 #[allow(clippy::cast_sign_loss)]
205 pub fn convert_frame_count(frames: u64, from: FrameRate, to: FrameRate) -> u64 {
206 let factor = Self::rate_conversion_factor(from, to);
207 (frames as f64 * factor).round() as u64
208 }
209}
210
211#[cfg(test)]
214mod tests {
215 use super::*;
216
217 fn tc25(h: u8, m: u8, s: u8, f: u8) -> Timecode {
218 Timecode::new(h, m, s, f, FrameRate::Fps25).unwrap()
219 }
220
221 #[test]
222 fn test_duration_from_hmsf() {
223 let d = TcDuration::from_hmsf(1, 0, 0, 0, 25);
224 assert_eq!(d.frames, 90000); }
226
227 #[test]
228 fn test_duration_to_hmsf() {
229 let d = TcDuration::from_frames(90000, 25);
230 let (h, m, s, f) = d.to_hmsf();
231 assert_eq!((h, m, s, f), (1, 0, 0, 0));
232 }
233
234 #[test]
235 fn test_duration_as_seconds() {
236 let d = TcDuration::from_frames(50, 25);
237 let secs = d.as_seconds();
238 assert!((secs - 2.0).abs() < 1e-6);
239 }
240
241 #[test]
242 fn test_duration_multiply() {
243 let d = TcDuration::from_frames(100, 25);
244 let result = d.multiply(3);
245 assert_eq!(result.frames, 300);
246 }
247
248 #[test]
249 fn test_duration_divide() {
250 let d = TcDuration::from_frames(300, 25);
251 let result = d.divide(3).unwrap();
252 assert_eq!(result.frames, 100);
253 }
254
255 #[test]
256 fn test_duration_divide_by_zero() {
257 let d = TcDuration::from_frames(100, 25);
258 assert!(d.divide(0).is_none());
259 }
260
261 #[test]
262 fn test_duration_scale() {
263 let d = TcDuration::from_frames(100, 25);
264 let result = d.scale(1.5);
265 assert_eq!(result.frames, 150);
266 }
267
268 #[test]
269 fn test_duration_midpoint() {
270 let d = TcDuration::from_frames(200, 25);
271 assert_eq!(d.midpoint().frames, 100);
272 }
273
274 #[test]
275 fn test_duration_add_subtract() {
276 let a = TcDuration::from_frames(100, 25);
277 let b = TcDuration::from_frames(50, 25);
278 assert_eq!(a.add(&b).frames, 150);
279 assert_eq!(a.subtract(&b).frames, 50);
280 assert_eq!(b.subtract(&a).frames, 0); }
282
283 #[test]
284 fn test_duration_display() {
285 let d = TcDuration::from_hmsf(1, 2, 3, 4, 25);
286 assert_eq!(d.to_string(), "01:02:03:04");
287 }
288
289 #[test]
290 fn test_math_duration_between() {
291 let a = tc25(0, 0, 0, 0);
292 let b = tc25(0, 0, 2, 0);
293 let dur = TcMath::duration_between(&a, &b);
294 assert_eq!(dur.frames, 50);
295 }
296
297 #[test]
298 fn test_math_midpoint() {
299 let a = tc25(0, 0, 0, 0);
300 let b = tc25(0, 0, 4, 0);
301 let mid = TcMath::midpoint(&a, &b, FrameRate::Fps25).unwrap();
302 assert_eq!(mid.seconds, 2);
303 assert_eq!(mid.frames, 0);
304 }
305
306 #[test]
307 fn test_math_offset_by_percentage() {
308 let tc = tc25(0, 0, 0, 0);
309 let dur = TcDuration::from_frames(100, 25);
310 let result = TcMath::offset_by_percentage(&tc, &dur, 50.0, FrameRate::Fps25).unwrap();
311 assert_eq!(result.to_frames(), 50);
312 }
313
314 #[test]
315 fn test_math_position_percentage() {
316 let start = tc25(0, 0, 0, 0);
317 let end = tc25(0, 0, 4, 0); let pos = tc25(0, 0, 2, 0); let pct = TcMath::position_percentage(&pos, &start, &end);
320 assert!((pct - 50.0).abs() < 1e-6);
321 }
322
323 #[test]
324 fn test_math_rate_conversion_factor() {
325 let factor = TcMath::rate_conversion_factor(FrameRate::Fps25, FrameRate::Fps50);
326 assert!((factor - 2.0).abs() < 1e-6);
327 }
328
329 #[test]
330 fn test_math_convert_frame_count() {
331 let result = TcMath::convert_frame_count(100, FrameRate::Fps25, FrameRate::Fps50);
332 assert_eq!(result, 200);
333 }
334
335 #[test]
336 fn test_duration_is_zero() {
337 assert!(TcDuration::from_frames(0, 25).is_zero());
338 assert!(!TcDuration::from_frames(1, 25).is_zero());
339 }
340}