1#![allow(dead_code)]
14#![allow(clippy::cast_possible_truncation)]
15
16use crate::Timecode;
17use std::fmt;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
21pub enum DisplayConvention {
22 Smpte,
24 Ebu,
26 Film,
28 FeetFrames35mm,
30 FeetFrames16mm,
32 Milliseconds,
34 FrameNumber,
36}
37
38#[derive(Debug, Clone)]
40pub struct DisplayOptions {
41 pub convention: DisplayConvention,
43 pub show_sign: bool,
45 pub zero_pad: bool,
47 pub custom_separator: Option<char>,
49}
50
51impl Default for DisplayOptions {
52 fn default() -> Self {
53 Self {
54 convention: DisplayConvention::Smpte,
55 show_sign: false,
56 zero_pad: true,
57 custom_separator: None,
58 }
59 }
60}
61
62impl DisplayOptions {
63 #[must_use]
65 pub fn smpte() -> Self {
66 Self {
67 convention: DisplayConvention::Smpte,
68 ..Self::default()
69 }
70 }
71
72 #[must_use]
74 pub fn ebu() -> Self {
75 Self {
76 convention: DisplayConvention::Ebu,
77 ..Self::default()
78 }
79 }
80
81 #[must_use]
83 pub fn film() -> Self {
84 Self {
85 convention: DisplayConvention::Film,
86 ..Self::default()
87 }
88 }
89
90 #[must_use]
92 pub fn milliseconds() -> Self {
93 Self {
94 convention: DisplayConvention::Milliseconds,
95 ..Self::default()
96 }
97 }
98
99 #[must_use]
101 pub fn frame_number() -> Self {
102 Self {
103 convention: DisplayConvention::FrameNumber,
104 ..Self::default()
105 }
106 }
107}
108
109#[derive(Debug, Clone)]
111pub struct FormattedTimecode {
112 pub text: String,
114 pub convention: DisplayConvention,
116}
117
118impl fmt::Display for FormattedTimecode {
119 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120 write!(f, "{}", self.text)
121 }
122}
123
124#[must_use]
137pub fn format_timecode(tc: &Timecode, opts: &DisplayOptions) -> FormattedTimecode {
138 let text = match opts.convention {
139 DisplayConvention::Smpte => format_smpte(tc, opts),
140 DisplayConvention::Ebu => format_ebu(tc, opts),
141 DisplayConvention::Film => format_film(tc, opts),
142 DisplayConvention::FeetFrames35mm => format_feet_frames(tc, 16),
143 DisplayConvention::FeetFrames16mm => format_feet_frames(tc, 40),
144 DisplayConvention::Milliseconds => format_milliseconds(tc),
145 DisplayConvention::FrameNumber => format_frame_number(tc),
146 };
147 FormattedTimecode {
148 text,
149 convention: opts.convention,
150 }
151}
152
153fn format_smpte(tc: &Timecode, _opts: &DisplayOptions) -> String {
155 let sep = if tc.frame_rate.drop_frame { ';' } else { ':' };
156 format!(
157 "{:02}:{:02}:{:02}{}{:02}",
158 tc.hours, tc.minutes, tc.seconds, sep, tc.frames
159 )
160}
161
162fn format_ebu(tc: &Timecode, _opts: &DisplayOptions) -> String {
164 format!(
165 "{:02}:{:02}:{:02}:{:02}",
166 tc.hours, tc.minutes, tc.seconds, tc.frames
167 )
168}
169
170fn format_film(tc: &Timecode, _opts: &DisplayOptions) -> String {
172 format!(
173 "{:02}+{:02}:{:02}:{:02}",
174 tc.hours, tc.minutes, tc.seconds, tc.frames
175 )
176}
177
178fn format_feet_frames(tc: &Timecode, frames_per_foot: u64) -> String {
182 let total_frames = tc.to_frames();
183 let feet = total_frames / frames_per_foot;
184 let leftover = total_frames % frames_per_foot;
185 format!("{feet:04}+{leftover:02}")
186}
187
188fn format_milliseconds(tc: &Timecode) -> String {
190 let fps = crate::frame_rate_from_info(&tc.frame_rate).as_float();
191 let ms = if fps > 0.0 {
192 ((tc.frames as f64 / fps) * 1000.0).round() as u32
193 } else {
194 0
195 };
196 format!(
197 "{:02}:{:02}:{:02}.{:03}",
198 tc.hours,
199 tc.minutes,
200 tc.seconds,
201 ms.min(999)
202 )
203}
204
205fn format_frame_number(tc: &Timecode) -> String {
207 format!("{}", tc.to_frames())
208}
209
210pub fn parse_display(
218 s: &str,
219 frame_rate: crate::FrameRate,
220) -> Result<Timecode, crate::TimecodeError> {
221 Timecode::from_string(s, frame_rate).or_else(|_| {
223 let normalized = s.replacen('+', ":", 1);
225 Timecode::from_string(&normalized, frame_rate)
226 })
227}
228
229#[derive(Debug, Clone)]
231pub struct ConventionComparison {
232 pub timecode: Timecode,
234 pub smpte: String,
236 pub ebu: String,
238 pub film: String,
240 pub ms: String,
242 pub frame: String,
244}
245
246impl ConventionComparison {
247 #[must_use]
249 pub fn build(tc: Timecode) -> Self {
250 let smpte = format_timecode(&tc, &DisplayOptions::smpte()).text;
251 let ebu = format_timecode(&tc, &DisplayOptions::ebu()).text;
252 let film = format_timecode(&tc, &DisplayOptions::film()).text;
253 let ms = format_timecode(&tc, &DisplayOptions::milliseconds()).text;
254 let frame = format_timecode(&tc, &DisplayOptions::frame_number()).text;
255 Self {
256 timecode: tc,
257 smpte,
258 ebu,
259 film,
260 ms,
261 frame,
262 }
263 }
264
265 #[must_use]
267 pub fn to_table_row(&self) -> String {
268 format!(
269 "| {:>13} | {:>13} | {:>13} | {:>12} | {:>8} |",
270 self.smpte, self.ebu, self.film, self.ms, self.frame
271 )
272 }
273}
274
275#[must_use]
277pub fn comparison_table_header() -> String {
278 format!(
279 "| {:>13} | {:>13} | {:>13} | {:>12} | {:>8} |",
280 "SMPTE", "EBU", "Film", "Milliseconds", "Frames"
281 )
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287 use crate::FrameRate;
288
289 fn tc25(h: u8, m: u8, s: u8, f: u8) -> Timecode {
290 Timecode::new(h, m, s, f, FrameRate::Fps25).expect("valid tc")
291 }
292
293 #[test]
294 fn test_smpte_ndf() {
295 let tc = tc25(1, 30, 0, 12);
296 let fmt = format_timecode(&tc, &DisplayOptions::smpte());
297 assert_eq!(fmt.text, "01:30:00:12");
298 }
299
300 #[test]
301 fn test_ebu_always_colon() {
302 let tc = tc25(0, 0, 0, 0);
303 let fmt = format_timecode(&tc, &DisplayOptions::ebu());
304 assert_eq!(fmt.text, "00:00:00:00");
305 }
306
307 #[test]
308 fn test_film_format() {
309 let tc = tc25(2, 15, 30, 5);
310 let fmt = format_timecode(&tc, &DisplayOptions::film());
311 assert_eq!(fmt.text, "02+15:30:05");
312 }
313
314 #[test]
315 fn test_milliseconds_format() {
316 let tc = tc25(0, 0, 0, 12);
318 let fmt = format_timecode(&tc, &DisplayOptions::milliseconds());
319 assert_eq!(fmt.text, "00:00:00.480");
320 }
321
322 #[test]
323 fn test_frame_number() {
324 let tc = tc25(1, 0, 0, 0);
326 let fmt = format_timecode(&tc, &DisplayOptions::frame_number());
327 assert_eq!(fmt.text, "90000");
328 }
329
330 #[test]
331 fn test_feet_frames_35mm() {
332 let tc = tc25(0, 0, 1, 7); let fmt = format_timecode(
335 &tc,
336 &DisplayOptions {
337 convention: DisplayConvention::FeetFrames35mm,
338 ..DisplayOptions::default()
339 },
340 );
341 assert!(!fmt.text.is_empty());
342 }
343
344 #[test]
345 fn test_comparison_build() {
346 let tc = tc25(1, 0, 0, 0);
347 let comp = ConventionComparison::build(tc);
348 assert!(!comp.smpte.is_empty());
349 assert!(!comp.ebu.is_empty());
350 }
351
352 #[test]
353 fn test_parse_display_smpte() {
354 let parsed = parse_display("01:30:00:12", FrameRate::Fps25).expect("parse ok");
355 assert_eq!(parsed.hours, 1);
356 assert_eq!(parsed.minutes, 30);
357 assert_eq!(parsed.seconds, 0);
358 assert_eq!(parsed.frames, 12);
359 }
360}