use crate::units::{Capacity, Dimensionless};
use std::cmp::Ordering;
use std::ops::{Add, Sub};
#[derive(Clone, PartialEq, Copy, Debug)]
pub enum AssetCapacity {
Continuous(Capacity),
Discrete(u32, Capacity),
}
impl AssetCapacity {
pub fn min(self, other: AssetCapacity) -> AssetCapacity {
match self.partial_cmp(&other) {
None => panic!("Comparing invalid AssetCapacity values ({self:?} and {other:?})"),
Some(Ordering::Greater) => other,
_ => self,
}
}
}
impl Add for AssetCapacity {
type Output = Self;
fn add(self, rhs: AssetCapacity) -> Self {
match (self, rhs) {
(AssetCapacity::Continuous(cap1), AssetCapacity::Continuous(cap2)) => {
AssetCapacity::Continuous(cap1 + cap2)
}
(AssetCapacity::Discrete(units1, size1), AssetCapacity::Discrete(units2, size2)) => {
Self::check_same_unit_size(size1, size2);
AssetCapacity::Discrete(units1 + units2, size1)
}
_ => panic!("Cannot add different types of AssetCapacity ({self:?} and {rhs:?})"),
}
}
}
impl Sub for AssetCapacity {
type Output = Self;
fn sub(self, rhs: AssetCapacity) -> Self {
match (self, rhs) {
(AssetCapacity::Continuous(cap1), AssetCapacity::Continuous(cap2)) => {
AssetCapacity::Continuous((cap1 - cap2).max(Capacity(0.0)))
}
(AssetCapacity::Discrete(units1, size1), AssetCapacity::Discrete(units2, size2)) => {
Self::check_same_unit_size(size1, size2);
AssetCapacity::Discrete(units1 - units2.min(units1), size1)
}
_ => panic!("Cannot subtract different types of AssetCapacity ({self:?} and {rhs:?})"),
}
}
}
impl PartialOrd for AssetCapacity {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match (self, other) {
(AssetCapacity::Continuous(a), AssetCapacity::Continuous(b)) => a.partial_cmp(b),
(AssetCapacity::Discrete(units1, size1), AssetCapacity::Discrete(units2, size2)) => {
(*size1 == *size2).then(|| units1.cmp(units2))
}
_ => None,
}
}
}
impl AssetCapacity {
fn check_same_unit_size(size1: Capacity, size2: Capacity) {
assert_eq!(
size1, size2,
"Can't perform operation on capacities with different unit sizes ({size1} and {size2})",
);
}
pub fn from_capacity(capacity: Capacity, unit_size: Option<Capacity>) -> Self {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
match unit_size {
Some(size) => {
let num_units = (capacity / size).value().ceil() as u32;
AssetCapacity::Discrete(num_units, size)
}
None => AssetCapacity::Continuous(capacity),
}
}
pub fn from_capacity_floor(capacity: Capacity, unit_size: Option<Capacity>) -> Self {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
match unit_size {
Some(size) => {
let num_units = (capacity / size).value().floor() as u32;
AssetCapacity::Discrete(num_units, size)
}
None => AssetCapacity::Continuous(capacity),
}
}
pub fn total_capacity(&self) -> Capacity {
match self {
AssetCapacity::Continuous(cap) => *cap,
AssetCapacity::Discrete(units, size) => *size * Dimensionless(*units as f64),
}
}
pub fn n_units(&self) -> Option<u32> {
match self {
AssetCapacity::Continuous(_) => None,
AssetCapacity::Discrete(units, _) => Some(*units),
}
}
pub fn assert_same_type(&self, other: AssetCapacity) {
assert!(
matches!(self, AssetCapacity::Continuous(_))
== matches!(other, AssetCapacity::Continuous(_)),
"Cannot change capacity type"
);
}
pub fn apply_limit_factor(self, limit_factor: Dimensionless) -> Self {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
match self {
AssetCapacity::Continuous(cap) => AssetCapacity::Continuous(cap * limit_factor),
AssetCapacity::Discrete(units, size) => {
let new_units = (units as f64 * limit_factor.value()).ceil() as u32;
AssetCapacity::Discrete(new_units, size)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::units::{Capacity, Dimensionless};
use rstest::rstest;
#[rstest]
#[case::exact_multiple(Capacity(12.0), Some(Capacity(4.0)), Some(3), Capacity(12.0))]
#[case::rounded_up(Capacity(11.0), Some(Capacity(4.0)), Some(3), Capacity(12.0))]
#[case::unit_size_greater_than_capacity(
Capacity(3.0),
Some(Capacity(4.0)),
Some(1),
Capacity(4.0)
)]
#[case::continuous(Capacity(5.5), None, None, Capacity(5.5))]
fn from_capacity(
#[case] capacity: Capacity,
#[case] unit_size: Option<Capacity>,
#[case] expected_n: Option<u32>,
#[case] expected_total: Capacity,
) {
let got = AssetCapacity::from_capacity(capacity, unit_size);
assert_eq!(got.n_units(), expected_n);
assert_eq!(got.total_capacity(), expected_total);
}
#[rstest]
#[case::exact_multiple(Capacity(12.0), Some(Capacity(4.0)), Some(3), Capacity(12.0))]
#[case::rounded_down(Capacity(11.0), Some(Capacity(4.0)), Some(2), Capacity(8.0))]
#[case::unit_size_greater_than_capacity(
Capacity(3.0),
Some(Capacity(4.0)),
Some(0),
Capacity(0.0)
)]
#[case::continuous(Capacity(5.5), None, None, Capacity(5.5))]
fn from_capacity_floor(
#[case] capacity: Capacity,
#[case] unit_size: Option<Capacity>,
#[case] expected_n: Option<u32>,
#[case] expected_total: Capacity,
) {
let got = AssetCapacity::from_capacity_floor(capacity, unit_size);
assert_eq!(got.n_units(), expected_n);
assert_eq!(got.total_capacity(), expected_total);
}
#[rstest]
#[case::round_up(3u32, Capacity(4.0), Dimensionless(0.5), 2u32)]
#[case::exact(3u32, Capacity(4.0), Dimensionless(0.33), 1u32)]
fn apply_limit_factor(
#[case] start_units: u32,
#[case] unit_size: Capacity,
#[case] factor: Dimensionless,
#[case] expected_units: u32,
) {
let orig = AssetCapacity::Discrete(start_units, unit_size);
let got = orig.apply_limit_factor(factor);
assert_eq!(got, AssetCapacity::Discrete(expected_units, unit_size));
}
#[rstest]
#[case::less(
AssetCapacity::Continuous(Capacity(4.0)),
AssetCapacity::Continuous(Capacity(6.0)),
Some(Ordering::Less)
)]
#[case::equal(
AssetCapacity::Continuous(Capacity(4.0)),
AssetCapacity::Continuous(Capacity(4.0)),
Some(Ordering::Equal)
)]
#[case::greater(
AssetCapacity::Continuous(Capacity(6.0)),
AssetCapacity::Continuous(Capacity(4.0)),
Some(Ordering::Greater)
)]
fn partial_cmp_continuous(
#[case] left: AssetCapacity,
#[case] right: AssetCapacity,
#[case] expected: Option<Ordering>,
) {
assert_eq!(left.partial_cmp(&right), expected);
assert_eq!(left == right, expected == Some(Ordering::Equal));
}
#[rstest]
#[case::less(
AssetCapacity::Discrete(2, Capacity(3.0)),
AssetCapacity::Discrete(4, Capacity(3.0)),
Some(Ordering::Less)
)]
#[case::equal(
AssetCapacity::Discrete(4, Capacity(3.0)),
AssetCapacity::Discrete(4, Capacity(3.0)),
Some(Ordering::Equal)
)]
#[case::greater(
AssetCapacity::Discrete(5, Capacity(3.0)),
AssetCapacity::Discrete(4, Capacity(3.0)),
Some(Ordering::Greater)
)]
fn partial_cmp_discrete_with_matching_unit_size(
#[case] left: AssetCapacity,
#[case] right: AssetCapacity,
#[case] expected: Option<Ordering>,
) {
assert_eq!(left.partial_cmp(&right), expected);
assert_eq!(left == right, expected == Some(Ordering::Equal));
}
#[rstest]
#[case::mixed_types(
AssetCapacity::Continuous(Capacity(4.0)),
AssetCapacity::Discrete(4, Capacity(1.0))
)]
#[case::different_unit_sizes(
AssetCapacity::Discrete(4, Capacity(1.0)),
AssetCapacity::Discrete(4, Capacity(2.0))
)]
#[case::nan_continuous(
AssetCapacity::Continuous(Capacity(f64::NAN)),
AssetCapacity::Continuous(Capacity(4.0))
)]
fn partial_cmp_returns_none_for_invalid_comparisons(
#[case] left: AssetCapacity,
#[case] right: AssetCapacity,
) {
assert_eq!(left.partial_cmp(&right), None);
assert!(left != right);
}
#[rstest]
#[case::continuous(
AssetCapacity::Continuous(Capacity(4.0)),
AssetCapacity::Continuous(Capacity(6.0)),
AssetCapacity::Continuous(Capacity(4.0))
)]
#[case::discrete(
AssetCapacity::Discrete(2, Capacity(3.0)),
AssetCapacity::Discrete(4, Capacity(3.0)),
AssetCapacity::Discrete(2, Capacity(3.0))
)]
fn min_returns_smaller_capacity(
#[case] left: AssetCapacity,
#[case] right: AssetCapacity,
#[case] expected: AssetCapacity,
) {
assert_eq!(left.min(right), expected);
}
#[rstest]
#[case::mixed_types(
AssetCapacity::Continuous(Capacity(4.0)),
AssetCapacity::Discrete(4, Capacity(1.0))
)]
#[case::different_unit_sizes(
AssetCapacity::Discrete(4, Capacity(1.0)),
AssetCapacity::Discrete(4, Capacity(2.0))
)]
#[should_panic(expected = "Comparing invalid AssetCapacity values")]
fn min_panics_for_invalid_comparisons(
#[case] left: AssetCapacity,
#[case] right: AssetCapacity,
) {
let _ = left.min(right);
}
}