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}