//! A span of milliseconds, written the way somebody reads it -- the port //! of `Durations.kt`'s `formatMillis`/`formatMillisText`, with its tests. //! //! Only the tool-timeout half is here. `formatSpan` (the usage //! countdown's rounding-up rule) belongs with whatever draws the usage //! bar, and nothing in this crate needs it yet. /// A span of milliseconds, written the way somebody reads it. /// /// A tool's timeout arrives as `480000`, which nobody reads as eight /// minutes. The rule has two halves, because a short span and a long one /// are read for different things. Under a minute the question is "roughly /// how long", so only the largest unit is shown and a fraction carries the /// rest -- `2.5s`. At a minute or more the question is "how long exactly", /// so every unit with something in it is written out -- `5d 12h 4m`. Empty /// units are left out rather than written as zero. /// /// Sub-second precision is dropped past a minute: nothing that takes days /// is measured in milliseconds. pub fn format_millis(ms: i64) -> String { if ms < 0 { return format!("-{}", format_millis(-ms)); } if ms < 1000 { return format!("{ms}ms"); } if ms < 60_000 { let tenths = (ms + 50) / 100; let (whole, rest) = (tenths / 10, tenths % 10); return if rest == 0 { format!("{whole}s") } else { format!("{whole}.{rest}s") }; } let seconds = ms / 1000; [ ("d", seconds / 86_400), ("h", seconds / 3600 % 24), ("m", seconds / 60 % 60), ("s", seconds % 60), ] .iter() .filter(|(_, n)| *n > 0) .map(|(unit, n)| format!("{n}{unit}")) .collect::>() .join(" ") } /// `text` as a span when it is a whole number of milliseconds, and /// unchanged when it is not. pub fn format_millis_text(text: &str) -> String { match text.trim().parse::() { Ok(ms) => format_millis(ms), Err(_) => text.to_string(), } } #[cfg(test)] mod tests { use super::*; /// The two ways a span of time is written here, and the rule each of /// them follows -- ported from `DurationsTest.kt`, whose doc says why: /// both are read off a screen to make a decision, so what matters is /// that the shortest form that answers the question is what appears. #[test] fn under_a_minute_is_the_largest_unit_alone() { assert_eq!(format_millis(30), "30ms"); assert_eq!(format_millis(999), "999ms"); assert_eq!(format_millis(1000), "1s"); assert_eq!(format_millis(2500), "2.5s"); // One decimal, rounded rather than cut: 2.46s is nearer two and a // half than two and four. assert_eq!(format_millis(2460), "2.5s"); assert_eq!(format_millis(59_900), "59.9s"); } #[test] fn a_minute_or_more_is_every_unit_that_has_something_in_it() { // The figure this rule was written for: a tool timeout, which // arrives as milliseconds and is unreadable as 480000. assert_eq!(format_millis(480_000), "8m"); assert_eq!(format_millis(60_000), "1m"); assert_eq!(format_millis(90_000), "1m 30s"); assert_eq!(format_millis(475_440_000), "5d 12h 4m"); // Empty units are left out rather than written as zero: the labels // say which is which, and "5d 0h 4m" is only longer. assert_eq!(format_millis(432_240_000), "5d 4m"); } #[test] fn only_a_whole_number_of_milliseconds_is_rewritten() { assert_eq!(format_millis_text(" 480000 "), "8m"); // A timeout a tool expressed some other way is its own words, // passed through rather than guessed at. assert_eq!(format_millis_text("2 minutes"), "2 minutes"); assert_eq!(format_millis_text(""), ""); } }