mirror of
https://github.com/sharkdp/hyperfine.git
synced 2026-11-01 08:50:19 +00:00
Group units, formatting, and statistics under quantity
This commit is contained in:
1 parent
bef66df7e4
commit
aa14b8c0d9
20 files changed
+266
-265
No files matched your search
@@ -0,0 +1,133 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
pub use uom::num_traits::Zero;
|
||||
pub use uom::si::f64::{Information, Ratio, Time};
|
||||
pub use uom::si::information::byte;
|
||||
pub use uom::si::ratio::ratio;
|
||||
pub use uom::si::time::{microsecond, nanosecond, second};
|
||||
|
||||
pub use units::TimeUnit;
|
||||
|
||||
pub mod statistics;
|
||||
mod units;
|
||||
|
||||
pub const fn const_time_from_seconds(value: f64) -> Time {
|
||||
// Quantity::new in uom is not yet const: https://docs.rs/uom/0.36.0/uom/si/struct.Quantity.html
|
||||
Time {
|
||||
dimension: PhantomData,
|
||||
units: PhantomData,
|
||||
value,
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the given duration as a string. The output-unit can be enforced by setting `unit` to
|
||||
/// `Some(target_unit)`. If `unit` is `None`, it will be determined automatically.
|
||||
pub fn format_duration(duration: f64, unit: Option<TimeUnit>) -> String {
|
||||
let (duration_fmt, _) = format_duration_unit(duration, unit);
|
||||
duration_fmt
|
||||
}
|
||||
|
||||
/// Like `format_duration`, but returns the target unit as well.
|
||||
pub fn format_duration_unit(duration: f64, unit: Option<TimeUnit>) -> (String, TimeUnit) {
|
||||
let (out_str, out_unit) = format_duration_value(duration, unit);
|
||||
|
||||
(format!("{} {}", out_str, out_unit.short_name()), out_unit)
|
||||
}
|
||||
|
||||
/// Like `format_duration`, but returns the target unit as well.
|
||||
pub fn format_duration_value(duration: f64, unit: Option<TimeUnit>) -> (String, TimeUnit) {
|
||||
if (duration < 0.001 && unit.is_none()) || unit == Some(TimeUnit::MicroSecond) {
|
||||
(
|
||||
TimeUnit::MicroSecond.format(duration),
|
||||
TimeUnit::MicroSecond,
|
||||
)
|
||||
} else if (duration < 1.0 && unit.is_none()) || unit == Some(TimeUnit::MilliSecond) {
|
||||
(
|
||||
TimeUnit::MilliSecond.format(duration),
|
||||
TimeUnit::MilliSecond,
|
||||
)
|
||||
} else {
|
||||
let unit = unit.unwrap_or(TimeUnit::Second);
|
||||
(unit.format(duration), unit)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
use uom::si::{information::kibibyte, time::millisecond};
|
||||
|
||||
#[test]
|
||||
fn test_time() {
|
||||
let time = Time::new::<millisecond>(123.4);
|
||||
assert_eq!(time.get::<millisecond>(), 123.4);
|
||||
|
||||
let time_s = time.get::<second>();
|
||||
approx::assert_relative_eq!(time_s, 0.1234);
|
||||
|
||||
let time_us = time.get::<microsecond>();
|
||||
approx::assert_relative_eq!(time_us, 123400.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_information() {
|
||||
use uom::si::information::pebibyte;
|
||||
|
||||
let information = Information::new::<kibibyte>(8.);
|
||||
assert_eq!(information.get::<byte>(), 8192.);
|
||||
|
||||
let information_kib = information.get::<kibibyte>();
|
||||
assert_eq!(information_kib, 8.);
|
||||
|
||||
let largest_exactly_representable = Information::new::<byte>(9_007_199_254_740_992.);
|
||||
assert_eq!(largest_exactly_representable.get::<pebibyte>(), 8.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_duration_unit_basic() {
|
||||
let (out_str, out_unit) = format_duration_unit(1.3, None);
|
||||
|
||||
assert_eq!("1.300 s", out_str);
|
||||
assert_eq!(TimeUnit::Second, out_unit);
|
||||
|
||||
let (out_str, out_unit) = format_duration_unit(1.0, None);
|
||||
|
||||
assert_eq!("1.000 s", out_str);
|
||||
assert_eq!(TimeUnit::Second, out_unit);
|
||||
|
||||
let (out_str, out_unit) = format_duration_unit(0.999, None);
|
||||
|
||||
assert_eq!("999.0 ms", out_str);
|
||||
assert_eq!(TimeUnit::MilliSecond, out_unit);
|
||||
|
||||
let (out_str, out_unit) = format_duration_unit(0.0005, None);
|
||||
|
||||
assert_eq!("500.0 µs", out_str);
|
||||
assert_eq!(TimeUnit::MicroSecond, out_unit);
|
||||
|
||||
let (out_str, out_unit) = format_duration_unit(0.0, None);
|
||||
|
||||
assert_eq!("0.0 µs", out_str);
|
||||
assert_eq!(TimeUnit::MicroSecond, out_unit);
|
||||
|
||||
let (out_str, out_unit) = format_duration_unit(1000.0, None);
|
||||
|
||||
assert_eq!("1000.000 s", out_str);
|
||||
assert_eq!(TimeUnit::Second, out_unit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_duration_unit_with_unit() {
|
||||
let (out_str, out_unit) = format_duration_unit(1.3, Some(TimeUnit::Second));
|
||||
|
||||
assert_eq!("1.300 s", out_str);
|
||||
assert_eq!(TimeUnit::Second, out_unit);
|
||||
|
||||
let (out_str, out_unit) = format_duration_unit(1.3, Some(TimeUnit::MilliSecond));
|
||||
|
||||
assert_eq!("1300.0 ms", out_str);
|
||||
assert_eq!(TimeUnit::MilliSecond, out_unit);
|
||||
|
||||
let (out_str, out_unit) = format_duration_unit(1.3, Some(TimeUnit::MicroSecond));
|
||||
|
||||
assert_eq!("1300000.0 µs", out_str);
|
||||
assert_eq!(TimeUnit::MicroSecond, out_unit);
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
//! Statistics for non-empty samples of finite measurements.
|
||||
|
||||
use std::ops::{Add, AddAssign, Div};
|
||||
|
||||
use crate::quantity::{byte, ratio, second, Information, Ratio, Time, Zero};
|
||||
|
||||
/// A min function that assumes no NaNs and at least one element
|
||||
pub fn min<Q: PartialOrd>(values: impl IntoIterator<Item = Q>) -> Q {
|
||||
values
|
||||
.into_iter()
|
||||
.min_by(|a, b| a.partial_cmp(b).expect("No NaN values"))
|
||||
.expect("'min' requires at least one element")
|
||||
}
|
||||
|
||||
/// A max function that assumes no NaNs and at least one element
|
||||
pub fn max<Q: PartialOrd>(values: impl IntoIterator<Item = Q>) -> Q {
|
||||
values
|
||||
.into_iter()
|
||||
.max_by(|a, b| a.partial_cmp(b).expect("No NaN values"))
|
||||
.expect("'max' requires at least one element")
|
||||
}
|
||||
|
||||
/// Arithmetic mean, summing measurements in their original order.
|
||||
pub fn mean<Q, P>(values: impl IntoIterator<Item = Q>) -> Q
|
||||
where
|
||||
Q: AddAssign + Zero + Div<Ratio, Output = P>,
|
||||
P: Into<Q>,
|
||||
{
|
||||
let mut sum = Q::zero();
|
||||
let mut count = 0usize;
|
||||
for value in values {
|
||||
sum += value;
|
||||
count += 1;
|
||||
}
|
||||
|
||||
let count = Ratio::new::<ratio>(count as f64);
|
||||
(sum / count).into()
|
||||
}
|
||||
|
||||
/// Median without modifying the input sample.
|
||||
pub fn median<Q, P>(values: impl IntoIterator<Item = Q>) -> Q
|
||||
where
|
||||
Q: Copy + PartialOrd + Add<Output = Q> + Div<Ratio, Output = P>,
|
||||
P: Into<Q>,
|
||||
{
|
||||
let mut values = values.into_iter().collect::<Vec<_>>();
|
||||
assert!(
|
||||
!values.is_empty(),
|
||||
"median requires at least one measurement"
|
||||
);
|
||||
values.sort_unstable_by(|a, b| a.partial_cmp(b).expect("No NaN measurements"));
|
||||
|
||||
let len = values.len();
|
||||
if len % 2 == 0 {
|
||||
let mid = len / 2;
|
||||
let a = &values[mid - 1];
|
||||
let b = &values[mid];
|
||||
((*b + *a) / Ratio::new::<ratio>(2.)).into()
|
||||
} else {
|
||||
values[len / 2]
|
||||
}
|
||||
}
|
||||
|
||||
/// This is just sad, but after several hours trying to figure out how to write
|
||||
/// a generic `standard_deviation` function using `uom` quantities, I gave up.
|
||||
pub trait UnsafeRawValue {
|
||||
fn unsafe_raw_value(&self) -> f64;
|
||||
fn unsafe_from_raw_value(value: f64) -> Self;
|
||||
}
|
||||
|
||||
impl UnsafeRawValue for Time {
|
||||
fn unsafe_raw_value(&self) -> f64 {
|
||||
self.get::<second>()
|
||||
}
|
||||
|
||||
fn unsafe_from_raw_value(value: f64) -> Self {
|
||||
Time::new::<second>(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl UnsafeRawValue for Information {
|
||||
fn unsafe_raw_value(&self) -> f64 {
|
||||
self.get::<byte>()
|
||||
}
|
||||
|
||||
fn unsafe_from_raw_value(value: f64) -> Self {
|
||||
Information::new::<byte>(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sample standard deviation, using an already computed mean.
|
||||
pub fn standard_deviation<Q: UnsafeRawValue>(values: &[Q], mean: Q) -> Q {
|
||||
let mean = mean.unsafe_raw_value();
|
||||
assert!(
|
||||
values.len() > 1,
|
||||
"standard deviation requires at least two measurements"
|
||||
);
|
||||
let sum = values
|
||||
.iter()
|
||||
.map(|value| (value.unsafe_raw_value() - mean) * (value.unsafe_raw_value() - mean))
|
||||
.fold(0.0, |sum, deviation| sum + deviation);
|
||||
assert!(sum >= 0.0, "invalid sum of squared deviations");
|
||||
|
||||
// Preserve the operation order of statistical 1.0: dividing by n - 1
|
||||
// before taking the square root also preserves exported floating-point values.
|
||||
Q::unsafe_from_raw_value((sum / (values.len() as f64 - 1.0)).sqrt())
|
||||
}
|
||||
|
||||
/// Compute modifized Z-scores for a given sample. A (unmodified) Z-score is defined by
|
||||
/// `(x_i - x_mean)/x_stddev` whereas the modified Z-score is defined by `(x_i - x_median)/MAD`
|
||||
/// where MAD is the median absolute deviation.
|
||||
///
|
||||
/// References:
|
||||
/// - <https://en.wikipedia.org/wiki/Median_absolute_deviation>
|
||||
pub fn modified_zscores(xs: &[f64]) -> Vec<f64> {
|
||||
assert!(!xs.is_empty());
|
||||
|
||||
// Compute sample median:
|
||||
let x_median = median(xs.iter().copied());
|
||||
|
||||
// Compute the absolute deviations from the median:
|
||||
let deviations: Vec<f64> = xs.iter().map(|x| (x - x_median).abs()).collect();
|
||||
|
||||
// Compute median absolute deviation:
|
||||
let mad = median(deviations.iter().copied());
|
||||
|
||||
// Handle MAD == 0 case
|
||||
let mad = if mad > 0.0 { mad } else { f64::EPSILON };
|
||||
|
||||
// Compute modified Z-scores (x_i - x_median) / MAD
|
||||
xs.iter().map(|&x| (x - x_median) / mad).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use approx::assert_relative_eq;
|
||||
use uom::si::information::kibibyte;
|
||||
use uom::si::time::{microsecond, millisecond};
|
||||
|
||||
#[test]
|
||||
fn test_min() {
|
||||
assert_eq!(1.0, min([1.0]));
|
||||
assert_eq!(-1.0, min([-1.0]));
|
||||
assert_eq!(-2.0, min([-2.0, -1.0]));
|
||||
assert_eq!(-1.0, min([-1.0, 1.0]));
|
||||
assert_eq!(-1.0, min([1.0, -1.0, 0.0]));
|
||||
|
||||
let values = vec![
|
||||
Information::new::<kibibyte>(1.0),
|
||||
Information::new::<byte>(2.0),
|
||||
Information::new::<byte>(3.0),
|
||||
];
|
||||
assert_eq!(min(&values).get::<byte>(), 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max() {
|
||||
assert_eq!(1.0, max([1.0]));
|
||||
assert_eq!(-1.0, max([-1.0]));
|
||||
assert_eq!(-1.0, max([-2.0, -1.0]));
|
||||
assert_eq!(1.0, max([-1.0, 1.0]));
|
||||
assert_eq!(1.0, max([-1.0, 1.0, 0.0]));
|
||||
|
||||
let values = vec![
|
||||
Information::new::<byte>(1.0),
|
||||
Information::new::<kibibyte>(2.0),
|
||||
Information::new::<byte>(3.0),
|
||||
];
|
||||
assert_eq!(max(&values).get::<kibibyte>(), 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mean() {
|
||||
assert_eq!(1.0, mean([1.0]));
|
||||
assert_relative_eq!(2.0, mean([1.0, 3.0]));
|
||||
|
||||
let values = [
|
||||
Time::new::<millisecond>(100.0),
|
||||
Time::new::<millisecond>(200.0),
|
||||
Time::new::<microsecond>(600_000.0),
|
||||
];
|
||||
let result = mean(values);
|
||||
assert_relative_eq!(result.get::<millisecond>(), 300.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_median() {
|
||||
assert_eq!(1.0, median([1.0]));
|
||||
assert_relative_eq!(2.0, median([1.0, 3.0]));
|
||||
|
||||
let values = [
|
||||
Time::new::<millisecond>(100.0),
|
||||
Time::new::<millisecond>(200.0),
|
||||
Time::new::<microsecond>(600_000.0),
|
||||
];
|
||||
let result = median(values);
|
||||
assert_relative_eq!(result.get::<millisecond>(), 200.0);
|
||||
|
||||
let values = [
|
||||
Time::new::<millisecond>(100.0),
|
||||
Time::new::<millisecond>(200.0),
|
||||
Time::new::<microsecond>(300_000.0),
|
||||
Time::new::<microsecond>(600_000.0),
|
||||
];
|
||||
let result = median(values);
|
||||
assert_relative_eq!(result.get::<millisecond>(), 250.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_standard_deviation() {
|
||||
let values = [
|
||||
Time::new::<millisecond>(100.0),
|
||||
Time::new::<millisecond>(200.0),
|
||||
Time::new::<microsecond>(300_000.0),
|
||||
];
|
||||
let result = standard_deviation(&values, mean(values));
|
||||
assert_relative_eq!(result.get::<millisecond>(), 100.0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
//! This module contains common units.
|
||||
|
||||
/// Supported time units
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TimeUnit {
|
||||
Second,
|
||||
MilliSecond,
|
||||
MicroSecond,
|
||||
Minute,
|
||||
Hour,
|
||||
}
|
||||
|
||||
impl TimeUnit {
|
||||
/// The abbreviation of the TimeUnit.
|
||||
pub fn short_name(self) -> String {
|
||||
match self {
|
||||
TimeUnit::Second => String::from("s"),
|
||||
TimeUnit::MilliSecond => String::from("ms"),
|
||||
TimeUnit::MicroSecond => String::from("µs"),
|
||||
TimeUnit::Minute => String::from("min"),
|
||||
TimeUnit::Hour => String::from("h"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the value in seconds formatted for the TimeUnit.
|
||||
pub fn format(self, value: f64) -> String {
|
||||
match self {
|
||||
TimeUnit::Second => format!("{value:.3}"),
|
||||
TimeUnit::MilliSecond => format!("{:.1}", value * 1e3),
|
||||
TimeUnit::MicroSecond => format!("{:.1}", value * 1e6),
|
||||
TimeUnit::Minute => format!("{:.1}", value / 60.0),
|
||||
TimeUnit::Hour => format!("{:.1}", value / 3600.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unit_short_name() {
|
||||
assert_eq!("s", TimeUnit::Second.short_name());
|
||||
assert_eq!("ms", TimeUnit::MilliSecond.short_name());
|
||||
assert_eq!("µs", TimeUnit::MicroSecond.short_name());
|
||||
assert_eq!("min", TimeUnit::Minute.short_name());
|
||||
assert_eq!("h", TimeUnit::Hour.short_name());
|
||||
}
|
||||
|
||||
// Note - the values are rounded when formatted.
|
||||
#[test]
|
||||
fn test_unit_format() {
|
||||
let value: f64 = 123.456789;
|
||||
assert_eq!("123.457", TimeUnit::Second.format(value));
|
||||
assert_eq!("123456.8", TimeUnit::MilliSecond.format(value));
|
||||
|
||||
assert_eq!("1234.6", TimeUnit::MicroSecond.format(0.00123456));
|
||||
assert_eq!("2.1", TimeUnit::Minute.format(value));
|
||||
assert_eq!("0.0", TimeUnit::Hour.format(value));
|
||||
}
|
||||
Reference in new issue
Block a user