movement/
lib.rs

1//! A simple library that helps with time calculations. The most common use case would
2//! be to calculate the end time of a watch given the start time and the time span.
3//! Internally, the library uses [`i64`] to represent time in seconds. From there, you can
4//! convert the time to a string in either 12h or 24h format by using the [`fmt::Display`]
5//! trait.
6//! 
7//! # Usage
8//! 
9//! Initialize a watch with the `start time` and `meridiem` option as the arguments. The start time 
10//! can be in either 12h or 24h format. The meridiem option is a [`bool`] that represents whether 
11//! the watch is in 12h or 24h format (true for 12h). The default display format is `HH:MM:SS` 
12//! and +/- days.
13//!
14//! # Examples
15//!
16//! ```
17//! use movement::Watch;
18//!
19//! let mut watch = Watch::new("13:34", true);
20//! watch += "01:23:45";
21//! watch += 43434343;
22//! println!("{}", watch);
23//! // 08:03:28 AM +503 days
24//! ```
25//!
26//! ```
27//! use movement::Watch;
28//!
29//! let mut watch = Watch::new("01:34 AM", false);
30//! watch += "01:23:45";
31//! watch -= 1000000;
32//! println!("{}", watch);
33//! // 13:11:05 -12 days
34//! ```
35//!
36//! ```
37//! use movement::Watch;
38//!
39//! let mut watch = Watch::new("13:34", true);
40//! let new_watch = watch + "01:23:45";
41//! println!("{}", new_watch);
42//! // 02:57:45 PM
43//! ```
44
45use std::{
46    fmt,
47    ops::{Add, AddAssign, Sub, SubAssign},
48};
49
50/// A struct that represents a watch which keeps track of the start time, offset, and meridiem.
51/// Can be addded and subtracted with i64 and &str. i64 will be treated as seconds and &str will be
52/// treated as a time string (e.g. "01:23:45"). The default display format is `HH:MM:SS` and +/- days.
53/// 
54/// ### Note on i64
55/// Using i64 to represent time in seconds has a maximum time span of several hundred billion years.
56/// This is more than enough for most use cases. There is a few microseconds added to computation time and doubling
57/// of memory usage compared to using i32. However, the tradeoff is worth it in my opinion as i32 has a max of around
58/// 68 years.
59#[derive(Debug, Clone, Copy, Default)]
60pub struct Watch {
61    /// the starting time of the watch. This won't change over the course of the program
62    pub start: i64,
63
64    /// the offset of the watch or the time span that will be added to the start time
65    pub offset: i64,
66
67    /// whether the watch is in 12h or 24h format (true for 12h)
68    pub meridiem: bool,
69}
70
71impl Watch {
72    /// create a new watch with the given time and meridiem option
73    pub fn new(time: &str, meridiem: bool) -> Self {
74        let secs = Watch::str_to_secs(time, true);
75        Watch {
76            start: secs,
77            meridiem,
78            ..Default::default()
79        }
80    }
81
82    /// take a time string (e.g. "01:23:45 AM") and return the number of seconds
83    pub fn str_to_secs(time: &str, is_time_span: bool) -> i64 {
84        let pm = {
85            let time = time.replace('.', "").to_uppercase();
86            time.contains("PM") && is_time_span
87        };
88        let mut time = time.split(' ').next().unwrap_or("").split(':');
89        let mut hours = time.next().unwrap_or("").parse::<i64>().unwrap_or(0);
90        let minutes = time.next().unwrap_or("").parse::<i64>().unwrap_or(0);
91        let seconds = time.next().unwrap_or("").parse::<i64>().unwrap_or(0);
92
93        if pm {
94            hours += 12;
95        }
96        hours * 3600 + minutes * 60 + seconds
97    }
98
99    /// convert secs to string (HH:MM:SS format)
100    pub fn secs_to_mil(secs: i64) -> String {
101        let hours = secs / 3600 % 24;
102        let minutes = (secs % 3600) / 60;
103        let seconds = secs % 60;
104        format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
105    }
106
107    /// convert secs to string (12h format)
108    pub fn secs_to_mer(secs: i64) -> String {
109        let mut hours = secs / 3600 % 24;
110        let minutes = (secs % 3600) / 60;
111        let seconds = secs % 60;
112
113        let meridiem = if hours >= 12 {
114            hours %= 12;
115            "PM"
116        } else {
117            "AM"
118        };
119
120        if hours == 0 {
121            hours = 12;
122        }
123
124        format!("{:02}:{:02}:{:02} {}", hours, minutes, seconds, meridiem)
125    }
126
127    /// convert diff seconds to num of days later or before
128    pub fn diff_to_days(diff: i64) -> String {
129        let days = diff / 86400;
130        match days.cmp(&0) {
131            std::cmp::Ordering::Greater => format!(" +{} days", days),
132            std::cmp::Ordering::Less => format!(" -{} days", days.abs()),
133            std::cmp::Ordering::Equal => "".to_string(),
134        }
135    }
136
137    /// return the end time of the watch
138    pub fn add_offset(&self) -> i64 {
139        self.start + self.offset
140    }
141
142    /// change the meridiem option
143    pub fn change_meridiem(&mut self, meridiem: bool) {
144        self.meridiem = meridiem;
145    }
146}
147
148// Operations
149impl Add<i64> for Watch {
150    type Output = Watch;
151
152    fn add(self, rhs: i64) -> Self::Output {
153        Watch {
154            offset: self.offset + rhs,
155            ..self
156        }
157    }
158}
159
160impl Sub<i64> for Watch {
161    type Output = Watch;
162
163    fn sub(self, rhs: i64) -> Self::Output {
164        Watch {
165            offset: self.offset - rhs,
166            ..self
167        }
168    }
169}
170
171impl Add<&str> for Watch {
172    type Output = Watch;
173
174    fn add(self, rhs: &str) -> Self::Output {
175        let secs = Watch::str_to_secs(rhs, false);
176        self + secs
177    }
178}
179
180impl Sub<&str> for Watch {
181    type Output = Watch;
182
183    fn sub(self, rhs: &str) -> Self::Output {
184        let secs = Watch::str_to_secs(rhs, false);
185        self - secs
186    }
187}
188
189// Custom trait that will be implemented by i64 and &str
190trait AddableToWatch {}
191
192// Implementing the trait for i64 and &str
193impl AddableToWatch for i64 {}
194impl AddableToWatch for &str {}
195
196impl<T: AddableToWatch + Copy> AddAssign<T> for Watch
197where
198    Watch: Add<T, Output = Watch>,
199{
200    fn add_assign(&mut self, other: T) {
201        *self = *self + other;
202    }
203}
204
205impl<T: AddableToWatch + Copy> SubAssign<T> for Watch
206where
207    Watch: Sub<T, Output = Watch>,
208{
209    fn sub_assign(&mut self, other: T) {
210        *self = *self - other;
211    }
212}
213
214// Display and Formatting
215impl fmt::Display for Watch {
216    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
217        let end = (self.add_offset() % 86400 + 86400) % 86400;
218
219        let diff = self.offset + self.start - end;
220
221        let end_str = if self.meridiem {
222            Watch::secs_to_mer(end)
223        } else {
224            Watch::secs_to_mil(end)
225        };
226
227        let diff_str = Watch::diff_to_days(diff);
228
229        write!(f, "{}{}", end_str, diff_str)
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use crate::Watch;
236
237    #[test]
238    fn basic_adding() {
239        let mut watch = Watch::new("13:33:23", false);
240        watch += "0:23:03";
241        assert_eq!(format!("{}", watch), "13:56:26");
242
243        watch += 1000;
244        assert_eq!(format!("{}", watch), "14:13:06");
245    }
246
247    #[test]
248    fn basic_subtracting() {
249        let mut watch = Watch::new("13:33:23", false);
250        watch -= "0:23:03";
251        assert_eq!(format!("{}", watch), "13:10:20");
252
253        watch -= 1000;
254        assert_eq!(format!("{}", watch), "12:53:40");
255    }
256
257    #[test]
258    fn basic_meridiem_adding() {
259        let mut watch = Watch::new("01:33:23 PM", true);
260        watch += "0:23:03";
261        assert_eq!(format!("{}", watch), "01:56:26 PM");
262
263        watch += 1000;
264        assert_eq!(format!("{}", watch), "02:13:06 PM");
265    }
266
267    #[test]
268    fn basic_meridiem_subtracting() {
269        let mut watch = Watch::new("13:33:23", true);
270        watch -= "0:23:03";
271        assert_eq!(format!("{}", watch), "01:10:20 PM");
272
273        watch -= 1000;
274        assert_eq!(format!("{}", watch), "12:53:40 PM");
275    }
276
277    #[test]
278    fn day_overflow_adding() {
279        let mut watch = Watch::new("13:33:23", false);
280        watch += "23:44:03";
281        assert_eq!(format!("{}", watch), "13:17:26 +1 days");
282
283        watch += 7989;
284        assert_eq!(format!("{}", watch), "15:30:35 +1 days");
285    }
286
287    #[test]
288    fn day_overflow_subtracting() {
289        let mut watch = Watch::new("13:33:23", false);
290        watch -= "23:44:03";
291        assert_eq!(format!("{}", watch), "13:49:20 -1 days");
292
293        watch -= 7989;
294        assert_eq!(format!("{}", watch), "11:36:11 -1 days");
295    }
296
297    #[test]
298    fn day_overflow_meridiem_adding() {
299        let mut watch = Watch::new("01:33:23 PM", true);
300        watch += "23:44:03";
301        assert_eq!(format!("{}", watch), "01:17:26 PM +1 days");
302
303        watch += 7989;
304        assert_eq!(format!("{}", watch), "03:30:35 PM +1 days");
305    }
306
307    #[test]
308    fn day_overflow_meridiem_subtracting() {
309        let mut watch = Watch::new("13:33:23", true);
310        watch -= "23:44:03";
311        assert_eq!(format!("{}", watch), "01:49:20 PM -1 days");
312
313        watch -= 7989;
314        assert_eq!(format!("{}", watch), "11:36:11 AM -1 days");
315    }
316
317    #[test]
318    fn large_subtraction() {
319        let mut watch = Watch::new("13:34", true);
320        watch -= 100000000;
321        assert_eq!(format!("{}", watch), "03:47:20 AM -1157 days");
322    }
323
324    #[test]
325    fn changing_meridiem() {
326        let mut watch = Watch::new("13:34", true);
327        watch -= 100000000;
328        assert_eq!(format!("{}", watch), "03:47:20 AM -1157 days");
329
330        watch.change_meridiem(false);
331        watch += "13:23:03";
332        assert_eq!(format!("{}", watch), "17:10:23 -1157 days");
333    }
334}