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