Skip to main content

qs_instruments/
grid.rs

1use serde::{Deserialize, Serialize};
2
3use crate::{Decimal, DecimalError, PositiveDecimal};
4
5/// An exact decimal grid with an origin and positive step.
6#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
7#[serde(deny_unknown_fields)]
8pub struct DecimalGrid {
9    pub origin: Decimal,
10    pub step: PositiveDecimal,
11}
12
13impl DecimalGrid {
14    pub fn new(origin: Decimal, step: PositiveDecimal) -> Self {
15        Self { origin, step }
16    }
17
18    pub fn contains(&self, value: Decimal) -> Result<bool, GridError> {
19        let delta = value.checked_sub(self.origin)?;
20        let (delta, step, _) = delta.aligned_coefficients(self.step.get())?;
21        Ok(delta % step == 0)
22    }
23
24    pub fn adjust(
25        &self,
26        value: Decimal,
27        rounding: GridRounding,
28    ) -> Result<GridAdjustment<Decimal>, GridError> {
29        let delta = value.checked_sub(self.origin)?;
30        let (delta, step, scale) = delta.aligned_coefficients(self.step.get())?;
31        let quotient = delta.div_euclid(step);
32        let remainder = delta.rem_euclid(step);
33        if remainder == 0 {
34            return Ok(GridAdjustment {
35                requested: value,
36                adjusted: value,
37                direction: AdjustmentDirection::Unchanged,
38            });
39        }
40        if rounding == GridRounding::Reject {
41            return Err(GridError::OffGrid { value });
42        }
43
44        let adjusted_quotient = match rounding {
45            GridRounding::Floor => quotient,
46            GridRounding::Ceil => quotient.checked_add(1).ok_or(DecimalError::Overflow)?,
47            GridRounding::Reject => unreachable!(),
48        };
49        let offset = adjusted_quotient
50            .checked_mul(step)
51            .ok_or(DecimalError::Overflow)?;
52        let adjusted = self.origin.checked_add(Decimal::new(offset, scale)?)?;
53        let direction = match adjusted.cmp(&value) {
54            std::cmp::Ordering::Less => AdjustmentDirection::Down,
55            std::cmp::Ordering::Equal => AdjustmentDirection::Unchanged,
56            std::cmp::Ordering::Greater => AdjustmentDirection::Up,
57        };
58
59        Ok(GridAdjustment {
60            requested: value,
61            adjusted,
62            direction,
63        })
64    }
65}
66
67/// Explicit handling for a value outside a declared grid.
68#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
69#[serde(rename_all = "snake_case")]
70pub enum GridRounding {
71    Reject,
72    Floor,
73    Ceil,
74}
75
76/// Direction taken by a grid adjustment.
77#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
78#[serde(rename_all = "snake_case")]
79pub enum AdjustmentDirection {
80    Unchanged,
81    Down,
82    Up,
83}
84
85/// Auditable requested and adjusted values.
86#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
87#[serde(deny_unknown_fields)]
88pub struct GridAdjustment<T> {
89    pub requested: T,
90    pub adjusted: T,
91    pub direction: AdjustmentDirection,
92}
93
94/// Decimal-grid validation and adjustment failures.
95#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
96pub enum GridError {
97    #[error("value {value} is not on the declared decimal grid")]
98    OffGrid { value: Decimal },
99    #[error(transparent)]
100    Decimal(#[from] DecimalError),
101}