1use core::fmt;
12
13use crate::units::Power;
14
15#[derive(Debug, Clone, Copy, PartialEq)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub struct Envelope {
25 pub floor: Power,
27 pub ceiling: Power,
29}
30
31impl Envelope {
32 pub const UNBOUNDED: Self = Self {
34 floor: Power::new_const(f64::NEG_INFINITY),
35 ceiling: Power::new_const(f64::INFINITY),
36 };
37
38 #[must_use]
40 pub const fn new(floor: Power, ceiling: Power) -> Self {
41 Self { floor, ceiling }
42 }
43
44 #[must_use]
46 pub const fn at_most(ceiling: Power) -> Self {
47 Self {
48 floor: Power::new_const(f64::NEG_INFINITY),
49 ceiling,
50 }
51 }
52
53 #[must_use]
55 pub const fn at_least(floor: Power) -> Self {
56 Self {
57 floor,
58 ceiling: Power::new_const(f64::INFINITY),
59 }
60 }
61
62 #[must_use]
64 pub const fn exactly(value: Power) -> Self {
65 Self {
66 floor: value,
67 ceiling: value,
68 }
69 }
70
71 #[must_use]
74 pub fn intersect(self, other: Self) -> Self {
75 Self {
76 floor: self.floor.max(other.floor),
77 ceiling: self.ceiling.min(other.ceiling),
78 }
79 }
80
81 #[must_use]
83 pub fn is_empty(self) -> bool {
84 self.floor > self.ceiling
85 }
86
87 #[must_use]
89 pub fn contains(self, value: Power) -> bool {
90 value >= self.floor && value <= self.ceiling
91 }
92
93 #[must_use]
99 pub fn clamp(self, wanted: Power) -> Power {
100 if self.is_empty() {
101 return self.ceiling;
102 }
103 wanted.max(self.floor).min(self.ceiling)
104 }
105
106 #[must_use]
109 pub fn resolve(self) -> Self {
110 if self.is_empty() {
111 Self::exactly(self.ceiling)
112 } else {
113 self
114 }
115 }
116
117 #[must_use]
119 pub fn width(self) -> Power {
120 if self.is_empty() {
121 Power::ZERO
122 } else {
123 self.ceiling - self.floor
124 }
125 }
126}
127
128impl fmt::Display for Envelope {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 match (
131 self.floor.get().is_infinite(),
132 self.ceiling.get().is_infinite(),
133 ) {
134 (true, true) => f.write_str("unbounded"),
135 (true, false) => write!(f, "≤ {}", self.ceiling),
136 (false, true) => write!(f, "≥ {}", self.floor),
137 (false, false) => write!(f, "{} … {}", self.floor, self.ceiling),
138 }
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn intersection_only_ever_narrows() {
148 let a = Envelope::new(Power::from_kw(0.0), Power::from_kw(11.0));
149 let b = Envelope::at_most(Power::from_kw(4.2));
150 let n = a.intersect(b);
151 assert_eq!(n.ceiling, Power::from_kw(4.2));
152 assert_eq!(n.floor, Power::ZERO);
153 assert!(n.width() <= a.width());
154 }
155
156 #[test]
157 fn an_unsatisfiable_pair_resolves_towards_the_ceiling() {
158 let device = Envelope::at_least(Power::from_kw(1.4));
160 let grid = Envelope::at_most(Power::from_kw(1.0));
161 let both = device.intersect(grid);
162 assert!(both.is_empty());
163 assert_eq!(
164 both.clamp(Power::from_kw(5.0)),
165 Power::from_kw(1.0),
166 "the grid limit wins"
167 );
168 assert_eq!(both.resolve(), Envelope::exactly(Power::from_kw(1.0)));
169 }
170
171 #[test]
172 fn clamping_keeps_a_wanted_value_that_already_fits() {
173 let e = Envelope::new(Power::ZERO, Power::from_kw(11.0));
174 assert_eq!(e.clamp(Power::from_kw(4.0)), Power::from_kw(4.0));
175 assert_eq!(e.clamp(Power::from_kw(20.0)), Power::from_kw(11.0));
176 assert_eq!(e.clamp(Power::from_kw(-5.0)), Power::ZERO);
177 }
178
179 #[test]
180 fn unbounded_is_the_identity_of_intersection() {
181 let e = Envelope::new(Power::from_kw(-5.0), Power::from_kw(5.0));
182 assert_eq!(e.intersect(Envelope::UNBOUNDED), e);
183 }
184}