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}