htime 0.0.3

Format durations in human-readable form
Documentation
//! `htime` is a crate to provide convenient human-centric time formatting.
//!
//! # Examples
//!
//! Given a `Duration`, generate a legible human output:
//!
//! ```
//! // One year minus 1 millisecond (demonstrates all the units).
//! let dur = std::time::Duration::from_millis(31_557_600_000 - 1);
//!
//! // Prints the output as a nicely formatted string, with units in descending
//! // order of size. Specify the degree of unit precision desired as the second
//! // argument.
//! assert_eq!(
//!     htime::pretty_print(&dur, "ms", ", "),
//!     "11mo, 4w, 2d, 10h, 29m, 59s, 999ms",
//! )
//! ```
//!
//! Alternatively, you can just get back the list of value/unit strings, and
//! arrange them yourself:
//!
//! ```
//! # let dur = std::time::Duration::from_millis(31_557_600_000 - 1);
//!
//! // Returns a list of the units, with no whitespace or separators.
//! assert_eq!(
//!     htime::components(&dur, "ms"),
//!     vec!["11mo", "4w", "2d", "10h", "29m", "59s", "999ms"],
//! )
//! ```
use std::time::Duration;

/// One year, in milliseconds.
const YEAR: u128 = 31_557_600_000;

/// One month, in milliseconds.
const MONTH: u128 = 2_629_800_000;

/// One week, in milliseconds.
const WEEK: u128 = 604_800_000;

/// One day, in milliseconds.
const DAY: u128 = 86_400_000;

/// One day, in milliseconds.
const HOUR: u128 = 3_600_000;

/// One day, in milliseconds.
const MINUTE: u128 = 60_000;

/// One day, in milliseconds.
const SECOND: u128 = 1_000;

/// One day, in milliseconds.
const MILLISECOND: u128 = 1;

/// All the units we'll go through to build the human string.
const UNITS: [(u128, &str); 8] = [
    (YEAR, "y"),
    (MONTH, "mo"),
    (WEEK, "w"),
    (DAY, "d"),
    (HOUR, "h"),
    (MINUTE, "m"),
    (SECOND, "s"),
    (MILLISECOND, "ms"),
];

/// Convert a duration to a human readable string.
///
/// # Examples
///
/// Given a `Duration`, generate a legible human output:
///
/// ```
/// // One year minus 1 millisecond (demonstrates all the units).
/// let dur = std::time::Duration::from_millis(31_557_600_000 - 1);
///
/// // Prints the output as a nicely formatted string, with units in descending
/// // order of size. Specify the degree of unit precision desired as the second
/// // argument.
/// assert_eq!(
///     htime::pretty_print(&dur, "ms", ", "),
///     "11mo, 4w, 2d, 10h, 29m, 59s, 999ms",
/// )
/// ```
#[must_use]
pub fn pretty_print(
    d: &Duration,
    min_resolution: &str,
    separator: &str,
) -> String {
    components(d, min_resolution).join(separator)
}

/// Convert a duration to a list of human time units, in decreasing order of size.
///
/// Examples
///
/// Get a list of value/unit strings in descending order of magnitude:
///
/// ```
/// // One year minus 1 millisecond (demonstrates all the units).
/// let dur = std::time::Duration::from_millis(31_557_600_000 - 1);
///
/// // Returns a list of the units, with no whitespace or separators.
/// assert_eq!(
///     htime::components(&dur, "ms"),
///     vec!["11mo", "4w", "2d", "10h", "29m", "59s", "999ms"],
/// )
/// ```
#[must_use]
pub fn components(d: &Duration, min_resolution: &str) -> Vec<String> {
    let mut d = d.as_millis();
    let mut out = vec![];

    // Go through each unit we want to display.
    for (millis, suffix) in UNITS {
        // If our duration is smaller than the number of milliseconds in the unit of time we're
        // checking, then skip it.
        if d < millis {
            continue;
        }

        // Figure out how many units of time are contained in our duration.
        let units = d / millis;

        // Push the number of units, appended with the unit indicator, to our output.
        out.push(format!("{}{}", units, suffix));

        // Subtract the total millis worth of this unit from the duration.
        d = d
            .checked_sub(units.checked_mul(millis).expect("overflow"))
            .expect("overflow");

        // If we've reached the minimum resolution requested, break.
        // TODO: This won't round the last unit.
        if suffix == min_resolution {
            break;
        }
    }

    // Return a vec of each component.
    out
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_pretty_print() {
        assert_eq!(pretty_print(&Duration::from_secs(0), "ms", ""), "");
        assert_eq!(
            pretty_print(&Duration::from_millis(YEAR as u64 / 2), "ms", ","),
            "6mo"
        );
        assert_eq!(
            pretty_print(&Duration::from_millis(YEAR as u64), "ms", ","),
            "1y"
        );
        assert_eq!(
            pretty_print(&Duration::from_millis(YEAR as u64 + 1), "ms", ","),
            "1y,1ms"
        );
        assert_eq!(
            pretty_print(&Duration::from_millis(YEAR as u64 - 1), "ms", ", "),
            "11mo, 4w, 2d, 10h, 29m, 59s, 999ms"
        );
        assert_eq!(
            pretty_print(&Duration::from_millis(2 * YEAR as u64), "ms", ""),
            "2y"
        );
        assert_eq!(
            pretty_print(&Duration::from_millis(YEAR as u64 - 1), "h", "-"),
            "11mo-4w-2d-10h"
        );
    }

    #[test]
    fn test_components() {
        let empty: Vec<String> = vec![];

        assert_eq!(components(&Duration::from_secs(0), "ms"), empty);
        assert_eq!(
            components(&Duration::from_millis(YEAR as u64 / 2), "ms"),
            vec!["6mo"]
        );
        assert_eq!(
            components(&Duration::from_millis(YEAR as u64), "ms"),
            vec!["1y"]
        );
        assert_eq!(
            components(&Duration::from_millis(YEAR as u64 + 1), "ms"),
            vec!["1y", "1ms"]
        );
        assert_eq!(
            components(&Duration::from_millis(YEAR as u64 - 1), "ms"),
            vec!["11mo", "4w", "2d", "10h", "29m", "59s", "999ms"]
        );
        assert_eq!(
            components(&Duration::from_millis(2 * YEAR as u64), "ms"),
            vec!["2y"]
        );
        assert_eq!(
            components(&Duration::from_millis(YEAR as u64 - 1), "h"),
            vec!["11mo", "4w", "2d", "10h"]
        );
    }
}