Skip to main content

proto_types/duration/
formatting.rs

1#![allow(clippy::option_map_unit_fn)]
2use alloc::string::String;
3use core::fmt::Write;
4
5use super::data::DurationData;
6use crate::{Duration, Vec};
7
8impl core::fmt::Display for Duration {
9	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
10		let normalized = self.normalized();
11
12		// 1. Handle Negative case
13		// Note: If seconds is -0 and nanos is -500, we need to print "-0.000000500s"
14		let is_negative = normalized.seconds < 0 || normalized.nanos < 0;
15
16		if is_negative {
17			write!(f, "-")?;
18		}
19
20		// Work with absolute values for printing
21		let abs_seconds = normalized.seconds.abs();
22		let mut abs_nanos = normalized.nanos.abs();
23
24		write!(f, "{abs_seconds}")?;
25
26		if abs_nanos > 0 {
27			let mut width = 9;
28
29			// Strip trailing zeros mathematically
30			// e.g. 500_000_000 (width 9) -> 5 (width 1) -> prints ".5"
31			// e.g. 000_000_500 (width 9) -> 5 (width 7) -> prints ".0000005"
32			while abs_nanos % 10 == 0 {
33				abs_nanos /= 10;
34				width -= 1;
35			}
36
37			write!(f, ".{abs_nanos:0width$}")?;
38		}
39
40		write!(f, "s")
41	}
42}
43
44impl Duration {
45	/// Formats a duration in human readable form. (e.g. "2 days 15 hours 12 minutes and 15 seconds")
46	#[must_use]
47	pub fn to_human_readable_string(&self) -> String {
48		let DurationData {
49			months,
50			days,
51			hours,
52			minutes,
53			seconds,
54			is_negative,
55			..
56		} = self.get_data();
57
58		let mut str = String::new();
59
60		let mut parts = Vec::new();
61
62		months.format_if_nonzero().map(|p| parts.push(p));
63		days.format_if_nonzero().map(|p| parts.push(p));
64		hours.format_if_nonzero().map(|p| parts.push(p));
65		minutes.format_if_nonzero().map(|p| parts.push(p));
66		seconds.format_if_nonzero().map(|p| parts.push(p));
67
68		if parts.is_empty() {
69			str.push_str("0 seconds");
70		} else {
71			let sign = if is_negative { "- " } else { "" };
72
73			match parts.len() {
74				1 => str.push_str(&parts.remove(0)),
75				2 => {
76					let _ = write!(str, "{}{} and {}", sign, parts[0], parts[1]);
77				}
78				_ => {
79					let last = parts.pop().unwrap();
80					let _ = write!(str, "{}{} and {}", sign, parts.join(" "), last);
81				}
82			};
83		}
84
85		str
86	}
87}
88
89#[cfg(test)]
90mod tests {
91	use super::*;
92	use crate::duration::duration_units::*;
93	use alloc::string::ToString;
94
95	fn dur(s: i64, n: i32) -> Duration {
96		Duration {
97			seconds: s,
98			nanos: n,
99		}
100	}
101
102	#[test]
103	fn test_canonical_display() {
104		// Simple
105		let d = dur(10, 0);
106		assert_eq!(d.to_string(), "10s");
107
108		// Fractional
109		let d = dur(10, 500_000_000);
110		assert_eq!(d.to_string(), "10.5s");
111
112		// Small Fractional
113		let d = dur(0, 1_000);
114		assert_eq!(d.to_string(), "0.000001s");
115
116		// Negative
117		let d = dur(-10, -500_000_000);
118		assert_eq!(d.to_string(), "-10.5s");
119
120		// Negative Zero
121		let d = dur(0, -500_000_000);
122		assert_eq!(d.to_string(), "-0.5s");
123	}
124
125	// --- 1. Unit Formatter Tests ---
126
127	#[test]
128	fn test_unit_display_formatting() {
129		// Singular
130		assert_eq!(Seconds { value: 1 }.to_string(), "1 second");
131		assert_eq!(Minutes { value: 1 }.to_string(), "1 minute");
132		assert_eq!(Hours { value: 1 }.to_string(), "1 hour");
133		assert_eq!(Days { value: 1 }.to_string(), "1 day");
134		assert_eq!(Weeks { value: 1 }.to_string(), "1 week");
135		assert_eq!(Months { value: 1 }.to_string(), "1 month");
136		assert_eq!(Years { value: 1 }.to_string(), "1 year");
137
138		// Plural
139		assert_eq!(Seconds { value: 2 }.to_string(), "2 seconds");
140		assert_eq!(Seconds { value: 0 }.to_string(), "0 seconds");
141		assert_eq!(Years { value: 10 }.to_string(), "10 years");
142	}
143
144	#[test]
145	fn test_format_if_nonzero() {
146		let s_zero = Seconds { value: 0 };
147		let s_one = Seconds { value: 1 };
148
149		assert_eq!(s_zero.format_if_nonzero(), None);
150		assert_eq!(s_one.format_if_nonzero(), Some("1 second".to_string()));
151	}
152
153	// --- 2. get_data Decomposition Tests ---
154
155	#[test]
156	fn test_get_data_basic_units() {
157		// 1 Minute
158		let d = Duration {
159			seconds: 60,
160			nanos: 0,
161		};
162		let data = d.get_data();
163		assert_eq!(data.minutes.value, 1);
164		assert_eq!(data.seconds.value, 0);
165
166		// 1 Hour
167		let d = Duration {
168			seconds: 3600,
169			nanos: 0,
170		};
171		let data = d.get_data();
172		assert_eq!(data.hours.value, 1);
173		assert_eq!(data.minutes.value, 0);
174
175		// 1 Day
176		let d = Duration {
177			seconds: 86400,
178			nanos: 0,
179		};
180		let data = d.get_data();
181		assert_eq!(data.days.value, 1);
182		assert_eq!(data.hours.value, 0);
183	}
184
185	#[test]
186	fn test_get_data_greedy_decomposition() {
187		// 1 Day, 1 Hour, 1 Minute, 1 Second
188		let total_seconds = 86400 + 3600 + 60 + 1;
189		let d = Duration {
190			seconds: total_seconds,
191			nanos: 0,
192		};
193
194		let data = d.get_data();
195		assert_eq!(data.days.value, 1);
196		assert_eq!(data.hours.value, 1);
197		assert_eq!(data.minutes.value, 1);
198		assert_eq!(data.seconds.value, 1);
199	}
200
201	#[test]
202	fn test_get_data_negative() {
203		// -65 seconds -> 1 minute, 5 seconds (negative flag set)
204		let d = Duration {
205			seconds: -65,
206			nanos: 0,
207		};
208
209		let data = d.get_data();
210		assert!(data.is_negative);
211		assert_eq!(data.minutes.value, 1);
212		assert_eq!(data.seconds.value, 5);
213	}
214
215	// --- 3. Duration Display Tests ---
216
217	#[test]
218	fn test_duration_display_cases() {
219		// Case 0: Zero
220		let d = Duration {
221			seconds: 0,
222			nanos: 0,
223		};
224		assert_eq!(d.to_human_readable_string(), "0 seconds");
225
226		// Case 1: Single unit
227		let d = Duration {
228			seconds: 10,
229			nanos: 0,
230		};
231		assert_eq!(d.to_human_readable_string(), "10 seconds");
232
233		// Case 2: Two units (use "and")
234		// 1 minute (60) + 30 seconds
235		let d = Duration {
236			seconds: 90,
237			nanos: 0,
238		};
239		assert_eq!(d.to_human_readable_string(), "1 minute and 30 seconds");
240
241		// Case 3: Three+ units (spaces and "and" at the end)
242		// 1 hour (3600) + 1 minute (60) + 1 second
243		let d = Duration {
244			seconds: 3661,
245			nanos: 0,
246		};
247		assert_eq!(d.to_human_readable_string(), "1 hour 1 minute and 1 second");
248
249		// Case 4: Skipping zero units
250		// 1 hour (3600) + 5 seconds (no minutes)
251		let d = Duration {
252			seconds: 3605,
253			nanos: 0,
254		};
255		assert_eq!(d.to_human_readable_string(), "1 hour and 5 seconds");
256	}
257
258	#[test]
259	fn test_duration_display_negative() {
260		// - 1 minute and 30 seconds
261		let d = Duration {
262			seconds: -90,
263			nanos: 0,
264		};
265		assert_eq!(d.to_human_readable_string(), "- 1 minute and 30 seconds");
266	}
267}