use std::sync::Arc;
use axioval_ir::{Evidence, ObjectId};
use crate::services::reviewable_exact_evidence;
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum LinearQuantityError {
#[error("linear interval must be finite, non-negative and ordered")]
InvalidInterval,
#[error("linear quantity evidence must be exact and reviewable")]
InexactEvidence,
#[error("linear quantity is unavailable for the requested scope")]
Unavailable,
#[error("shelf geometry must be positive, finite and ordered")]
InvalidGeometry,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LinearInterval {
lower_metres: f64,
upper_metres: f64,
}
impl LinearInterval {
pub fn try_new(lower: f64, upper: f64) -> Result<Self, LinearQuantityError> {
let valid = |v: f64| v.is_finite() && v >= 0.0;
if !valid(lower) || !valid(upper) || lower > upper {
return Err(LinearQuantityError::InvalidInterval);
}
Ok(Self {
lower_metres: lower,
upper_metres: upper,
})
}
pub fn exact(metres: f64) -> Result<Self, LinearQuantityError> {
Self::try_new(metres, metres)
}
pub fn lower_metres(&self) -> f64 {
self.lower_metres
}
pub fn upper_metres(&self) -> f64 {
self.upper_metres
}
#[allow(clippy::float_cmp)]
pub fn is_exact(&self) -> bool {
self.lower_metres == self.upper_metres
}
pub fn definitely_at_least(&self, minimum: f64) -> bool {
self.lower_metres >= minimum
}
pub fn definitely_below(&self, minimum: f64) -> bool {
self.upper_metres < minimum
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[allow(clippy::struct_field_names)]
pub struct ShelfGeometry {
depth_metres: f64,
horizontal_spacing_metres: f64,
vertical_spacing_metres: f64,
bottom_elevation_metres: f64,
top_elevation_metres: f64,
door_clearance_metres: f64,
}
impl ShelfGeometry {
pub fn try_new(
depth_metres: f64,
horizontal_spacing_metres: f64,
vertical_spacing_metres: f64,
bottom_elevation_metres: f64,
top_elevation_metres: f64,
door_clearance_metres: f64,
) -> Result<Self, LinearQuantityError> {
let positive = |v: f64| v.is_finite() && v > 0.0;
let non_negative = |v: f64| v.is_finite() && v >= 0.0;
if !positive(depth_metres)
|| !positive(horizontal_spacing_metres)
|| !positive(vertical_spacing_metres)
|| !non_negative(bottom_elevation_metres)
|| !non_negative(door_clearance_metres)
|| !top_elevation_metres.is_finite()
|| top_elevation_metres <= bottom_elevation_metres
{
return Err(LinearQuantityError::InvalidGeometry);
}
Ok(Self {
depth_metres,
horizontal_spacing_metres,
vertical_spacing_metres,
bottom_elevation_metres,
top_elevation_metres,
door_clearance_metres,
})
}
pub fn depth_metres(&self) -> f64 {
self.depth_metres
}
pub fn horizontal_spacing_metres(&self) -> f64 {
self.horizontal_spacing_metres
}
pub fn vertical_spacing_metres(&self) -> f64 {
self.vertical_spacing_metres
}
pub fn bottom_elevation_metres(&self) -> f64 {
self.bottom_elevation_metres
}
pub fn top_elevation_metres(&self) -> f64 {
self.top_elevation_metres
}
pub fn door_clearance_metres(&self) -> f64 {
self.door_clearance_metres
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum LinearQuantityKind {
ShelfRunningLength(ShelfGeometry),
}
impl LinearQuantityKind {
pub fn as_str(self) -> &'static str {
match self {
LinearQuantityKind::ShelfRunningLength(_) => "shelf-running-length",
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct LinearQuantityRequest {
scope: ObjectId,
kind: LinearQuantityKind,
}
impl LinearQuantityRequest {
pub fn new(scope: ObjectId, kind: LinearQuantityKind) -> Self {
Self { scope, kind }
}
pub fn scope(&self) -> &ObjectId {
&self.scope
}
pub fn kind(&self) -> LinearQuantityKind {
self.kind
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct LinearQuantityEvidence {
request: LinearQuantityRequest,
measured: LinearInterval,
evidence: Evidence,
}
impl LinearQuantityEvidence {
pub fn try_new(
request: LinearQuantityRequest,
measured: LinearInterval,
evidence: Evidence,
) -> Result<Self, LinearQuantityError> {
if !reviewable_exact_evidence(&evidence) {
return Err(LinearQuantityError::InexactEvidence);
}
Ok(Self {
request,
measured,
evidence,
})
}
pub fn request(&self) -> &LinearQuantityRequest {
&self.request
}
pub fn measured(&self) -> LinearInterval {
self.measured
}
pub fn evidence(&self) -> &Evidence {
&self.evidence
}
}
pub trait LinearQuantityService: Send + Sync + 'static {
fn measure_linear_quantity(
&self,
request: &LinearQuantityRequest,
) -> Result<LinearQuantityEvidence, LinearQuantityError>;
}
#[derive(Clone)]
pub struct LinearQuantityServiceHandle(Arc<dyn LinearQuantityService>);
impl LinearQuantityServiceHandle {
pub fn new(service: Arc<dyn LinearQuantityService>) -> Self {
Self(service)
}
pub fn measure_linear_quantity(
&self,
request: &LinearQuantityRequest,
) -> Result<LinearQuantityEvidence, LinearQuantityError> {
self.0.measure_linear_quantity(request)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn interval_rejects_inverted_negative_and_non_finite_bounds() {
assert!(LinearInterval::try_new(2.0, 1.0).is_err());
assert!(LinearInterval::try_new(-1.0, 1.0).is_err());
assert!(LinearInterval::try_new(0.0, f64::NAN).is_err());
assert!(LinearInterval::try_new(0.0, f64::INFINITY).is_err());
assert!(LinearInterval::try_new(0.0, 0.0).is_ok());
}
#[test]
fn straddling_interval_is_neither_pass_nor_definite_failure() {
let straddles = LinearInterval::try_new(9.0, 11.0).unwrap();
assert!(!straddles.definitely_at_least(10.0));
assert!(!straddles.definitely_below(10.0));
assert!(!straddles.is_exact());
let clears = LinearInterval::exact(10.0).unwrap();
assert!(clears.definitely_at_least(10.0));
assert!(!clears.definitely_below(10.0));
assert!(clears.is_exact());
}
}