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}