use std::time::Duration;
#[must_use]
pub fn progress_stats(done: u64, total: u64, elapsed: Duration) -> (u64, u64, u64) {
let percent =
if total == 0 { 100 } else { done.saturating_mul(100).checked_div(total).unwrap_or(100) };
let elapsed_seconds = elapsed.as_secs();
let remaining = total.saturating_sub(done);
let remaining_seconds = elapsed
.as_millis()
.saturating_mul(u128::from(remaining))
.checked_div(u128::from(done).saturating_mul(1000))
.map_or(0, |seconds| u64::try_from(seconds).unwrap_or(u64::MAX));
(percent, elapsed_seconds, remaining_seconds)
}
#[must_use]
pub fn hms(seconds: u64) -> String {
let h = seconds.checked_div(3600).unwrap_or(0);
let m = seconds.checked_div(60).and_then(|minutes| minutes.checked_rem(60)).unwrap_or(0);
let s = seconds.checked_rem(60).unwrap_or(0);
format!("{h:02}:{m:02}:{s:02}")
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::{hms, progress_stats};
#[test]
fn an_empty_scan_reads_one_hundred_percent() {
let (percent, _, remaining) = progress_stats(0, 0, Duration::from_secs(5));
assert_eq!(percent, 100);
assert_eq!(remaining, 0);
}
#[test]
fn percent_is_the_byte_ratio() {
assert_eq!(progress_stats(0, 200, Duration::ZERO).0, 0);
assert_eq!(progress_stats(50, 200, Duration::ZERO).0, 25);
assert_eq!(progress_stats(200, 200, Duration::ZERO).0, 100);
}
#[test]
fn the_first_second_already_estimates() {
let (_, elapsed, remaining) = progress_stats(100, 200, Duration::from_millis(500));
assert_eq!(elapsed, 0);
assert_eq!(remaining, 0);
assert_eq!(progress_stats(100, 200, Duration::from_secs(4)).2, 4);
}
#[test]
fn remaining_is_zero_before_any_bytes() {
assert_eq!(progress_stats(0, 200, Duration::from_secs(3)).2, 0);
}
#[test]
fn an_estimate_past_a_u64_of_seconds_saturates() {
let (_, _, remaining) =
progress_stats(1, u64::MAX, Duration::from_secs(u64::from(u32::MAX)));
assert_eq!(remaining, u64::MAX);
}
#[test]
fn hms_formats_hours_minutes_seconds() {
assert_eq!(hms(0), "00:00:00");
assert_eq!(hms(61), "00:01:01");
assert_eq!(hms(3661), "01:01:01");
assert_eq!(hms(90_061), "25:01:01");
}
mod prop {
use std::time::Duration;
use proptest::prelude::{Just, Strategy, any, prop_assert, prop_assert_eq, proptest};
use super::super::{hms, progress_stats};
fn done_and_total() -> impl Strategy<Value = (u64, u64)> {
(0_u64..1_000_000).prop_flat_map(|total| (0..=total, Just(total)))
}
proptest! {
#[test]
fn percent_is_bounded_and_monotone(
(done, total) in done_and_total(),
millis in 0_u64..10_000,
) {
let (percent, _, _) = progress_stats(done, total, Duration::from_millis(millis));
prop_assert!(percent <= 100);
let more = progress_stats(done.saturating_add(1), total, Duration::ZERO).0;
prop_assert!(more >= percent || done >= total);
}
#[test]
fn the_triple_is_total_over_any_counts(
done in any::<u64>(),
total in any::<u64>(),
millis in any::<u64>(),
) {
let (percent, elapsed, remaining) =
progress_stats(done, total, Duration::from_millis(millis));
prop_assert!(percent <= 100 || total < done);
let _ = (elapsed, remaining);
}
#[test]
fn hms_is_well_formed(seconds in any::<u64>()) {
let formatted = hms(seconds);
let mut parts = formatted.split(':');
let hours = parts.next().and_then(|part| part.parse::<u64>().ok());
let minutes = parts.next().and_then(|part| part.parse::<u64>().ok());
let secs = parts.next().and_then(|part| part.parse::<u64>().ok());
prop_assert!(parts.next().is_none(), "exactly three segments");
prop_assert_eq!(hours, seconds.checked_div(3600));
prop_assert!(minutes.is_some_and(|value| value < 60));
prop_assert!(secs.is_some_and(|value| value < 60));
}
}
}
}