pretty-ms 0.1.0

Convert milliseconds to a human-readable string: 1337000000 -> "15d 11h 23m 20s". A faithful port of the pretty-ms npm package. Zero dependencies.
Documentation
//! Behavioral spec for `pretty-ms`, cross-checked against the npm package.

use pretty_ms::{pretty_ms, Options};

fn p(ms: f64) -> String {
    pretty_ms(ms, &Options::default())
}

#[test]
fn defaults() {
    assert_eq!(p(1_337_000_000.0), "15d 11h 23m 20s");
    assert_eq!(p(1337.0), "1.3s");
    assert_eq!(p(133.0), "133ms");
    assert_eq!(p(0.0), "0ms");
    assert_eq!(p(1000.0), "1s");
    assert_eq!(p(1500.0), "1.5s");
    assert_eq!(p(60_000.0), "1m");
    assert_eq!(p(3_600_000.0), "1h");
    assert_eq!(p(86_400_000.0), "1d");
    assert_eq!(p(4_020_000.0), "1h 7m");
    assert_eq!(p(40_176_000_000.0), "1y 100d");
    assert_eq!(p(123_456_789.0), "1d 10h 17m 36.7s");
    assert_eq!(p(999.0), "999ms");
    assert_eq!(p(1001.0), "1s");
}

#[test]
fn negatives_and_fractions() {
    assert_eq!(p(-3000.0), "-3s");
    assert_eq!(p(1.5), "2ms"); // rounds
    assert_eq!(p(0.4), "1ms"); // sub-1ms rounds up via ceil
    assert_eq!(p(100.4), "100ms");
    assert_eq!(p(33.333), "33ms");
}

#[test]
fn compact_and_unit_count() {
    let c = Options::default().compact(true);
    assert_eq!(pretty_ms(1_337_000_000.0, &c), "15d");
    assert_eq!(pretty_ms(123_456_789.0, &c), "1d");
    assert_eq!(
        pretty_ms(1_337_000_000.0, &Options::default().unit_count(2)),
        "15d 11h"
    );
    assert_eq!(
        pretty_ms(1_337_000_000.0, &Options::default().unit_count(1)),
        "15d"
    );
}

#[test]
fn verbose() {
    let v = Options::default().verbose(true);
    assert_eq!(
        pretty_ms(1_335_669_000.0, &v),
        "15 days 11 hours 1 minute 9 seconds"
    );
    assert_eq!(pretty_ms(1500.0, &v), "1.5 seconds");
    assert_eq!(pretty_ms(1000.0, &v), "1 second");
    assert_eq!(pretty_ms(0.0, &v), "0 milliseconds");
}

#[test]
fn colon_notation() {
    let c = Options::default().colon_notation(true);
    assert_eq!(pretty_ms(1337.0, &c), "0:01.3");
    assert_eq!(pretty_ms(95_500.0, &c), "1:35.5");
    assert_eq!(pretty_ms(3_661_000.0, &c), "1:01:01");
    assert_eq!(pretty_ms(1_335_669_000.0, &c), "15:11:01:09");
    assert_eq!(pretty_ms(3_600_000.0, &c), "1:00:00");
    assert_eq!(pretty_ms(60_000.0, &c), "1:00");
    assert_eq!(pretty_ms(500.0, &c), "0:00.5");
    assert_eq!(pretty_ms(0.0, &c), "0:00");
}

#[test]
fn decimal_digits() {
    assert_eq!(
        pretty_ms(1500.0, &Options::default().seconds_decimal_digits(0)),
        "1s"
    );
    assert_eq!(
        pretty_ms(1500.0, &Options::default().seconds_decimal_digits(2)),
        "1.50s"
    );
    assert_eq!(
        pretty_ms(1543.0, &Options::default().seconds_decimal_digits(0)),
        "1s"
    );
    assert_eq!(
        pretty_ms(
            3000.0,
            &Options::default().keep_decimals_on_whole_seconds(true)
        ),
        "3.0s"
    );
    assert_eq!(
        pretty_ms(
            3000.0,
            &Options::default()
                .seconds_decimal_digits(2)
                .keep_decimals_on_whole_seconds(true)
        ),
        "3.00s"
    );
}

#[test]
fn sub_second_units() {
    let sep = Options::default().separate_milliseconds(true);
    assert_eq!(pretty_ms(1010.0, &sep), "1s 10ms");
    assert_eq!(pretty_ms(1337.0, &sep), "1s 337ms");

    let sub = Options::default().format_sub_milliseconds(true);
    assert_eq!(pretty_ms(0.123, &sub), "123µs");
    assert_eq!(pretty_ms(100.4, &sub), "100ms 400µs");
    assert_eq!(pretty_ms(1010.5, &sub), "1s 10ms 500µs");
}

#[test]
fn hide_options() {
    assert_eq!(
        pretty_ms(40_176_000_000.0, &Options::default().hide_year(true)),
        "465d"
    );
    assert_eq!(
        pretty_ms(
            40_176_000_000.0,
            &Options::default().hide_year_and_days(true)
        ),
        "11160h"
    );
    assert_eq!(
        pretty_ms(3_661_000.0, &Options::default().hide_seconds(true)),
        "1h 1m"
    );
    assert_eq!(
        pretty_ms(5000.0, &Options::default().hide_seconds(true)),
        "0ms"
    );
}

#[test]
fn non_finite_is_zero() {
    assert_eq!(p(f64::NAN), "0ms");
    assert_eq!(p(f64::INFINITY), "0ms");
}

// Regression: large decimal-digit counts and huge inputs must not panic/overflow.
#[test]
fn extreme_inputs_do_not_panic() {
    let _ = pretty_ms(1500.0, &Options::default().seconds_decimal_digits(20));
    let _ = pretty_ms(1500.0, &Options::default().seconds_decimal_digits(100));
    let _ = pretty_ms(1500.5, &Options::default().milliseconds_decimal_digits(40));
    let _ = pretty_ms(f64::MAX, &Options::default());
    let _ = pretty_ms(1e300, &Options::default().colon_notation(true));
    let _ = pretty_ms(1500.0, &Options::default().unit_count(usize::MAX));
    // large digit counts still produce output; realistic ones are unaffected
    assert!(!pretty_ms(1500.0, &Options::default().seconds_decimal_digits(20)).is_empty());
    assert!(!pretty_ms(
        1500.0,
        &Options::default().seconds_decimal_digits(usize::MAX)
    )
    .is_empty());
    assert_eq!(
        pretty_ms(1500.0, &Options::default().seconds_decimal_digits(4)),
        "1.5000s"
    );
}

// Regression: millisecondsDecimalDigits must round the true f64 (like JS toFixed).
#[test]
fn milliseconds_decimal_rounding() {
    let d2 = Options::default().milliseconds_decimal_digits(2);
    assert_eq!(pretty_ms(2.675, &d2), "2.67ms"); // 2.675 is really 2.67499…
    assert_eq!(pretty_ms(0.615, &d2), "0.61ms");
    let d3 = Options::default().milliseconds_decimal_digits(3);
    assert_eq!(pretty_ms(531.5785, &d3), "531.578ms");
}

#[test]
fn duration_wrapper() {
    use pretty_ms::pretty_duration;
    use std::time::Duration;
    assert_eq!(
        pretty_duration(Duration::from_secs(90), &Options::default()),
        "1m 30s"
    );
    assert_eq!(
        pretty_duration(Duration::from_millis(1337), &Options::default()),
        "1.3s"
    );
}