substudy/time.rs
1//! Tools for working with time.
2
3use std::result;
4
5use anyhow::anyhow;
6use serde::{ser::SerializeTuple, Serialize, Serializer};
7
8use crate::Result;
9
10/// The minimum spacing between two points in time to count as
11/// unambiguously different. This is related to the typical precision used
12/// in *.srt subtitle files.
13pub const MIN_SPACING: f32 = 0.001;
14
15// Break seconds down into hours, minutes and seconds.
16fn decompose_time(time: f32) -> (u32, u32, f32) {
17 let mut seconds = time;
18 let hours = (seconds / 3600.0).floor() as u32;
19 seconds %= 3600.0;
20 let mins = (seconds / 60.0).floor() as u32;
21 seconds %= 60.0;
22 (hours, mins, seconds)
23}
24
25/// Converts a time to a pretty, human-readable format, with second
26/// precision.
27///
28/// ```
29/// use substudy::time::seconds_to_hhmmss;
30/// assert_eq!("3:02:01", seconds_to_hhmmss(3.0*3600.0+2.0*60.0+1.001));
31/// ```
32pub fn seconds_to_hhmmss(time: f32) -> String {
33 let (hours, mins, seconds) = decompose_time(time);
34 format!("{}:{:02}:{:02}", hours, mins, (seconds.floor() as u32))
35}
36
37/// Converts a time to a pretty, human-readable format, with millisecond
38/// precision.
39///
40/// ```
41/// use substudy::time::seconds_to_hhmmss_sss;
42/// assert_eq!("3:02:01.001", seconds_to_hhmmss_sss(3.0*3600.0+2.0*60.0+1.001));
43/// ```
44pub fn seconds_to_hhmmss_sss(time: f32) -> String {
45 let (hours, mins, seconds) = decompose_time(time);
46 format!("{}:{:02}:{:06.3}", hours, mins, seconds)
47}
48
49/// A period of time, in seconds. The beginning is guaranteed to be less
50/// than the end, and all times are positive. This is lightweight
51/// structure which implements `Copy`, so it can be passed by value.
52///
53/// ```
54/// use substudy::time::Period;
55///
56/// let period = Period::new(1.0, 5.0).unwrap();
57/// assert_eq!(1.0, period.begin());
58/// assert_eq!(5.0, period.end());
59/// assert_eq!(4.0, period.duration());
60/// assert_eq!(3.0, period.midpoint());
61/// ```
62#[derive(Clone, Copy, Debug, PartialEq)]
63pub struct Period {
64 begin: f32,
65 end: f32,
66}
67
68impl Period {
69 /// Create a new time period.
70 pub fn new(begin: f32, end: f32) -> Result<Period> {
71 if begin < end && begin >= 0.0 && end >= 0.0 {
72 Ok(Period {
73 begin: begin,
74 end: end,
75 })
76 } else {
77 Err(anyhow!(
78 "Beginning of range is before end: {}-{}",
79 begin,
80 end
81 ))
82 }
83 }
84
85 /// Construct a time period from two optional time periods, taking the
86 /// union if both are present. This is normally used when working with
87 /// aligned subtitle pairs, either of which might be missing.
88 ///
89 /// ```
90 /// use substudy::time::Period;
91 ///
92 /// assert_eq!(None, Period::from_union_opt(None, None));
93 ///
94 /// let p1 = Period::new(1.0, 2.0).unwrap();
95 /// let p2 = Period::new(2.5, 3.0).unwrap();
96 /// assert_eq!(Some(p1),
97 /// Period::from_union_opt(Some(p1), None));
98 /// assert_eq!(Some(Period::new(1.0, 3.0).unwrap()),
99 /// Period::from_union_opt(Some(p1), Some(p2)));
100 /// ```
101 pub fn from_union_opt(p1: Option<Period>, p2: Option<Period>) -> Option<Period> {
102 match (p1, p2) {
103 (None, None) => None,
104 (Some(p), None) => Some(p),
105 (None, Some(p)) => Some(p),
106 (Some(p1), Some(p2)) => Some(p1.union(p2)),
107 }
108 }
109
110 /// The beginning of this time period.
111 pub fn begin(&self) -> f32 {
112 self.begin
113 }
114
115 /// The end of this time period.
116 pub fn end(&self) -> f32 {
117 self.end
118 }
119
120 /// How long this time period lasts.
121 pub fn duration(&self) -> f32 {
122 self.end - self.begin
123 }
124
125 /// The midpoint of this time period.
126 pub fn midpoint(&self) -> f32 {
127 self.begin + self.duration() / 2.0
128 }
129
130 /// Grow this time period by the specified amount, making any necessary
131 /// adjustments to keep it valid.
132 ///
133 /// ```
134 /// use substudy::time::Period;
135 ///
136 /// let period = Period::new(1.0, 5.0).unwrap();
137 /// assert_eq!(Period::new(0.0, 7.0).unwrap(),
138 /// period.grow(1.5, 2.0));
139 /// ```
140 pub fn grow(&self, before: f32, after: f32) -> Period {
141 let mid = self.midpoint();
142 Period {
143 begin: (self.begin - before).min(mid).max(0.0),
144 end: (self.end + after).max(mid + MIN_SPACING),
145 }
146 }
147
148 /// Calculate the smallest time period containing this time period and
149 /// another.
150 ///
151 /// ```
152 /// use substudy::time::Period;
153 ///
154 /// let p1 = Period::new(1.0, 2.0).unwrap();
155 /// let p2 = Period::new(2.5, 3.0).unwrap();
156 /// assert_eq!(Period::new(1.0, 3.0).unwrap(),
157 /// p1.union(p2));
158 /// ```
159 pub fn union(&self, other: Period) -> Period {
160 Period {
161 begin: self.begin.min(other.begin),
162 end: self.end.max(other.end),
163 }
164 }
165
166 /// Make sure this subtitle begins after `limit`.
167 pub fn begin_after(&mut self, limit: f32) -> Result<()> {
168 if limit > self.end - 2.0 * MIN_SPACING {
169 Err(anyhow!(
170 "Cannot begin time period {:?} after {}",
171 self,
172 limit
173 ))?;
174 }
175
176 self.begin = self.begin.max(limit + MIN_SPACING);
177 Ok(())
178 }
179
180 /// Truncate this subtitle before `limit`, which must be at least
181 /// `2*MIN_SPACING` greater than the begin time.
182 pub fn end_before(&mut self, limit: f32) -> Result<()> {
183 if limit < self.begin + 2.0 * MIN_SPACING {
184 Err(anyhow!(
185 "Cannot truncate time period {:?} at {}",
186 self,
187 limit
188 ))?;
189 }
190
191 self.end = self.end.min(limit - MIN_SPACING);
192 Ok(())
193 }
194
195 /// Return the absolute value of the distance between two durations, or
196 /// `None` if the durations overlap.
197 ///
198 /// ```
199 /// use substudy::time::Period;
200 ///
201 /// let p1 = Period::new(1.0, 2.0).unwrap();
202 /// let p2 = Period::new(2.0, 3.0).unwrap();
203 /// let p3 = Period::new(2.5, 3.0).unwrap();
204 /// assert_eq!(Some(0.5), p1.distance(p3));
205 /// assert_eq!(Some(0.5), p3.distance(p1));
206 /// assert_eq!(Some(0.0), p1.distance(p2));
207 /// assert_eq!(None, p2.distance(p3));
208 /// ```
209 pub fn distance(&self, other: Period) -> Option<f32> {
210 if self.end <= other.begin {
211 Some((other.begin - self.end).abs())
212 } else if other.end <= self.begin {
213 Some((self.begin - other.end).abs())
214 } else {
215 None
216 }
217 }
218
219 /// Return the total amount of time which appears in both durations.
220 ///
221 /// ```
222 /// use substudy::time::Period;
223 ///
224 /// let p1 = Period::new(1.0, 2.0).unwrap();
225 /// let p2 = Period::new(2.0, 3.0).unwrap();
226 /// let p3 = Period::new(2.5, 3.0).unwrap();
227 /// assert_eq!(0.0, p1.overlap(p3));
228 /// assert_eq!(0.0, p3.overlap(p1));
229 /// assert_eq!(0.0, p1.overlap(p2));
230 /// assert_eq!(0.5, p2.overlap(p3));
231 /// ```
232 pub fn overlap(&self, other: Period) -> f32 {
233 (self.end.min(other.end) - self.begin.max(other.begin)).max(0.0)
234 }
235}
236
237impl Serialize for Period {
238 fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
239 where
240 S: Serializer,
241 {
242 let mut tuple = serializer.serialize_tuple(2)?;
243 tuple.serialize_element(&self.begin)?;
244 tuple.serialize_element(&self.end)?;
245 tuple.end()
246 }
247}
248
249/// Convert a time to a timestamp string. This is mostly used for giving
250/// files semi-unique names so that we can dump files from multiple,
251/// related export runs into a single directory without too much chance of
252/// them overwriting each other unless they're basically the same file.
253pub trait ToTimestamp {
254 /// Convert to a string describing this time.
255 fn to_timestamp(&self) -> String;
256
257 /// Convert to a string describing this time, replacing periods with
258 /// "_".
259 fn to_file_timestamp(&self) -> String {
260 self.to_timestamp().replace(".", "_")
261 }
262}
263
264impl ToTimestamp for f32 {
265 fn to_timestamp(&self) -> String {
266 format!("{:09.3}", *self)
267 }
268}
269
270impl ToTimestamp for Period {
271 fn to_timestamp(&self) -> String {
272 format!("{:09.3}-{:09.3}", self.begin(), self.end())
273 }
274}
275
276#[test]
277fn test_timestamp() {
278 assert_eq!("00010.500", (10.5).to_timestamp());
279 let period = Period::new(10.0, 20.0).unwrap();
280 assert_eq!("00010.000-00020.000", period.to_timestamp());
281 assert_eq!("00010_000-00020_000", period.to_file_timestamp());
282}