ord/
decimal.rs

1use super::*;
2
3#[derive(Debug, PartialEq, Copy, Clone, Default, DeserializeFromStr, SerializeDisplay)]
4pub struct Decimal {
5  pub value: u128,
6  pub scale: u8,
7}
8
9impl Decimal {
10  pub fn to_integer(self, divisibility: u8) -> Result<u128> {
11    match divisibility.checked_sub(self.scale) {
12      Some(difference) => Ok(
13        self
14          .value
15          .checked_mul(
16            10u128
17              .checked_pow(u32::from(difference))
18              .context("divisibility out of range")?,
19          )
20          .context("amount out of range")?,
21      ),
22      None => bail!("excessive precision"),
23    }
24  }
25}
26
27impl Display for Decimal {
28  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
29    let magnitude = 10u128.checked_pow(self.scale.into()).ok_or(fmt::Error)?;
30
31    let integer = self.value / magnitude;
32    let mut fraction = self.value % magnitude;
33
34    write!(f, "{integer}")?;
35
36    if fraction > 0 {
37      let mut width = self.scale.into();
38
39      while fraction % 10 == 0 {
40        fraction /= 10;
41        width -= 1;
42      }
43
44      write!(f, ".{fraction:0>width$}", width = width)?;
45    }
46
47    Ok(())
48  }
49}
50
51impl FromStr for Decimal {
52  type Err = Error;
53
54  fn from_str(s: &str) -> Result<Self, Self::Err> {
55    if let Some((integer, decimal)) = s.split_once('.') {
56      if integer.is_empty() && decimal.is_empty() {
57        bail!("empty decimal");
58      }
59
60      let integer = if integer.is_empty() {
61        0
62      } else {
63        integer.parse::<u128>()?
64      };
65
66      let (decimal, scale) = if decimal.is_empty() {
67        (0, 0)
68      } else {
69        let trailing_zeros = decimal.chars().rev().take_while(|c| *c == '0').count();
70        let significant_digits = decimal.chars().count() - trailing_zeros;
71        let decimal = decimal.parse::<u128>()?
72          / 10u128
73            .checked_pow(u32::try_from(trailing_zeros).unwrap())
74            .context("excessive trailing zeros")?;
75        (decimal, u8::try_from(significant_digits).unwrap())
76      };
77
78      Ok(Self {
79        value: integer * 10u128.pow(u32::from(scale)) + decimal,
80        scale,
81      })
82    } else {
83      Ok(Self {
84        value: s.parse::<u128>()?,
85        scale: 0,
86      })
87    }
88  }
89}
90
91#[cfg(test)]
92mod tests {
93  use super::*;
94
95  #[test]
96  fn from_str() {
97    #[track_caller]
98    fn case(s: &str, value: u128, scale: u8) {
99      assert_eq!(s.parse::<Decimal>().unwrap(), Decimal { value, scale });
100    }
101
102    assert_eq!(
103      ".".parse::<Decimal>().unwrap_err().to_string(),
104      "empty decimal",
105    );
106
107    assert_eq!(
108      "a.b".parse::<Decimal>().unwrap_err().to_string(),
109      "invalid digit found in string",
110    );
111
112    assert_eq!(
113      " 0.1 ".parse::<Decimal>().unwrap_err().to_string(),
114      "invalid digit found in string",
115    );
116
117    case("0", 0, 0);
118    case("0.00000", 0, 0);
119    case("1.0", 1, 0);
120    case("1.1", 11, 1);
121    case("1.11", 111, 2);
122    case("1.", 1, 0);
123    case(".1", 1, 1);
124    case("1.10", 11, 1);
125  }
126
127  #[test]
128  fn to_amount() {
129    #[track_caller]
130    fn case(s: &str, divisibility: u8, amount: u128) {
131      assert_eq!(
132        s.parse::<Decimal>()
133          .unwrap()
134          .to_integer(divisibility)
135          .unwrap(),
136        amount,
137      );
138    }
139
140    assert_eq!(
141      Decimal { value: 0, scale: 0 }
142        .to_integer(255)
143        .unwrap_err()
144        .to_string(),
145      "divisibility out of range"
146    );
147
148    assert_eq!(
149      Decimal {
150        value: u128::MAX,
151        scale: 0,
152      }
153      .to_integer(1)
154      .unwrap_err()
155      .to_string(),
156      "amount out of range",
157    );
158
159    assert_eq!(
160      Decimal { value: 1, scale: 1 }
161        .to_integer(0)
162        .unwrap_err()
163        .to_string(),
164      "excessive precision",
165    );
166
167    case("1", 0, 1);
168    case("1.0", 0, 1);
169    case("1.0", 1, 10);
170    case("1.2", 1, 12);
171    case("1.2", 2, 120);
172    case("123.456", 3, 123456);
173    case("123.456", 6, 123456000);
174  }
175
176  #[test]
177  fn to_string() {
178    #[track_caller]
179    fn case(decimal: Decimal, string: &str) {
180      assert_eq!(decimal.to_string(), string);
181      assert_eq!(decimal, string.parse::<Decimal>().unwrap());
182    }
183
184    case(Decimal { value: 0, scale: 0 }, "0");
185    case(Decimal { value: 1, scale: 0 }, "1");
186    case(Decimal { value: 1, scale: 1 }, "0.1");
187    case(
188      Decimal {
189        value: 101,
190        scale: 2,
191      },
192      "1.01",
193    );
194    case(
195      Decimal {
196        value: 1234,
197        scale: 6,
198      },
199      "0.001234",
200    );
201    case(
202      Decimal {
203        value: 12,
204        scale: 0,
205      },
206      "12",
207    );
208    case(
209      Decimal {
210        value: 12,
211        scale: 1,
212      },
213      "1.2",
214    );
215    case(
216      Decimal {
217        value: 12,
218        scale: 2,
219      },
220      "0.12",
221    );
222    case(
223      Decimal {
224        value: 123456,
225        scale: 3,
226      },
227      "123.456",
228    );
229    case(
230      Decimal {
231        value: 123456789,
232        scale: 6,
233      },
234      "123.456789",
235    );
236  }
237}