use core::fmt;
use crate::{
MeasurementMeanError, MeasurementStateError, add_count, add_total, checked_scaled_ratio,
mean_total,
};
#[cfg(test)]
mod tests;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MeasurementIntervalError {
SaturatedSamples,
SaturatedTotal,
DecreasingSamples,
DecreasingTotal,
TotalWithoutSamples,
}
impl fmt::Display for MeasurementIntervalError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::SaturatedSamples => "interval sample count has overflowed",
Self::SaturatedTotal => "interval total has overflowed",
Self::DecreasingSamples => "interval sample count decreased",
Self::DecreasingTotal => "interval total decreased",
Self::TotalWithoutSamples => "interval total changed without samples",
})
}
}
impl core::error::Error for MeasurementIntervalError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MeasurementInterval {
samples: u64,
total: u128,
}
impl MeasurementInterval {
#[must_use]
pub const fn samples(self) -> u64 {
self.samples
}
#[must_use]
pub const fn total(self) -> u128 {
self.total
}
#[must_use]
pub const fn mean(self) -> Option<u128> {
mean_total(self.samples, self.total)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct MeasurementAggregate {
samples: u64,
total: u128,
maximum: u64,
samples_saturated: bool,
total_saturated: bool,
}
impl MeasurementAggregate {
pub const fn from_parts(
samples: u64,
total: u128,
maximum: Option<u64>,
samples_saturated: bool,
total_saturated: bool,
) -> Result<Self, MeasurementStateError> {
if samples == 0 {
return if total == 0 && maximum.is_none() && !samples_saturated && !total_saturated {
Ok(Self::EMPTY)
} else {
Err(MeasurementStateError::InvalidEmpty)
};
}
let Some(maximum) = maximum else {
return Err(MeasurementStateError::MissingMaximum);
};
if (samples_saturated && samples != u64::MAX)
|| (total_saturated && (total != u128::MAX || !samples_saturated))
{
return Err(MeasurementStateError::InvalidSaturation);
}
if total < maximum as u128
|| (maximum == 0 && total != 0)
|| (!samples_saturated && total > samples as u128 * maximum as u128)
{
return Err(MeasurementStateError::InvalidTotal);
}
Ok(Self {
samples,
total,
maximum,
samples_saturated,
total_saturated,
})
}
pub const EMPTY: Self = Self {
samples: 0,
total: 0,
maximum: 0,
samples_saturated: false,
total_saturated: false,
};
pub const fn record(&mut self, value: u64) {
add_count(&mut self.samples, &mut self.samples_saturated, 1);
add_total(&mut self.total, &mut self.total_saturated, value as u128);
if value > self.maximum {
self.maximum = value;
}
}
pub const fn merge(&mut self, other: Self) {
add_count(
&mut self.samples,
&mut self.samples_saturated,
other.samples,
);
add_total(&mut self.total, &mut self.total_saturated, other.total);
self.samples_saturated |= other.samples_saturated;
self.total_saturated |= other.total_saturated;
if other.maximum > self.maximum {
self.maximum = other.maximum;
}
}
#[must_use]
pub const fn samples(self) -> u64 {
self.samples
}
#[must_use]
pub const fn total(self) -> u128 {
self.total
}
#[must_use]
pub const fn samples_saturated(self) -> bool {
self.samples_saturated
}
#[must_use]
pub const fn total_saturated(self) -> bool {
self.total_saturated
}
#[must_use]
pub const fn maximum(self) -> Option<u64> {
if self.samples == 0 {
None
} else {
Some(self.maximum)
}
}
pub const fn mean(self) -> Result<Option<u128>, MeasurementMeanError> {
if self.samples_saturated {
return Err(MeasurementMeanError::SaturatedSamples);
}
if self.total_saturated {
return Err(MeasurementMeanError::SaturatedTotal);
}
Ok(mean_total(self.samples, self.total))
}
pub const fn scaled_mean(self, scale: u64) -> Result<Option<u128>, MeasurementMeanError> {
if self.samples_saturated {
return Err(MeasurementMeanError::SaturatedSamples);
}
if self.total_saturated {
return Err(MeasurementMeanError::SaturatedTotal);
}
if self.samples == 0 {
return Ok(None);
}
match checked_scaled_ratio(self.total, self.samples, scale) {
Ok(value) => Ok(Some(value)),
Err(error) => Err(MeasurementMeanError::Projection(error)),
}
}
pub const fn interval_since(
self,
previous: Self,
) -> Result<MeasurementInterval, MeasurementIntervalError> {
if self.samples_saturated || previous.samples_saturated {
return Err(MeasurementIntervalError::SaturatedSamples);
}
if self.total_saturated || previous.total_saturated {
return Err(MeasurementIntervalError::SaturatedTotal);
}
let Some(samples) = self.samples.checked_sub(previous.samples) else {
return Err(MeasurementIntervalError::DecreasingSamples);
};
let Some(total) = self.total.checked_sub(previous.total) else {
return Err(MeasurementIntervalError::DecreasingTotal);
};
if samples == 0 && total != 0 {
return Err(MeasurementIntervalError::TotalWithoutSamples);
}
Ok(MeasurementInterval { samples, total })
}
}