1use 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)]
109#[allow(clippy::struct_field_names)]
114pub struct ShelfGeometry {
115 depth_metres: f64,
116 horizontal_spacing_metres: f64,
117 vertical_spacing_metres: f64,
118 bottom_elevation_metres: f64,
119 top_elevation_metres: f64,
120 door_clearance_metres: f64,
121}
122
123impl ShelfGeometry {
124 pub fn try_new(
126 depth_metres: f64,
127 horizontal_spacing_metres: f64,
128 vertical_spacing_metres: f64,
129 bottom_elevation_metres: f64,
130 top_elevation_metres: f64,
131 door_clearance_metres: f64,
132 ) -> Result<Self, LinearQuantityError> {
133 let positive = |v: f64| v.is_finite() && v > 0.0;
134 let non_negative = |v: f64| v.is_finite() && v >= 0.0;
135 if !positive(depth_metres)
136 || !positive(horizontal_spacing_metres)
137 || !positive(vertical_spacing_metres)
138 || !non_negative(bottom_elevation_metres)
139 || !non_negative(door_clearance_metres)
140 || !top_elevation_metres.is_finite()
141 || top_elevation_metres <= bottom_elevation_metres
142 {
143 return Err(LinearQuantityError::InvalidGeometry);
144 }
145 Ok(Self {
146 depth_metres,
147 horizontal_spacing_metres,
148 vertical_spacing_metres,
149 bottom_elevation_metres,
150 top_elevation_metres,
151 door_clearance_metres,
152 })
153 }
154 pub fn depth_metres(&self) -> f64 {
155 self.depth_metres
156 }
157 pub fn horizontal_spacing_metres(&self) -> f64 {
158 self.horizontal_spacing_metres
159 }
160 pub fn vertical_spacing_metres(&self) -> f64 {
161 self.vertical_spacing_metres
162 }
163 pub fn bottom_elevation_metres(&self) -> f64 {
164 self.bottom_elevation_metres
165 }
166 pub fn top_elevation_metres(&self) -> f64 {
167 self.top_elevation_metres
168 }
169 pub fn door_clearance_metres(&self) -> f64 {
170 self.door_clearance_metres
171 }
172}
173
174#[derive(Clone, Copy, Debug, PartialEq)]
176#[non_exhaustive]
177pub enum LinearQuantityKind {
178 ShelfRunningLength(ShelfGeometry),
180}
181
182impl LinearQuantityKind {
183 pub fn as_str(self) -> &'static str {
184 match self {
185 LinearQuantityKind::ShelfRunningLength(_) => "shelf-running-length",
186 }
187 }
188}
189
190#[derive(Clone, Debug, PartialEq)]
196pub struct LinearQuantityRequest {
197 scope: ObjectId,
198 kind: LinearQuantityKind,
199 doors: Vec<ObjectId>,
200}
201
202impl LinearQuantityRequest {
203 pub fn new(scope: ObjectId, kind: LinearQuantityKind) -> Self {
205 Self {
206 scope,
207 kind,
208 doors: Vec::new(),
209 }
210 }
211 #[must_use]
214 pub fn with_doors(mut self, doors: impl IntoIterator<Item = ObjectId>) -> Self {
215 let mut doors: Vec<ObjectId> = doors.into_iter().collect();
216 doors.sort();
217 doors.dedup();
218 self.doors = doors;
219 self
220 }
221 pub fn scope(&self) -> &ObjectId {
222 &self.scope
223 }
224 pub fn kind(&self) -> LinearQuantityKind {
225 self.kind
226 }
227 pub fn doors(&self) -> &[ObjectId] {
229 &self.doors
230 }
231}
232
233#[derive(Clone, Debug, PartialEq)]
235pub struct LinearQuantityEvidence {
236 request: LinearQuantityRequest,
237 measured: LinearInterval,
238 clear_height: Option<LinearInterval>,
239 evidence: Evidence,
240}
241
242impl LinearQuantityEvidence {
243 pub fn try_new(
246 request: LinearQuantityRequest,
247 measured: LinearInterval,
248 evidence: Evidence,
249 ) -> Result<Self, LinearQuantityError> {
250 if !reviewable_exact_evidence(&evidence) {
251 return Err(LinearQuantityError::InexactEvidence);
252 }
253 Ok(Self {
254 request,
255 measured,
256 clear_height: None,
257 evidence,
258 })
259 }
260 #[must_use]
263 pub fn with_clear_height(mut self, clear_height: LinearInterval) -> Self {
264 self.clear_height = Some(clear_height);
265 self
266 }
267 pub fn clear_height(&self) -> Option<LinearInterval> {
269 self.clear_height
270 }
271 pub fn request(&self) -> &LinearQuantityRequest {
272 &self.request
273 }
274 pub fn measured(&self) -> LinearInterval {
275 self.measured
276 }
277 pub fn evidence(&self) -> &Evidence {
278 &self.evidence
279 }
280}
281
282pub trait LinearQuantityService: Send + Sync + 'static {
286 fn measure_linear_quantity(
287 &self,
288 request: &LinearQuantityRequest,
289 ) -> Result<LinearQuantityEvidence, LinearQuantityError>;
290}
291
292#[derive(Clone)]
294pub struct LinearQuantityServiceHandle(Arc<dyn LinearQuantityService>);
295
296impl LinearQuantityServiceHandle {
297 pub fn new(service: Arc<dyn LinearQuantityService>) -> Self {
298 Self(service)
299 }
300 pub fn measure_linear_quantity(
301 &self,
302 request: &LinearQuantityRequest,
303 ) -> Result<LinearQuantityEvidence, LinearQuantityError> {
304 self.0.measure_linear_quantity(request)
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
313 fn doors_are_carried_sorted_and_once() {
314 let source = axioval_ir::SourceId::new("cad", "m").unwrap();
315 let id = |local: &str| ObjectId::new(source.clone(), local).unwrap();
316 let shelf = ShelfGeometry::try_new(0.3, 1.0, 0.4, 0.0, 2.0, 0.9).unwrap();
317 let request =
318 LinearQuantityRequest::new(id("room"), LinearQuantityKind::ShelfRunningLength(shelf))
319 .with_doors([id("d2"), id("d1"), id("d2")]);
320 assert_eq!(request.doors(), [id("d1"), id("d2")]);
321 }
322
323 #[test]
324 fn interval_rejects_inverted_negative_and_non_finite_bounds() {
325 assert!(LinearInterval::try_new(2.0, 1.0).is_err());
326 assert!(LinearInterval::try_new(-1.0, 1.0).is_err());
327 assert!(LinearInterval::try_new(0.0, f64::NAN).is_err());
328 assert!(LinearInterval::try_new(0.0, f64::INFINITY).is_err());
329 assert!(LinearInterval::try_new(0.0, 0.0).is_ok());
330 }
331
332 #[test]
336 fn straddling_interval_is_neither_pass_nor_definite_failure() {
337 let straddles = LinearInterval::try_new(9.0, 11.0).unwrap();
338 assert!(!straddles.definitely_at_least(10.0));
339 assert!(!straddles.definitely_below(10.0));
340 assert!(!straddles.is_exact());
341
342 let clears = LinearInterval::exact(10.0).unwrap();
343 assert!(clears.definitely_at_least(10.0));
344 assert!(!clears.definitely_below(10.0));
345 assert!(clears.is_exact());
346 }
347}