diff --git a/src/benchmark/benchmark_result.rs b/src/benchmark/benchmark_result.rs index 287c73b..69de16c 100644 --- a/src/benchmark/benchmark_result.rs +++ b/src/benchmark/benchmark_result.rs @@ -2,8 +2,6 @@ use std::collections::BTreeMap; use serde::Serialize; -use crate::util::units::Second; - /// Set of values that will be exported. // NOTE: `serde` is used for JSON serialization, but not for CSV serialization due to the // `parameters` map. Update `src/hyperfine/export/csv.rs` with new fields, as appropriate. @@ -18,29 +16,29 @@ pub struct BenchmarkResult { pub command_with_unused_parameters: String, /// The average run time - pub mean: Second, + pub mean: f64, /// The standard deviation of all run times. Not available if only one run has been performed - pub stddev: Option, + pub stddev: Option, /// The median run time - pub median: Second, + pub median: f64, /// Time spent in user mode - pub user: Second, + pub user: f64, /// Time spent in kernel mode - pub system: Second, + pub system: f64, /// Minimum of all measured times - pub min: Second, + pub min: f64, /// Maximum of all measured times - pub max: Second, + pub max: f64, /// All run time measurements #[serde(skip_serializing_if = "Option::is_none")] - pub times: Option>, + pub times: Option>, /// Maximum memory usage of the process, in bytes #[serde(skip_serializing_if = "Option::is_none")] diff --git a/src/benchmark/measurement.rs b/src/benchmark/measurement.rs index eb1edc9..9eaa8d0 100644 --- a/src/benchmark/measurement.rs +++ b/src/benchmark/measurement.rs @@ -1,8 +1,7 @@ use std::process::ExitStatus; -use crate::outlier_detection::modified_zscores; +use crate::quantity::statistics::{max, mean, median, min, modified_zscores, standard_deviation}; use crate::quantity::{second, Information, Time}; -use crate::util::statistics::{max, mean, median, min, standard_deviation}; /// Performance measurements and exit status from running a single command #[derive(Debug, Default, Copy, Clone)] diff --git a/src/benchmark/mod.rs b/src/benchmark/mod.rs index 7b6f965..18a861a 100644 --- a/src/benchmark/mod.rs +++ b/src/benchmark/mod.rs @@ -14,11 +14,12 @@ use crate::options::{ }; use crate::outlier_detection::OUTLIER_THRESHOLD; use crate::output::console_writeln; -use crate::output::format::{format_duration, format_duration_unit}; use crate::output::progress_bar::get_progress_bar; use crate::output::warnings::{OutlierWarningOptions, Warnings}; use crate::parameter::ParameterNameAndValue; -use crate::quantity::{byte, const_time_from_seconds, ratio, second, Time, Zero}; +use crate::quantity::{ + byte, const_time_from_seconds, format_duration, format_duration_unit, ratio, second, Time, Zero, +}; use crate::util::exit_code::extract_exit_code; use benchmark_result::BenchmarkResult; use measurement::{Measurement, Measurements}; diff --git a/src/benchmark/relative_speed.rs b/src/benchmark/relative_speed.rs index a16feaf..f40c85a 100644 --- a/src/benchmark/relative_speed.rs +++ b/src/benchmark/relative_speed.rs @@ -1,13 +1,13 @@ use std::cmp::Ordering; use super::benchmark_result::BenchmarkResult; -use crate::{options::SortOrder, util::units::Scalar}; +use crate::options::SortOrder; #[derive(Debug)] pub struct BenchmarkResultWithRelativeSpeed<'a> { pub result: &'a BenchmarkResult, - pub relative_speed: Scalar, - pub relative_speed_stddev: Option, + pub relative_speed: f64, + pub relative_speed_stddev: Option, pub is_reference: bool, // Less means faster pub relative_ordering: Ordering, @@ -119,7 +119,7 @@ pub fn compute( } #[cfg(test)] -fn create_result(name: &str, mean: Scalar) -> BenchmarkResult { +fn create_result(name: &str, mean: f64) -> BenchmarkResult { use std::collections::BTreeMap; BenchmarkResult { diff --git a/src/export/csv.rs b/src/export/csv.rs index a0cf8aa..ae87391 100644 --- a/src/export/csv.rs +++ b/src/export/csv.rs @@ -5,7 +5,7 @@ use csv::WriterBuilder; use super::Exporter; use crate::benchmark::benchmark_result::BenchmarkResult; use crate::options::SortOrder; -use crate::util::units::Unit; +use crate::quantity::TimeUnit; use anyhow::Result; @@ -16,7 +16,7 @@ impl Exporter for CsvExporter { fn serialize( &self, results: &[BenchmarkResult], - _unit: Option, + _unit: Option, _sort_order: SortOrder, ) -> Result> { let mut writer = WriterBuilder::new().from_writer(vec![]); @@ -110,7 +110,7 @@ fn test_csv() { let actual = String::from_utf8( exporter - .serialize(&results, Some(Unit::Second), SortOrder::Command) + .serialize(&results, Some(TimeUnit::Second), SortOrder::Command) .unwrap(), ) .unwrap(); diff --git a/src/export/json.rs b/src/export/json.rs index d2542c1..6c9b21d 100644 --- a/src/export/json.rs +++ b/src/export/json.rs @@ -4,7 +4,7 @@ use serde_json::to_vec_pretty; use super::Exporter; use crate::benchmark::benchmark_result::BenchmarkResult; use crate::options::SortOrder; -use crate::util::units::Unit; +use crate::quantity::TimeUnit; use anyhow::Result; @@ -20,7 +20,7 @@ impl Exporter for JsonExporter { fn serialize( &self, results: &[BenchmarkResult], - _unit: Option, + _unit: Option, _sort_order: SortOrder, ) -> Result> { let mut output = to_vec_pretty(&HyperfineSummary { results }); diff --git a/src/export/markup.rs b/src/export/markup.rs index 2de67c5..f63bb80 100644 --- a/src/export/markup.rs +++ b/src/export/markup.rs @@ -1,8 +1,7 @@ use crate::benchmark::relative_speed::BenchmarkResultWithRelativeSpeed; use crate::benchmark::{benchmark_result::BenchmarkResult, relative_speed}; use crate::options::SortOrder; -use crate::output::format::format_duration_value; -use crate::util::units::Unit; +use crate::quantity::{format_duration_value, TimeUnit}; use super::Exporter; use anyhow::Result; @@ -13,7 +12,11 @@ pub enum Alignment { } pub trait MarkupExporter { - fn table_results(&self, entries: &[BenchmarkResultWithRelativeSpeed], unit: Unit) -> String { + fn table_results( + &self, + entries: &[BenchmarkResultWithRelativeSpeed], + unit: TimeUnit, + ) -> String { // prepare table header strings let notation = format!("[{}]", unit.short_name()); @@ -95,13 +98,13 @@ pub trait MarkupExporter { fn command(&self, size: &str) -> String; } -fn determine_unit_from_results(results: &[BenchmarkResult]) -> Unit { +fn determine_unit_from_results(results: &[BenchmarkResult]) -> TimeUnit { if let Some(first_result) = results.first() { // Use the first BenchmarkResult entry to determine the unit for all entries. format_duration_value(first_result.mean, None).1 } else { // Default to `Second`. - Unit::Second + TimeUnit::Second } } @@ -109,7 +112,7 @@ impl Exporter for T { fn serialize( &self, results: &[BenchmarkResult], - unit: Option, + unit: Option, sort_order: SortOrder, ) -> Result> { let unit = unit.unwrap_or_else(|| determine_unit_from_results(results)); diff --git a/src/export/mod.rs b/src/export/mod.rs index e13c1d6..3eb4433 100644 --- a/src/export/mod.rs +++ b/src/export/mod.rs @@ -19,7 +19,7 @@ use self::orgmode::OrgmodeExporter; use crate::benchmark::benchmark_result::BenchmarkResult; use crate::options::SortOrder; use crate::output::console_writeln; -use crate::util::units::Unit; +use crate::quantity::TimeUnit; use anyhow::{Context, Result}; use clap::ArgMatches; @@ -49,7 +49,7 @@ trait Exporter { fn serialize( &self, results: &[BenchmarkResult], - unit: Option, + unit: Option, sort_order: SortOrder, ) -> Result>; } @@ -67,7 +67,7 @@ struct ExporterWithTarget { /// Handles the management of multiple file exporters. pub struct ExportManager { exporters: Vec, - time_unit: Option, + time_unit: Option, sort_order: SortOrder, } @@ -76,7 +76,7 @@ impl ExportManager { /// in the given ArgMatches pub fn from_cli_arguments( matches: &ArgMatches, - time_unit: Option, + time_unit: Option, sort_order: SortOrder, ) -> Result { let mut export_manager = Self { diff --git a/src/export/tests.rs b/src/export/tests.rs index bd7ea79..d57dfec 100644 --- a/src/export/tests.rs +++ b/src/export/tests.rs @@ -2,13 +2,13 @@ use super::Exporter; use crate::benchmark::benchmark_result::BenchmarkResult; use crate::export::asciidoc::AsciidocExporter; use crate::export::orgmode::OrgmodeExporter; -use crate::util::units::Unit; +use crate::quantity::TimeUnit; use crate::{export::markdown::MarkdownExporter, options::SortOrder}; use std::collections::BTreeMap; fn get_output( results: &[BenchmarkResult], - unit: Option, + unit: Option, sort_order: SortOrder, ) -> String { let exporter = E::default(); @@ -206,14 +206,14 @@ fn test_markup_export_manual_ms() { }, ]; - insta::assert_snapshot!(get_output::(&timing_results, Some(Unit::MilliSecond), SortOrder::Command), @r#" + insta::assert_snapshot!(get_output::(&timing_results, Some(TimeUnit::MilliSecond), SortOrder::Command), @r#" | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `sleep 2` | 2005.0 ± 2.0 | 2002.0 | 2008.0 | 18.97 ± 0.29 | | `sleep 0.1` | 105.7 ± 1.6 | 102.3 | 108.0 | 1.00 | "#); - insta::assert_snapshot!(get_output::(&timing_results, Some(Unit::MilliSecond), SortOrder::Command), @r#" + insta::assert_snapshot!(get_output::(&timing_results, Some(TimeUnit::MilliSecond), SortOrder::Command), @r#" [cols="<,>,>,>,>"] |=== | Command @@ -236,7 +236,7 @@ fn test_markup_export_manual_ms() { |=== "#); - insta::assert_snapshot!(get_output::(&timing_results, Some(Unit::MilliSecond), SortOrder::Command), @r#" + insta::assert_snapshot!(get_output::(&timing_results, Some(TimeUnit::MilliSecond), SortOrder::Command), @r#" | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |--+--+--+--+--| | =sleep 2= | 2005.0 ± 2.0 | 2002.0 | 2008.0 | 18.97 ± 0.29 | @@ -280,21 +280,21 @@ fn test_markup_export_manual_s() { }, ]; - insta::assert_snapshot!(get_output::(&results, Some(Unit::Second), SortOrder::Command), @r#" + insta::assert_snapshot!(get_output::(&results, Some(TimeUnit::Second), SortOrder::Command), @r#" | Command | Mean [s] | Min [s] | Max [s] | Relative | |:---|---:|---:|---:|---:| | `sleep 2` | 2.005 ± 0.002 | 2.002 | 2.008 | 18.97 ± 0.29 | | `sleep 0.1` | 0.106 ± 0.002 | 0.102 | 0.108 | 1.00 | "#); - insta::assert_snapshot!(get_output::(&results, Some(Unit::Second), SortOrder::MeanTime), @r#" + insta::assert_snapshot!(get_output::(&results, Some(TimeUnit::Second), SortOrder::MeanTime), @r#" | Command | Mean [s] | Min [s] | Max [s] | Relative | |:---|---:|---:|---:|---:| | `sleep 0.1` | 0.106 ± 0.002 | 0.102 | 0.108 | 1.00 | | `sleep 2` | 2.005 ± 0.002 | 2.002 | 2.008 | 18.97 ± 0.29 | "#); - insta::assert_snapshot!(get_output::(&results, Some(Unit::Second), SortOrder::Command), @r#" + insta::assert_snapshot!(get_output::(&results, Some(TimeUnit::Second), SortOrder::Command), @r#" [cols="<,>,>,>,>"] |=== | Command diff --git a/src/options.rs b/src/options.rs index 89fd3e7..0b902d5 100644 --- a/src/options.rs +++ b/src/options.rs @@ -9,8 +9,7 @@ use clap::ArgMatches; use crate::command::Commands; use crate::error::OptionsError; -use crate::quantity::{second, Time}; -use crate::util::units::Unit; +use crate::quantity::{second, Time, TimeUnit}; use anyhow::Result; @@ -246,7 +245,7 @@ pub struct Options { pub command_output_policies: Vec, /// Which time unit to use when displaying results - pub time_unit: Option, + pub time_unit: Option, } impl Default for Options { @@ -430,11 +429,11 @@ impl Options { } options.time_unit = match matches.get_one::("time-unit").map(|s| s.as_str()) { - Some("µs" | "us" | "microsecond" | "microseconds") => Some(Unit::MicroSecond), - Some("ms" | "millisecond" | "milliseconds") => Some(Unit::MilliSecond), - Some("s" | "second" | "seconds") => Some(Unit::Second), - Some("min" | "minute" | "minutes") => Some(Unit::Minute), - Some("h" | "hour" | "hours") => Some(Unit::Hour), + Some("µs" | "us" | "microsecond" | "microseconds") => Some(TimeUnit::MicroSecond), + Some("ms" | "millisecond" | "milliseconds") => Some(TimeUnit::MilliSecond), + Some("s" | "second" | "seconds") => Some(TimeUnit::Second), + Some("min" | "minute" | "minutes") => Some(TimeUnit::Minute), + Some("h" | "hour" | "hours") => Some(TimeUnit::Hour), _ => None, }; diff --git a/src/outlier_detection.rs b/src/outlier_detection.rs index 50707a7..422481a 100644 --- a/src/outlier_detection.rs +++ b/src/outlier_detection.rs @@ -5,42 +5,17 @@ //! The ASQC Basic References in Quality Control: Statistical Techniques, Edward F. Mykytka, //! Ph.D., Editor. -use crate::util::statistics::median; - /// Minimum modified Z-score for a datapoint to be an outlier. Here, 1.4826 is a factor that /// converts the MAD to an estimator for the standard deviation. The second factor is the number /// of standard deviations. pub const OUTLIER_THRESHOLD: f64 = 1.4826 * 10.0; -/// 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: -/// - -pub fn modified_zscores(xs: &[f64]) -> Vec { - assert!(!xs.is_empty()); - - // Compute sample median: - let x_median = median(xs.iter().copied()); - - // Compute the absolute deviations from the median: - let deviations: Vec = 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() -} - /// Return the number of outliers in a given sample. Outliers are defined as data points with a /// modified Z-score that is larger than `OUTLIER_THRESHOLD`. #[cfg(test)] pub fn num_outliers(xs: &[f64]) -> usize { + use crate::quantity::statistics::modified_zscores; + if xs.is_empty() { return 0; } diff --git a/src/output/format.rs b/src/output/format.rs deleted file mode 100644 index 95a480f..0000000 --- a/src/output/format.rs +++ /dev/null @@ -1,78 +0,0 @@ -use crate::util::units::{Second, Unit}; - -/// 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: Second, unit: Option) -> 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: Second, unit: Option) -> (String, Unit) { - 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: Second, unit: Option) -> (String, Unit) { - if (duration < 0.001 && unit.is_none()) || unit == Some(Unit::MicroSecond) { - (Unit::MicroSecond.format(duration), Unit::MicroSecond) - } else if (duration < 1.0 && unit.is_none()) || unit == Some(Unit::MilliSecond) { - (Unit::MilliSecond.format(duration), Unit::MilliSecond) - } else { - let unit = unit.unwrap_or(Unit::Second); - (unit.format(duration), unit) - } -} - -#[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!(Unit::Second, out_unit); - - let (out_str, out_unit) = format_duration_unit(1.0, None); - - assert_eq!("1.000 s", out_str); - assert_eq!(Unit::Second, out_unit); - - let (out_str, out_unit) = format_duration_unit(0.999, None); - - assert_eq!("999.0 ms", out_str); - assert_eq!(Unit::MilliSecond, out_unit); - - let (out_str, out_unit) = format_duration_unit(0.0005, None); - - assert_eq!("500.0 µs", out_str); - assert_eq!(Unit::MicroSecond, out_unit); - - let (out_str, out_unit) = format_duration_unit(0.0, None); - - assert_eq!("0.0 µs", out_str); - assert_eq!(Unit::MicroSecond, out_unit); - - let (out_str, out_unit) = format_duration_unit(1000.0, None); - - assert_eq!("1000.000 s", out_str); - assert_eq!(Unit::Second, out_unit); -} - -#[test] -fn test_format_duration_unit_with_unit() { - let (out_str, out_unit) = format_duration_unit(1.3, Some(Unit::Second)); - - assert_eq!("1.300 s", out_str); - assert_eq!(Unit::Second, out_unit); - - let (out_str, out_unit) = format_duration_unit(1.3, Some(Unit::MilliSecond)); - - assert_eq!("1300.0 ms", out_str); - assert_eq!(Unit::MilliSecond, out_unit); - - let (out_str, out_unit) = format_duration_unit(1.3, Some(Unit::MicroSecond)); - - assert_eq!("1300000.0 µs", out_str); - assert_eq!(Unit::MicroSecond, out_unit); -} diff --git a/src/output/mod.rs b/src/output/mod.rs index aa2f85b..2f037e1 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -1,4 +1,3 @@ -pub mod format; pub mod progress_bar; pub mod warnings; diff --git a/src/output/warnings.rs b/src/output/warnings.rs index 5e40bc7..5ae7794 100644 --- a/src/output/warnings.rs +++ b/src/output/warnings.rs @@ -1,8 +1,7 @@ use std::fmt; use crate::benchmark::MIN_EXECUTION_TIME; -use crate::output::format::format_duration; -use crate::quantity::{second, Time}; +use crate::quantity::{format_duration, second, Time}; pub struct OutlierWarningOptions { pub warmup_in_use: bool, diff --git a/src/quantity.rs b/src/quantity.rs deleted file mode 100644 index aeaa356..0000000 --- a/src/quantity.rs +++ /dev/null @@ -1,45 +0,0 @@ -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 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, - } -} - -#[cfg(test)] -use uom::si::{information::kibibyte, time::millisecond}; - -#[test] -fn test_time() { - let time = Time::new::(123.4); - assert_eq!(time.get::(), 123.4); - - let time_s = time.get::(); - approx::assert_relative_eq!(time_s, 0.1234); - - let time_us = time.get::(); - approx::assert_relative_eq!(time_us, 123400.0); -} - -#[test] -fn test_information() { - use uom::si::information::pebibyte; - - let information = Information::new::(8.); - assert_eq!(information.get::(), 8192.); - - let information_kib = information.get::(); - assert_eq!(information_kib, 8.); - - let largest_exactly_representable = Information::new::(9_007_199_254_740_992.); - assert_eq!(largest_exactly_representable.get::(), 8.); -} diff --git a/src/quantity/mod.rs b/src/quantity/mod.rs new file mode 100644 index 0000000..ac63722 --- /dev/null +++ b/src/quantity/mod.rs @@ -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) -> 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) -> (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) -> (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::(123.4); + assert_eq!(time.get::(), 123.4); + + let time_s = time.get::(); + approx::assert_relative_eq!(time_s, 0.1234); + + let time_us = time.get::(); + approx::assert_relative_eq!(time_us, 123400.0); +} + +#[test] +fn test_information() { + use uom::si::information::pebibyte; + + let information = Information::new::(8.); + assert_eq!(information.get::(), 8192.); + + let information_kib = information.get::(); + assert_eq!(information_kib, 8.); + + let largest_exactly_representable = Information::new::(9_007_199_254_740_992.); + assert_eq!(largest_exactly_representable.get::(), 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); +} diff --git a/src/util/statistics.rs b/src/quantity/statistics.rs similarity index 86% rename from src/util/statistics.rs rename to src/quantity/statistics.rs index 0802f32..4065777 100644 --- a/src/util/statistics.rs +++ b/src/quantity/statistics.rs @@ -106,6 +106,31 @@ pub fn standard_deviation(values: &[Q], mean: Q) -> Q { 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: +/// - +pub fn modified_zscores(xs: &[f64]) -> Vec { + assert!(!xs.is_empty()); + + // Compute sample median: + let x_median = median(xs.iter().copied()); + + // Compute the absolute deviations from the median: + let deviations: Vec = 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::*; diff --git a/src/quantity/units.rs b/src/quantity/units.rs new file mode 100644 index 0000000..9a7ad35 --- /dev/null +++ b/src/quantity/units.rs @@ -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)); +} diff --git a/src/util/mod.rs b/src/util/mod.rs index e5cdaba..e229c24 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,5 +1,3 @@ pub mod exit_code; pub mod number; pub mod randomized_environment_offset; -pub mod statistics; -pub mod units; diff --git a/src/util/units.rs b/src/util/units.rs deleted file mode 100644 index 65b81dd..0000000 --- a/src/util/units.rs +++ /dev/null @@ -1,61 +0,0 @@ -//! This module contains common units. - -pub type Scalar = f64; - -/// Type alias for unit of time -pub type Second = Scalar; - -/// Supported time units -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Unit { - Second, - MilliSecond, - MicroSecond, - Minute, - Hour, -} - -impl Unit { - /// The abbreviation of the Unit. - pub fn short_name(self) -> String { - match self { - Unit::Second => String::from("s"), - Unit::MilliSecond => String::from("ms"), - Unit::MicroSecond => String::from("µs"), - Unit::Minute => String::from("min"), - Unit::Hour => String::from("h"), - } - } - - /// Returns the Second value formatted for the Unit. - pub fn format(self, value: Second) -> String { - match self { - Unit::Second => format!("{value:.3}"), - Unit::MilliSecond => format!("{:.1}", value * 1e3), - Unit::MicroSecond => format!("{:.1}", value * 1e6), - Unit::Minute => format!("{:.1}", value / 60.0), - Unit::Hour => format!("{:.1}", value / 3600.0), - } - } -} - -#[test] -fn test_unit_short_name() { - assert_eq!("s", Unit::Second.short_name()); - assert_eq!("ms", Unit::MilliSecond.short_name()); - assert_eq!("µs", Unit::MicroSecond.short_name()); - assert_eq!("min", Unit::Minute.short_name()); - assert_eq!("h", Unit::Hour.short_name()); -} - -// Note - the values are rounded when formatted. -#[test] -fn test_unit_format() { - let value: Second = 123.456789; - assert_eq!("123.457", Unit::Second.format(value)); - assert_eq!("123456.8", Unit::MilliSecond.format(value)); - - assert_eq!("1234.6", Unit::MicroSecond.format(0.00123456)); - assert_eq!("2.1", Unit::Minute.format(value)); - assert_eq!("0.0", Unit::Hour.format(value)); -}