use core::fmt;
use crate::units::Power;
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Envelope {
pub floor: Power,
pub ceiling: Power,
}
impl Envelope {
pub const UNBOUNDED: Self = Self {
floor: Power::new_const(f64::NEG_INFINITY),
ceiling: Power::new_const(f64::INFINITY),
};
#[must_use]
pub const fn new(floor: Power, ceiling: Power) -> Self {
Self { floor, ceiling }
}
#[must_use]
pub const fn at_most(ceiling: Power) -> Self {
Self {
floor: Power::new_const(f64::NEG_INFINITY),
ceiling,
}
}
#[must_use]
pub const fn at_least(floor: Power) -> Self {
Self {
floor,
ceiling: Power::new_const(f64::INFINITY),
}
}
#[must_use]
pub const fn exactly(value: Power) -> Self {
Self {
floor: value,
ceiling: value,
}
}
#[must_use]
pub fn intersect(self, other: Self) -> Self {
Self {
floor: self.floor.max(other.floor),
ceiling: self.ceiling.min(other.ceiling),
}
}
#[must_use]
pub fn is_empty(self) -> bool {
self.floor > self.ceiling
}
#[must_use]
pub fn contains(self, value: Power) -> bool {
value >= self.floor && value <= self.ceiling
}
#[must_use]
pub fn clamp(self, wanted: Power) -> Power {
if self.is_empty() {
return self.ceiling;
}
wanted.max(self.floor).min(self.ceiling)
}
#[must_use]
pub fn resolve(self) -> Self {
if self.is_empty() {
Self::exactly(self.ceiling)
} else {
self
}
}
#[must_use]
pub fn width(self) -> Power {
if self.is_empty() {
Power::ZERO
} else {
self.ceiling - self.floor
}
}
}
impl fmt::Display for Envelope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match (
self.floor.get().is_infinite(),
self.ceiling.get().is_infinite(),
) {
(true, true) => f.write_str("unbounded"),
(true, false) => write!(f, "≤ {}", self.ceiling),
(false, true) => write!(f, "≥ {}", self.floor),
(false, false) => write!(f, "{} … {}", self.floor, self.ceiling),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn intersection_only_ever_narrows() {
let a = Envelope::new(Power::from_kw(0.0), Power::from_kw(11.0));
let b = Envelope::at_most(Power::from_kw(4.2));
let n = a.intersect(b);
assert_eq!(n.ceiling, Power::from_kw(4.2));
assert_eq!(n.floor, Power::ZERO);
assert!(n.width() <= a.width());
}
#[test]
fn an_unsatisfiable_pair_resolves_towards_the_ceiling() {
let device = Envelope::at_least(Power::from_kw(1.4));
let grid = Envelope::at_most(Power::from_kw(1.0));
let both = device.intersect(grid);
assert!(both.is_empty());
assert_eq!(
both.clamp(Power::from_kw(5.0)),
Power::from_kw(1.0),
"the grid limit wins"
);
assert_eq!(both.resolve(), Envelope::exactly(Power::from_kw(1.0)));
}
#[test]
fn clamping_keeps_a_wanted_value_that_already_fits() {
let e = Envelope::new(Power::ZERO, Power::from_kw(11.0));
assert_eq!(e.clamp(Power::from_kw(4.0)), Power::from_kw(4.0));
assert_eq!(e.clamp(Power::from_kw(20.0)), Power::from_kw(11.0));
assert_eq!(e.clamp(Power::from_kw(-5.0)), Power::ZERO);
}
#[test]
fn unbounded_is_the_identity_of_intersection() {
let e = Envelope::new(Power::from_kw(-5.0), Power::from_kw(5.0));
assert_eq!(e.intersect(Envelope::UNBOUNDED), e);
}
}