Switch to unit-safe quantities

This commit is contained in:
David Peter authored and David Peter committed 2026-10-05 15:58:56 +02:00
1 parent 914d5cbc59
commit f9aa299c0f
15 files changed
+399 -287

No files matched your search

+184 -82
View File
@@ -1,103 +1,205 @@
use std::fmt::{self, Display, Formatter};
use std::{
fmt::{self, Display, Formatter},
ops::{Add, Div, Sub},
};
use serde::Serialize;
#[derive(Debug, Clone, Serialize, PartialEq)]
enum MetricPrefix {
Nano,
Micro,
Milli,
None,
pub trait IsQuantity {
type Value;
const IS_TIME: bool;
const METRIC_PREFIX_TIME: i32;
const IS_INFORMATION: bool;
const BINARY_PREFIX_INFORMATION: i32;
fn unsafe_from(value: Self::Value) -> Self;
}
impl Display for MetricPrefix {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
MetricPrefix::Nano => write!(f, "n"),
MetricPrefix::Micro => write!(f, "µ"),
MetricPrefix::Milli => write!(f, "m"),
MetricPrefix::None => Ok(()),
#[derive(Debug, Clone, Copy, Default, Serialize, PartialEq, Eq, PartialOrd)] // TODO: check partialord
pub struct Quantity<
Value,
const IS_TIME: bool,
const METRIC_PREFIX_TIME: i32,
const IS_INFORMATION: bool,
const BINARY_PREFIX_INFORMATION: i32,
>(Value);
impl<
Value,
const IS_TIME: bool,
const METRIC_PREFIX_TIME: i32,
const IS_INFORMATION: bool,
const BINARY_PREFIX_INFORMATION: i32,
> IsQuantity
for Quantity<Value, IS_TIME, METRIC_PREFIX_TIME, IS_INFORMATION, BINARY_PREFIX_INFORMATION>
{
type Value = Value;
const IS_TIME: bool = IS_TIME;
const METRIC_PREFIX_TIME: i32 = METRIC_PREFIX_TIME;
const IS_INFORMATION: bool = IS_INFORMATION;
const BINARY_PREFIX_INFORMATION: i32 = BINARY_PREFIX_INFORMATION;
fn unsafe_from(value: Self::Value) -> Self {
Self(value)
}
}
type Time<const PREFIX: i32> = Quantity<f64, true, PREFIX, false, 0>;
type Information<const PREFIX: i32> = Quantity<u64, false, 0, true, PREFIX>;
pub type Second = Time<0>;
pub type MilliSecond = Time<-3>;
pub type MicroSecond = Time<-6>;
pub type Byte = Information<0>;
pub type KibiByte = Information<10>;
pub type MebiByte = Information<20>;
pub type GibiByte = Information<30>;
pub type TebiByte = Information<40>;
impl<
Value: Copy + Default,
const IS_TIME: bool,
const METRIC_PREFIX_TIME: i32,
const IS_INFORMATION: bool,
const BINARY_PREFIX_INFORMATION: i32,
> Quantity<Value, IS_TIME, METRIC_PREFIX_TIME, IS_INFORMATION, BINARY_PREFIX_INFORMATION>
{
pub const fn new(value: Value) -> Self {
Self(value)
}
pub fn zero() -> Self {
Self::new(Value::default())
}
pub fn value_in<TargetUnit: IsQuantity>(&self) -> Value {
const {
assert!(IS_TIME == TargetUnit::IS_TIME);
assert!(METRIC_PREFIX_TIME == TargetUnit::METRIC_PREFIX_TIME);
assert!(IS_INFORMATION == TargetUnit::IS_INFORMATION);
assert!(BINARY_PREFIX_INFORMATION == TargetUnit::BINARY_PREFIX_INFORMATION);
}
self.0
}
}
#[derive(Debug, Clone, Serialize, PartialEq)]
enum BinaryPrefix {
None,
Kibi,
Mebi,
Gibi,
Tebi,
impl<const METRIC_PREFIX_TIME: i32> Time<METRIC_PREFIX_TIME> {
pub fn convert_to<TargetUnit: IsQuantity<Value = f64>>(&self) -> TargetUnit {
let factor = 10_f64.powi(METRIC_PREFIX_TIME - TargetUnit::METRIC_PREFIX_TIME);
TargetUnit::unsafe_from(self.0 * factor)
}
}
impl Display for BinaryPrefix {
impl<const METRIC_PREFIX_TIME: i32> Add<Time<METRIC_PREFIX_TIME>> for Time<METRIC_PREFIX_TIME> {
type Output = Time<METRIC_PREFIX_TIME>;
fn add(self, rhs: Time<METRIC_PREFIX_TIME>) -> Self::Output {
Time::new(self.0 + rhs.0)
}
}
impl<const METRIC_PREFIX_TIME: i32> Sub<Time<METRIC_PREFIX_TIME>> for Time<METRIC_PREFIX_TIME> {
type Output = Time<METRIC_PREFIX_TIME>;
fn sub(self, rhs: Time<METRIC_PREFIX_TIME>) -> Self::Output {
Time::new(self.0 - rhs.0)
}
}
impl<const METRIC_PREFIX_TIME: i32> Div<Time<METRIC_PREFIX_TIME>> for Time<METRIC_PREFIX_TIME> {
type Output = f64;
fn div(self, rhs: Time<METRIC_PREFIX_TIME>) -> Self::Output {
self.0 / rhs.0
}
}
impl<const METRIC_PREFIX_TIME: i32> Display for Time<METRIC_PREFIX_TIME> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
BinaryPrefix::None => Ok(()),
BinaryPrefix::Kibi => write!(f, "Ki"),
BinaryPrefix::Mebi => write!(f, "Mi"),
BinaryPrefix::Gibi => write!(f, "Gi"),
BinaryPrefix::Tebi => write!(f, "Ti"),
let prefix = match METRIC_PREFIX_TIME {
-6 => "µ",
-3 => "m",
0 => "s",
_ => unreachable!(),
};
write!(f, "{} {}s", self.0, prefix)
}
}
impl<const BINARY_PREFIX_INFORMATION: i32> Display for Information<BINARY_PREFIX_INFORMATION> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let prefix = match BINARY_PREFIX_INFORMATION {
0 => "",
10 => "Ki",
20 => "Mi",
30 => "Gi",
40 => "Ti",
_ => unreachable!(),
};
write!(f, "{} {}B", self.0, prefix)
}
}
macro_rules! quantity_fn {
($name:ident, $unwrapped_values:ident, $body:expr) => {
pub fn $name<
const IS_TIME: bool,
const METRIC_PREFIX_TIME: i32,
const IS_INFORMATION: bool,
const BINARY_PREFIX_INFORMATION: i32,
>(
values: &[Quantity<
f64,
IS_TIME,
METRIC_PREFIX_TIME,
IS_INFORMATION,
BINARY_PREFIX_INFORMATION,
>],
) -> Quantity<f64, IS_TIME, METRIC_PREFIX_TIME, IS_INFORMATION, BINARY_PREFIX_INFORMATION> {
let $unwrapped_values: Vec<f64> = values.iter().map(|v| v.0).collect();
let result_value = $body;
Quantity::new(result_value)
}
}
};
}
#[derive(Debug, Clone, Serialize, PartialEq)]
pub enum Unit {
Second(MetricPrefix),
Byte(BinaryPrefix),
}
pub const SECOND: Unit = Unit::Second(MetricPrefix::None);
pub const MILLISECOND: Unit = Unit::Second(MetricPrefix::Milli);
pub const MICROSECOND: Unit = Unit::Second(MetricPrefix::Micro);
pub const BYTE: Unit = Unit::Byte(BinaryPrefix::None);
impl Display for Unit {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Unit::Second(prefix) => write!(f, "{prefix}s",),
Unit::Byte(prefix) => write!(f, "{prefix}B"),
}
}
}
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct Quantity<Value> {
value: Value,
unit: Unit,
}
impl<Value> Quantity<Value> {
pub fn new(value: Value, unit: Unit) -> Self {
Self { value, unit }
}
}
impl<Value: Display> Display for Quantity<Value> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{} {}", self.value, self.unit)
}
}
pub fn mean(values: &[Quantity<f64>]) -> Quantity<f64> {
statistical::mean(values).unwrap()
}
pub type Time = Quantity<f64>;
pub type Memory = Quantity<u64>;
quantity_fn!(mean, values, statistical::mean(&values));
quantity_fn!(median, values, statistical::median(&values));
quantity_fn!(min, values, crate::util::min_max::min(&values));
quantity_fn!(max, values, crate::util::min_max::max(&values));
quantity_fn!(standard_deviation, values, {
let mean_value = statistical::mean(&values);
statistical::standard_deviation(&values, Some(mean_value))
});
#[test]
fn basic() {
let time = Time {
value: 123.4,
unit: Unit::Second(MetricPrefix::Milli),
};
fn test_quantity() {
let time = MilliSecond::new(123.4);
assert_eq!(time.value_in::<MilliSecond>(), 123.4);
let time_s = time.convert_to::<Second>();
approx::assert_relative_eq!(time_s.value_in::<Second>(), 0.1234);
let time_us = time.convert_to::<MicroSecond>();
approx::assert_relative_eq!(time_us.value_in::<MicroSecond>(), 123400.0);
}
#[test]
fn test_format() {
let time = MilliSecond::new(123.4);
assert_eq!(time.to_string(), "123.4 ms");
let peak_memory_usage = Memory {
value: 8,
unit: Unit::Byte(BinaryPrefix::Kibi),
};
let peak_memory_usage = KibiByte::new(8);
assert_eq!(peak_memory_usage.to_string(), "8 KiB");
}
#[test]
fn test_mean() {
let values = vec![MilliSecond::new(123.4), MilliSecond::new(234.5)];
let result = mean(&values);
assert_eq!(result.to_string(), "178.95 ms");
}