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 let is_negative = normalized.seconds < 0 || normalized.nanos < 0;
15
16 if is_negative {
17 write!(f, "-")?;
18 }
19
20 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 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 #[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 let d = dur(10, 0);
106 assert_eq!(d.to_string(), "10s");
107
108 let d = dur(10, 500_000_000);
110 assert_eq!(d.to_string(), "10.5s");
111
112 let d = dur(0, 1_000);
114 assert_eq!(d.to_string(), "0.000001s");
115
116 let d = dur(-10, -500_000_000);
118 assert_eq!(d.to_string(), "-10.5s");
119
120 let d = dur(0, -500_000_000);
122 assert_eq!(d.to_string(), "-0.5s");
123 }
124
125 #[test]
128 fn test_unit_display_formatting() {
129 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 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 #[test]
156 fn test_get_data_basic_units() {
157 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 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 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 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 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 #[test]
218 fn test_duration_display_cases() {
219 let d = Duration {
221 seconds: 0,
222 nanos: 0,
223 };
224 assert_eq!(d.to_human_readable_string(), "0 seconds");
225
226 let d = Duration {
228 seconds: 10,
229 nanos: 0,
230 };
231 assert_eq!(d.to_human_readable_string(), "10 seconds");
232
233 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 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 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 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}