pretty_duration/
lib.rs

1//! Pretty_duration takes a `Duration` and output a `String` in prettier way that make
2//! sense for a human being.
3//!
4//! Provide customization with a short and long format. There is an optional configuration
5//! to override terms for each time bins (year, month, day, hour, minute, second, millisecond)
6//! allowing people using the library from a different language to adapt the output [^note].
7//!
8//! [^note]: The library let you specify the singular and plural form providing flexibility for
9//! language that must have special rule. For example, the French word for month is `mois` with
10//! an ending end for the singular and plural form.
11mod pretty_duration_lib;
12use std::time::Duration;
13
14// Private because we do not want to expose outside the crate
15use crate::pretty_duration_lib::PrettyDurationOptionsWithDefault;
16
17// Public to expose these types to the consumer of the crate
18pub use crate::pretty_duration_lib::{
19    DurationBins, PrettyDurationLabels, PrettyDurationOptions, PrettyDurationOutputFormat,
20};
21const COMPACT_DEFAULT: PrettyDurationLabels = PrettyDurationLabels {
22    year: "y",
23    month: "mon",
24    day: "d",
25    hour: "h",
26    minute: "m",
27    second: "s",
28    millisecond: "ms",
29};
30const EXPANDED_SINGULAR_DEFAULT: PrettyDurationLabels = PrettyDurationLabels {
31    year: "year",
32    month: "month",
33    day: "day",
34    hour: "hour",
35    minute: "minute",
36    second: "second",
37    millisecond: "millisecond",
38};
39const EXPANDED_PLURAL_DEFAULT: PrettyDurationLabels = PrettyDurationLabels {
40    year: "years",
41    month: "months",
42    day: "days",
43    hour: "hours",
44    minute: "minutes",
45    second: "seconds",
46    millisecond: "milliseconds",
47};
48
49/// Main function of the pretty-duration library that takes a required [std::time::Duration] and
50/// and optional [PrettyDurationOptions].
51///
52/// # Arguments
53/// The first argument is a [std::time::Duration] that is the input to produce the [String] output.
54///
55/// The second argument is the optional configuration [PrettyDurationOptions] giving you the possibility
56/// to decide the format (extended or compact) but also to decide the symbol and word for each [PrettyDurationLabels]
57///
58/// # Default
59/// With an option set to [None], the function returns the duration in a compact format in US English symbol.
60///
61/// # Examples
62///
63/// ## No option outputs a string in a short format
64/// ```rust
65/// use std::time::Duration;
66/// use pretty_duration::pretty_duration;
67///    
68/// let result = pretty_duration(&Duration::from_millis(1), None);
69/// assert_eq!(result, "1ms");
70/// ```
71/// ## Option to have the extended long format
72/// ```rust
73/// use pretty_duration::pretty_duration;
74/// use pretty_duration::PrettyDurationLabels;
75/// use pretty_duration::PrettyDurationOptions;
76/// use pretty_duration::PrettyDurationOutputFormat;
77/// use std::time::Duration;
78/// let result = pretty_duration(
79///     &Duration::from_millis(43556556722),
80///     Some(PrettyDurationOptions {
81///         output_format: Some(PrettyDurationOutputFormat::Expanded),
82///         singular_labels: None,
83///         plural_labels: None,
84///     }),
85/// );
86/// assert_eq!(
87///     result,
88///     "1 year 16 months 248 days 3 hours 2 minutes 36 seconds 722 milliseconds"
89///);
90/// ```
91pub fn pretty_duration(duration: &Duration, options: Option<PrettyDurationOptions>) -> String {
92    let options_with_default = set_default_options(options);
93    let ms = duration.as_millis();
94    let duration_by_bin = extract_bins(&ms);
95
96    let mut result: Vec<String> = Vec::new();
97    let is_full_word = matches!(
98        options_with_default.output_format,
99        PrettyDurationOutputFormat::Expanded
100    );
101    try_adding(
102        &mut result,
103        duration_by_bin.years.to_string(),
104        &get_unit(
105            options_with_default.singular_labels.year,
106            options_with_default.plural_labels.year,
107            duration_by_bin.years > 1,
108        ),
109        is_full_word,
110    );
111    try_adding(
112        &mut result,
113        duration_by_bin.months.to_string(),
114        &get_unit(
115            options_with_default.singular_labels.month,
116            options_with_default.plural_labels.month,
117            duration_by_bin.months > 1,
118        ),
119        is_full_word,
120    );
121    try_adding(
122        &mut result,
123        duration_by_bin.days.to_string(),
124        &get_unit(
125            options_with_default.singular_labels.day,
126            options_with_default.plural_labels.day,
127            duration_by_bin.days > 1,
128        ),
129        is_full_word,
130    );
131    try_adding(
132        &mut result,
133        duration_by_bin.hours.to_string(),
134        &get_unit(
135            options_with_default.singular_labels.hour,
136            options_with_default.plural_labels.hour,
137            duration_by_bin.hours > 1,
138        ),
139        is_full_word,
140    );
141    try_adding(
142        &mut result,
143        duration_by_bin.minutes.to_string(),
144        &get_unit(
145            options_with_default.singular_labels.minute,
146            options_with_default.plural_labels.minute,
147            duration_by_bin.minutes > 1,
148        ),
149        is_full_word,
150    );
151    try_adding(
152        &mut result,
153        duration_by_bin.seconds.to_string(),
154        &get_unit(
155            options_with_default.singular_labels.second,
156            options_with_default.plural_labels.second,
157            duration_by_bin.seconds > 1,
158        ),
159        is_full_word,
160    );
161    try_adding(
162        &mut result,
163        duration_by_bin.milliseconds.to_string(),
164        &get_unit(
165            options_with_default.singular_labels.millisecond,
166            options_with_default.plural_labels.millisecond,
167            duration_by_bin.milliseconds > 1,
168        ),
169        is_full_word,
170    );
171
172    if result.len() == 0 {
173        let separator = match is_full_word {
174            true => " ",
175            false => "",
176        };
177        return format!(
178            "{}{}{}",
179            "0", separator, options_with_default.singular_labels.millisecond
180        );
181    }
182    return result.join(" ");
183}
184
185fn try_adding(result: &mut Vec<String>, value: String, unit: &str, is_full_word: bool) -> () {
186    let separator = match is_full_word {
187        true => " ",
188        false => "",
189    };
190    if value != "0" {
191        result.push(value + separator + unit);
192    }
193}
194
195fn get_unit(singular_string: &str, plural_string: &str, is_plural: bool) -> String {
196    return match is_plural {
197        true => plural_string.to_string(),
198        false => singular_string.to_string(),
199    };
200}
201
202fn set_default_options(
203    user_options: Option<PrettyDurationOptions>,
204) -> PrettyDurationOptionsWithDefault {
205    // Ensure if we the user passed nothing that we have a type with all options with no value
206    let user_options2 = user_options.unwrap_or_else(|| PrettyDurationOptions {
207        output_format: None,
208        singular_labels: None,
209        plural_labels: None,
210    });
211
212    let output_format = user_options2
213        .output_format
214        .unwrap_or_else(|| PrettyDurationOutputFormat::Compact);
215
216    // Give default value to all options not defined by the user
217    let default_options = PrettyDurationOptionsWithDefault {
218        output_format,
219        singular_labels: user_options2
220            .singular_labels
221            .unwrap_or_else(|| match output_format {
222                PrettyDurationOutputFormat::Compact => COMPACT_DEFAULT,
223                PrettyDurationOutputFormat::Expanded => EXPANDED_SINGULAR_DEFAULT,
224                PrettyDurationOutputFormat::Colon => EXPANDED_SINGULAR_DEFAULT,
225            }),
226        plural_labels: user_options2
227            .plural_labels
228            .unwrap_or_else(|| match output_format {
229                PrettyDurationOutputFormat::Compact => COMPACT_DEFAULT,
230                PrettyDurationOutputFormat::Expanded => EXPANDED_PLURAL_DEFAULT,
231                PrettyDurationOutputFormat::Colon => EXPANDED_PLURAL_DEFAULT,
232            }),
233    };
234
235    // Return all configurations with user first, then default value when not specified
236    return default_options;
237}
238
239/// Convert a millisecond number into bins of time
240fn extract_bins(ms: &u128) -> DurationBins {
241    return DurationBins {
242        years: (ms / 31556926000) as u16,
243        months: (ms / 2629800000) as u8,
244        days: (ms / 86400000) as u8,
245        hours: ((ms / 3600000) % 24) as u8,
246        minutes: ((ms / 60000) % 60) as u8,
247        seconds: ((ms / 1000) % 60) as u8,
248        milliseconds: ((ms) % 1000) as u16,
249    };
250}
251
252#[cfg(test)]
253mod test_get_unit {
254    use super::*;
255
256    #[test]
257    fn test_get_unit_left_side() {
258        let result = get_unit("unit1", "unit2", true);
259        assert_eq!(result, "unit2")
260    }
261
262    #[test]
263    fn test_get_unit_right_side() {
264        let result = get_unit("unit1", "unit2", false);
265        assert_eq!(result, "unit1")
266    }
267}
268#[cfg(test)]
269mod test_extract_bins {
270    use super::*;
271
272    #[test]
273    fn test_extract_bins_huge() {
274        let result = extract_bins(&31556956789);
275        assert_eq!(result.years, 1, "Year mismatch");
276        assert_eq!(result.months, 11, "Month mismatch");
277        assert_eq!(result.days, 109, "Day mismatch");
278        assert_eq!(result.hours, 5, "Hour mismatch");
279        assert_eq!(result.minutes, 49, "Minute mismatch");
280        assert_eq!(result.seconds, 16, "Second mismatch");
281        assert_eq!(result.milliseconds, 789, "Millisecond mismatch");
282    }
283    #[test]
284    fn test_extract_bins_zero() {
285        let result = extract_bins(&0);
286        assert_eq!(result.years, 0, "Year mismatch");
287        assert_eq!(result.months, 0, "Month mismatch");
288        assert_eq!(result.days, 0, "Day mismatch");
289        assert_eq!(result.hours, 0, "Hour mismatch");
290        assert_eq!(result.minutes, 0, "Minute mismatch");
291        assert_eq!(result.seconds, 0, "Second mismatch");
292        assert_eq!(result.milliseconds, 0, "Millisecond mismatch");
293    }
294}