1use serde::{Deserialize, Serialize};
2
3use crate::{Decimal, DecimalError, PositiveDecimal};
4
5#[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#[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#[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#[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#[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}