simple_duration/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2
3//! # simple_duration
4//!
5//! `simple_duration` is a crate that provides a "simple and minimal dependency" second-precision Duration type for Rust.
6//! It's optimized for everyday "hours, minutes, seconds" handling and embedded environments (no_std).
7//!
8//! ## Features
9//!
10//! - **Simple time representation in seconds**: Specialized for use cases that don't require high precision like milliseconds or nanoseconds
11//! - **Intuitive creation and formatting**: Easy creation from hours/minutes/seconds and conversion to `"hh:mm:ss"` format strings
12//! - **String parsing support**: Can create Duration objects from `"hh:mm:ss"` format strings
13//! - **Addition and subtraction operations**: Duration objects can be added and subtracted (results never become negative)
14//! - **SystemTime integration**: Can create Duration from two `SystemTime` instances (when `std` feature is enabled)
15//! - **no_std support & minimal dependencies**: Safe to use in embedded projects or projects that want to minimize dependencies
16//! - **Safe error handling**: Failures like string parsing return explicit errors via Option/Result without panicking
17//!
18//! ## Usage Examples
19//!
20//! ```rust
21//! use simple_duration::Duration;
22//!
23//! // Create from hours, minutes, seconds
24//! let duration = Duration::from_hms(1, 30, 45); // 1 hour 30 minutes 45 seconds
25//!
26//! // Create from hours
27//! let duration = Duration::from_hours(2); // 2 hours
28//!
29//! // Create from minutes
30//! let duration = Duration::from_minutes(90); // 90 minutes (1 hour 30 minutes)
31//!
32//! // Create from seconds
33//! let duration = Duration::from_seconds(3661); // 1 hour 1 minute 1 second
34//!
35//! // Create from string
36//! let duration = Duration::parse("01:30:45").unwrap();
37//!
38//! // Format
39//! assert_eq!(duration.format(), "01:30:45");
40//!
41//! // Get total amounts in each unit
42//! assert_eq!(duration.as_seconds(), 5445);
43//! assert_eq!(duration.as_minutes(), 90); // 90 minutes
44//! assert_eq!(duration.as_hours(), 1); // 1 hour (truncated)
45//!
46//! // Get each component (in h:m:s format)
47//! assert_eq!(duration.seconds_part(), 45); // seconds component (0-59)
48//! assert_eq!(duration.minutes_part(), 30);   // minutes component (0-59)
49//! assert_eq!(duration.hours_part(), 1);      // hours component
50//!
51//! // Arithmetic operations
52//! let d1 = Duration::from_seconds(100);
53//! let d2 = Duration::from_seconds(50);
54//! let sum = d1 + d2; // 150 seconds
55//! let diff = d1 - d2; // 50 seconds
56//! ```
57
58#[cfg(feature = "std")]
59use std::time::SystemTime;
60
61#[cfg(not(feature = "std"))]
62extern crate alloc;
63
64#[cfg(not(feature = "std"))]
65use alloc::{string::String, format};
66
67use core::ops::{Add, Sub};
68
69/// Simple Duration type with second precision
70///
71/// This struct provides time representation in seconds, optimized for hours/minutes/seconds handling.
72#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
73pub struct Duration {
74    seconds: u64,
75}
76
77/// Possible errors that can occur during Duration operations
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum DurationError {
80    /// Invalid string format
81    InvalidFormat,
82    /// Invalid value for hours, minutes, or seconds
83    InvalidValue,
84}
85
86impl Duration {
87    /// Create a new Duration from seconds
88    ///
89    /// # Examples
90    ///
91    /// ```rust
92    /// use simple_duration::Duration;
93    ///
94    /// let duration = Duration::from_seconds(3661);
95    /// assert_eq!(duration.hours_part(), 1);
96    /// assert_eq!(duration.minutes_part(), 1);
97    /// assert_eq!(duration.seconds_part(), 1);
98    /// ```
99    pub fn from_seconds(seconds: u64) -> Self {
100        Self { seconds }
101    }
102
103    /// Create Duration from minutes
104    ///
105    /// # Examples
106    ///
107    /// ```rust
108    /// use simple_duration::Duration;
109    ///
110    /// let duration = Duration::from_minutes(90);
111    /// assert_eq!(duration.as_seconds(), 5400);
112    /// assert_eq!(duration.hours_part(), 1);
113    /// assert_eq!(duration.minutes_part(), 30);
114    /// assert_eq!(duration.seconds_part(), 0);
115    /// ```
116    pub fn from_minutes(minutes: u64) -> Self {
117        Self {
118            seconds: minutes * 60,
119        }
120    }
121
122    /// Create Duration from hours
123    ///
124    /// # Examples
125    ///
126    /// ```rust
127    /// use simple_duration::Duration;
128    ///
129    /// let duration = Duration::from_hours(2);
130    /// assert_eq!(duration.as_seconds(), 7200);
131    /// assert_eq!(duration.hours_part(), 2);
132    /// assert_eq!(duration.minutes_part(), 0);
133    /// assert_eq!(duration.seconds_part(), 0);
134    /// ```
135    pub fn from_hours(hours: u64) -> Self {
136        Self {
137            seconds: hours * 3600,
138        }
139    }
140
141    /// Create Duration from hours, minutes, and seconds
142    ///
143    /// # Examples
144    ///
145    /// ```rust
146    /// use simple_duration::Duration;
147    ///
148    /// let duration = Duration::from_hms(1, 30, 45);
149    /// assert_eq!(duration.as_seconds(), 5445);
150    /// ```
151    pub fn from_hms(hours: u64, minutes: u64, seconds: u64) -> Self {
152        Self {
153            seconds: hours * 3600 + minutes * 60 + seconds,
154        }
155    }
156
157    /// Parse Duration from "hh:mm:ss" format string
158    ///
159    /// # Examples
160    ///
161    /// ```rust
162    /// use simple_duration::Duration;
163    ///
164    /// let duration = Duration::parse("01:30:45").unwrap();
165    /// assert_eq!(duration.hours_part(), 1);
166    /// assert_eq!(duration.minutes_part(), 30);
167    /// assert_eq!(duration.seconds_part(), 45);
168    ///
169    /// assert!(Duration::parse("invalid").is_err());
170    /// ```
171    pub fn parse(s: &str) -> Result<Self, DurationError> {
172        let parts: Vec<&str> = s.split(':').collect();
173        if parts.len() != 3 {
174            return Err(DurationError::InvalidFormat);
175        }
176
177        let hours = parts[0].parse::<u64>().map_err(|_| DurationError::InvalidValue)?;
178        let minutes = parts[1].parse::<u64>().map_err(|_| DurationError::InvalidValue)?;
179        let seconds = parts[2].parse::<u64>().map_err(|_| DurationError::InvalidValue)?;
180
181        if minutes >= 60 || seconds >= 60 {
182            return Err(DurationError::InvalidValue);
183        }
184
185        Ok(Self::from_hms(hours, minutes, seconds))
186    }
187
188    /// Get total seconds
189    ///
190    /// # Examples
191    ///
192    /// ```rust
193    /// use simple_duration::Duration;
194    ///
195    /// let duration = Duration::from_seconds(3661);
196    /// assert_eq!(duration.as_seconds(), 3661);
197    /// ```
198    pub fn as_seconds(&self) -> u64 {
199        self.seconds
200    }
201
202    /// Get total minutes (truncated)
203    ///
204    /// # Examples
205    ///
206    /// ```rust
207    /// use simple_duration::Duration;
208    ///
209    /// let duration = Duration::from_seconds(150); // 2 minutes 30 seconds
210    /// assert_eq!(duration.as_minutes(), 2);
211    ///
212    /// let duration = Duration::from_seconds(3661); // 1 hour 1 minute 1 second
213    /// assert_eq!(duration.as_minutes(), 61);
214    /// ```
215    pub fn as_minutes(&self) -> u64 {
216        self.seconds / 60
217    }
218
219    /// Get total hours (truncated)
220    ///
221    /// # Examples
222    ///
223    /// ```rust
224    /// use simple_duration::Duration;
225    ///
226    /// let duration = Duration::from_seconds(3661); // 1 hour 1 minute 1 second
227    /// assert_eq!(duration.as_hours(), 1);
228    ///
229    /// let duration = Duration::from_seconds(7200); // 2 hours
230    /// assert_eq!(duration.as_hours(), 2);
231    /// ```
232    pub fn as_hours(&self) -> u64 {
233        self.seconds / 3600
234    }
235
236    /// Get seconds component (0-59)
237    ///
238    /// # Examples
239    ///
240    /// ```rust
241    /// use simple_duration::Duration;
242    ///
243    /// let duration = Duration::from_seconds(3661); // 1 hour 1 minute 1 second
244    /// assert_eq!(duration.seconds_part(), 1);
245    ///
246    /// let duration = Duration::from_seconds(150); // 2 minutes 30 seconds
247    /// assert_eq!(duration.seconds_part(), 30);
248    /// ```
249    pub fn seconds_part(&self) -> u64 {
250        self.seconds % 60
251    }
252
253    /// Get minutes component (0-59)
254    ///
255    /// # Examples
256    ///
257    /// ```rust
258    /// use simple_duration::Duration;
259    ///
260    /// let duration = Duration::from_seconds(3661); // 1 hour 1 minute 1 second
261    /// assert_eq!(duration.minutes_part(), 1);
262    ///
263    /// let duration = Duration::from_seconds(150); // 2 minutes 30 seconds
264    /// assert_eq!(duration.minutes_part(), 2);
265    /// ```
266    pub fn minutes_part(&self) -> u64 {
267        (self.seconds % 3600) / 60
268    }
269
270    /// Get hours component (0-∞)
271    ///
272    /// # Examples
273    ///
274    /// ```rust
275    /// use simple_duration::Duration;
276    ///
277    /// let duration = Duration::from_seconds(3661); // 1 hour 1 minute 1 second
278    /// assert_eq!(duration.hours_part(), 1);
279    ///
280    /// let duration = Duration::from_seconds(7200); // 2 hours
281    /// assert_eq!(duration.hours_part(), 2);
282    /// ```
283    pub fn hours_part(&self) -> u64 {
284        self.seconds / 3600
285    }
286
287    /// Format as "hh:mm:ss" string
288    ///
289    /// # Examples
290    ///
291    /// ```rust
292    /// use simple_duration::Duration;
293    ///
294    /// let duration = Duration::from_hms(1, 5, 30);
295    /// assert_eq!(duration.format(), "01:05:30");
296    /// ```
297    pub fn format(&self) -> String {
298        format!("{:02}:{:02}:{:02}", self.hours_part(), self.minutes_part(), self.seconds_part())
299    }
300
301    /// Create a zero Duration
302    pub fn zero() -> Self {
303        Self { seconds: 0 }
304    }
305
306    /// Check if this Duration is zero
307    pub fn is_zero(&self) -> bool {
308        self.seconds == 0
309    }
310
311    /// Saturating addition (prevents overflow)
312    pub fn saturating_add(self, other: Self) -> Self {
313        Self {
314            seconds: self.seconds.saturating_add(other.seconds),
315        }
316    }
317
318    /// Saturating subtraction (prevents underflow)
319    pub fn saturating_sub(self, other: Self) -> Self {
320        Self {
321            seconds: self.seconds.saturating_sub(other.seconds),
322        }
323    }
324}
325
326/// SystemTime conversion (only when std feature is enabled)
327#[cfg(feature = "std")]
328impl Duration {
329    /// Create Duration from the time difference between two SystemTimes
330    ///
331    /// # Examples
332    ///
333    /// ```rust,no_run
334    /// use simple_duration::Duration;
335    /// use std::time::SystemTime;
336    ///
337    /// let start = SystemTime::now();
338    /// // Some processing...
339    /// let end = SystemTime::now();
340    ///
341    /// if let Some(duration) = Duration::from_system_time_diff(start, end) {
342    ///     println!("Elapsed time: {}", duration.format());
343    /// }
344    /// ```
345    pub fn from_system_time_diff(start: SystemTime, end: SystemTime) -> Option<Self> {
346        end.duration_since(start)
347            .ok()
348            .map(|std_duration| Self::from_seconds(std_duration.as_secs()))
349    }
350}
351
352impl Add for Duration {
353    type Output = Self;
354
355    fn add(self, other: Self) -> Self::Output {
356        self.saturating_add(other)
357    }
358}
359
360impl Sub for Duration {
361    type Output = Self;
362
363    fn sub(self, other: Self) -> Self::Output {
364        self.saturating_sub(other)
365    }
366}
367
368impl core::fmt::Display for Duration {
369    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
370        write!(f, "{}", self.format())
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377
378    #[test]
379    fn test_constructors() {
380        // Creation from seconds
381        let d1 = Duration::from_seconds(3661); // 1 hour 1 minute 1 second
382        assert_eq!(d1.as_seconds(), 3661);
383        assert_eq!(d1.seconds_part(), 1);
384        assert_eq!(d1.minutes_part(), 1);
385        assert_eq!(d1.hours_part(), 1);
386
387        // Creation from minutes
388        let d2 = Duration::from_minutes(150); // 2 hours 30 minutes
389        assert_eq!(d2.as_seconds(), 9000);
390        assert_eq!(d2.format(), "02:30:00");
391
392        // Creation from hours
393        let d3 = Duration::from_hours(3);
394        assert_eq!(d3.as_seconds(), 10800);
395        assert_eq!(d3.format(), "03:00:00");
396
397        // Creation from hours, minutes, seconds
398        let d4 = Duration::from_hms(2, 30, 45);
399        assert_eq!(d4.as_seconds(), 9045);
400        assert_eq!(d4.format(), "02:30:45");
401    }
402
403    #[test]
404    fn test_unit_conversions() {
405        let duration = Duration::from_seconds(3661); // 1 hour 1 minute 1 second
406        
407        // Get total amount in each unit
408        assert_eq!(duration.as_seconds(), 3661);
409        assert_eq!(duration.as_minutes(), 61); // 61 minutes
410        assert_eq!(duration.as_hours(), 1); // 1 hour (truncated)
411        
412        // Get each component
413        assert_eq!(duration.seconds_part(), 1);
414        assert_eq!(duration.minutes_part(), 1);
415        assert_eq!(duration.hours_part(), 1);
416
417        // More complex example
418        let duration2 = Duration::from_seconds(7890); // 2 hours 11 minutes 30 seconds
419        assert_eq!(duration2.as_minutes(), 131); // 131 minutes
420        assert_eq!(duration2.as_hours(), 2); // 2 hours
421        assert_eq!(duration2.seconds_part(), 30);
422        assert_eq!(duration2.minutes_part(), 11);
423        assert_eq!(duration2.hours_part(), 2);
424    }
425
426    #[test]
427    fn test_string_parsing() {
428        // Normal parsing - boundary value test
429        let duration = Duration::parse("23:59:59").unwrap(); // Maximum valid h:m:s
430        assert_eq!(duration.seconds_part(), 59);
431        assert_eq!(duration.minutes_part(), 59);
432        assert_eq!(duration.hours_part(), 23);
433
434        // Minimum value test
435        let duration_min = Duration::parse("00:00:00").unwrap();
436        assert_eq!(duration_min.seconds_part(), 0);
437        assert_eq!(duration_min.minutes_part(), 0);
438        assert_eq!(duration_min.hours_part(), 0);
439
440        // Abnormal parsing - cases exceeding boundary values
441        assert!(Duration::parse("invalid").is_err());
442        assert!(Duration::parse("1:2").is_err()); // Invalid format
443        assert!(Duration::parse("1:60:30").is_err()); // Minutes is 60 (exceeds boundary)
444        assert!(Duration::parse("1:30:60").is_err()); // Seconds is 60 (exceeds boundary)
445        assert!(Duration::parse("24:59:59").is_ok()); // 24 hours is valid (crosses day)
446        assert!(Duration::parse("00:59:59").is_ok()); // Within boundary
447        assert!(Duration::parse("00:00:59").is_ok()); // Within boundary
448    }
449
450    #[test]
451    fn test_formatting() {
452        let cases = [
453            (Duration::from_hms(1, 5, 30), "01:05:30"),
454            (Duration::from_hms(12, 0, 0), "12:00:00"),
455            (Duration::zero(), "00:00:00"),
456        ];
457
458        for (duration, expected) in cases {
459            assert_eq!(duration.format(), expected);
460            assert_eq!(format!("{}", duration), expected);
461        }
462    }
463
464    #[test]
465    fn test_arithmetic() {
466        let d1 = Duration::from_seconds(100);
467        let d2 = Duration::from_seconds(50);
468        
469        // Normal addition
470        assert_eq!((d1 + d2).as_seconds(), 150);
471        
472        // Normal subtraction
473        assert_eq!((d1 - d2).as_seconds(), 50);
474        
475        // Underflow (saturating subtraction)
476        assert_eq!((d2 - d1).as_seconds(), 0);
477        
478        // Overflow (saturating addition) test
479        let max_duration = Duration::from_seconds(u64::MAX);
480        let small_duration = Duration::from_seconds(1);
481        
482        // Adding 1 to u64::MAX should remain u64::MAX (saturated)
483        assert_eq!((max_duration + small_duration).as_seconds(), u64::MAX);
484        
485        // Addition of large values should also saturate
486        let large1 = Duration::from_seconds(u64::MAX - 10);
487        let large2 = Duration::from_seconds(20);
488        assert_eq!((large1 + large2).as_seconds(), u64::MAX);
489        
490        // Boundary test just before overflow (no overflow case)
491        let near_max = Duration::from_seconds(u64::MAX - 50);
492        let small = Duration::from_seconds(30);
493        assert_eq!((near_max + small).as_seconds(), u64::MAX - 20);
494    }
495
496    #[test]
497    fn test_utility_methods() {
498        let zero = Duration::zero();
499        assert!(zero.is_zero());
500        assert_eq!(zero.as_seconds(), 0);
501
502        let non_zero = Duration::from_seconds(1);
503        assert!(!non_zero.is_zero());
504    }
505
506    #[test]
507    fn test_comparison() {
508        let d1 = Duration::from_seconds(100);
509        let d2 = Duration::from_seconds(200);
510        
511        assert!(d1 < d2);
512        assert!(d2 > d1);
513        assert_eq!(d1, Duration::from_seconds(100));
514        assert_ne!(d1, d2);
515    }
516
517    #[cfg(feature = "std")]
518    #[test]
519    fn test_system_time_integration() {
520        use std::time::SystemTime;
521        
522        let start = SystemTime::now();
523        let end = start + std::time::Duration::from_secs(100);
524        
525        let duration = Duration::from_system_time_diff(start, end).unwrap();
526        assert_eq!(duration.as_seconds(), 100);
527    }
528}