axioval_engine/
linear_quantity.rs1use std::sync::Arc;
13
14use axioval_ir::{Evidence, ObjectId};
15
16use crate::services::reviewable_exact_evidence;
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
20pub enum LinearQuantityError {
21 #[error("linear interval must be finite, non-negative and ordered")]
23 InvalidInterval,
24 #[error("linear quantity evidence must be exact and reviewable")]
26 InexactEvidence,
27 #[error("linear quantity is unavailable for the requested scope")]
29 Unavailable,
30 #[error("shelf geometry must be positive, finite and ordered")]
32 InvalidGeometry,
33}
34
35#[derive(Clone, Copy, Debug, PartialEq)]
37pub struct LinearInterval {
38 lower_metres: f64,
39 upper_metres: f64,
40}
41
42impl LinearInterval {
43 pub fn try_new(lower: f64, upper: f64) -> Result<Self, LinearQuantityError> {
45 let valid = |v: f64| v.is_finite() && v >= 0.0;
46 if !valid(lower) || !valid(upper) || lower > upper {
47 return Err(LinearQuantityError::InvalidInterval);
48 }
49 Ok(Self {
50 lower_metres: lower,
51 upper_metres: upper,
52 })
53 }
54
55 pub fn exact(metres: f64) -> Result<Self, LinearQuantityError> {
57 Self::try_new(metres, metres)
58 }
59
60 pub fn lower_metres(&self) -> f64 {
61 self.lower_metres
62 }
63
64 pub fn upper_metres(&self) -> f64 {
65 self.upper_metres
66 }
67
68 #[allow(clippy::float_cmp)]
74 pub fn is_exact(&self) -> bool {
75 self.lower_metres == self.upper_metres
76 }
77
78 pub fn definitely_at_least(&self, minimum: f64) -> bool {
84 self.lower_metres >= minimum
85 }
86
87 pub fn definitely_below(&self, minimum: f64) -> bool {
89 self.upper_metres < minimum
90 }
91}
92
93#[derive(Clone, Copy, Debug, PartialEq)]
100#[allow(clippy::struct_field_names)]
105pub struct ShelfGeometry {
106 depth_metres: f64,
107 horizontal_spacing_metres: f64,
108 vertical_spacing_metres: f64,
109 bottom_elevation_metres: f64,
110 top_elevation_metres: f64,
111 door_clearance_metres: f64,
112}
113
114impl ShelfGeometry {
115 pub fn try_new(
117 depth_metres: f64,
118 horizontal_spacing_metres: f64,
119 vertical_spacing_metres: f64,
120 bottom_elevation_metres: f64,
121 top_elevation_metres: f64,
122 door_clearance_metres: f64,
123 ) -> Result<Self, LinearQuantityError> {
124 let positive = |v: f64| v.is_finite() && v > 0.0;
125 let non_negative = |v: f64| v.is_finite() && v >= 0.0;
126 if !positive(depth_metres)
127 || !positive(horizontal_spacing_metres)
128 || !positive(vertical_spacing_metres)
129 || !non_negative(bottom_elevation_metres)
130 || !non_negative(door_clearance_metres)
131 || !top_elevation_metres.is_finite()
132 || top_elevation_metres <= bottom_elevation_metres
133 {
134 return Err(LinearQuantityError::InvalidGeometry);
135 }
136 Ok(Self {
137 depth_metres,
138 horizontal_spacing_metres,
139 vertical_spacing_metres,
140 bottom_elevation_metres,
141 top_elevation_metres,
142 door_clearance_metres,
143 })
144 }
145 pub fn depth_metres(&self) -> f64 {
146 self.depth_metres
147 }
148 pub fn horizontal_spacing_metres(&self) -> f64 {
149 self.horizontal_spacing_metres
150 }
151 pub fn vertical_spacing_metres(&self) -> f64 {
152 self.vertical_spacing_metres
153 }
154 pub fn bottom_elevation_metres(&self) -> f64 {
155 self.bottom_elevation_metres
156 }
157 pub fn top_elevation_metres(&self) -> f64 {
158 self.top_elevation_metres
159 }
160 pub fn door_clearance_metres(&self) -> f64 {
161 self.door_clearance_metres
162 }
163}
164
165#[derive(Clone, Copy, Debug, PartialEq)]
167#[non_exhaustive]
168pub enum LinearQuantityKind {
169 ShelfRunningLength(ShelfGeometry),
171}
172
173impl LinearQuantityKind {
174 pub fn as_str(self) -> &'static str {
175 match self {
176 LinearQuantityKind::ShelfRunningLength(_) => "shelf-running-length",
177 }
178 }
179}
180
181#[derive(Clone, Debug, PartialEq)]
183pub struct LinearQuantityRequest {
184 scope: ObjectId,
185 kind: LinearQuantityKind,
186}
187
188impl LinearQuantityRequest {
189 pub fn new(scope: ObjectId, kind: LinearQuantityKind) -> Self {
190 Self { scope, kind }
191 }
192 pub fn scope(&self) -> &ObjectId {
193 &self.scope
194 }
195 pub fn kind(&self) -> LinearQuantityKind {
196 self.kind
197 }
198}
199
200#[derive(Clone, Debug, PartialEq)]
202pub struct LinearQuantityEvidence {
203 request: LinearQuantityRequest,
204 measured: LinearInterval,
205 evidence: Evidence,
206}
207
208impl LinearQuantityEvidence {
209 pub fn try_new(
212 request: LinearQuantityRequest,
213 measured: LinearInterval,
214 evidence: Evidence,
215 ) -> Result<Self, LinearQuantityError> {
216 if !reviewable_exact_evidence(&evidence) {
217 return Err(LinearQuantityError::InexactEvidence);
218 }
219 Ok(Self {
220 request,
221 measured,
222 evidence,
223 })
224 }
225 pub fn request(&self) -> &LinearQuantityRequest {
226 &self.request
227 }
228 pub fn measured(&self) -> LinearInterval {
229 self.measured
230 }
231 pub fn evidence(&self) -> &Evidence {
232 &self.evidence
233 }
234}
235
236pub trait LinearQuantityService: Send + Sync + 'static {
240 fn measure_linear_quantity(
241 &self,
242 request: &LinearQuantityRequest,
243 ) -> Result<LinearQuantityEvidence, LinearQuantityError>;
244}
245
246#[derive(Clone)]
248pub struct LinearQuantityServiceHandle(Arc<dyn LinearQuantityService>);
249
250impl LinearQuantityServiceHandle {
251 pub fn new(service: Arc<dyn LinearQuantityService>) -> Self {
252 Self(service)
253 }
254 pub fn measure_linear_quantity(
255 &self,
256 request: &LinearQuantityRequest,
257 ) -> Result<LinearQuantityEvidence, LinearQuantityError> {
258 self.0.measure_linear_quantity(request)
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[test]
267 fn interval_rejects_inverted_negative_and_non_finite_bounds() {
268 assert!(LinearInterval::try_new(2.0, 1.0).is_err());
269 assert!(LinearInterval::try_new(-1.0, 1.0).is_err());
270 assert!(LinearInterval::try_new(0.0, f64::NAN).is_err());
271 assert!(LinearInterval::try_new(0.0, f64::INFINITY).is_err());
272 assert!(LinearInterval::try_new(0.0, 0.0).is_ok());
273 }
274
275 #[test]
279 fn straddling_interval_is_neither_pass_nor_definite_failure() {
280 let straddles = LinearInterval::try_new(9.0, 11.0).unwrap();
281 assert!(!straddles.definitely_at_least(10.0));
282 assert!(!straddles.definitely_below(10.0));
283 assert!(!straddles.is_exact());
284
285 let clears = LinearInterval::exact(10.0).unwrap();
286 assert!(clears.definitely_at_least(10.0));
287 assert!(!clears.definitely_below(10.0));
288 assert!(clears.is_exact());
289 }
290}