1use std::{borrow::Cow, fmt};
6
7use rust_decimal::Decimal;
8
9use crate::{
10 into_caveat, json,
11 warning::{self, IntoCaveat},
12};
13
14pub const SCALE: u32 = 4;
18
19#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
21pub enum WarningKind {
22 ContainsEscapeCodes,
24
25 ExceedsMaximumPossibleValue,
27
28 InvalidType,
30
31 LessThanMinimumPossibleValue,
33
34 Underflow,
36}
37
38impl fmt::Display for WarningKind {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 match self {
41 WarningKind::ContainsEscapeCodes => write!(
42 f,
43 "The value contains escape codes but it does not need them"
44 ),
45 WarningKind::InvalidType => write!(f, "The value should be a number."),
46 WarningKind::ExceedsMaximumPossibleValue => {
47 write!(
48 f,
49 "The value provided exceeds `79,228,162,514,264,337,593,543,950,335`."
50 )
51 }
52 WarningKind::LessThanMinimumPossibleValue => write!(
53 f,
54 "The value provided is less than `-79,228,162,514,264,337,593,543,950,335`."
55 ),
56 WarningKind::Underflow => write!(
57 f,
58 "An underflow is when there are more than 28 fractional digits"
59 ),
60 }
61 }
62}
63
64impl warning::Kind for WarningKind {
65 fn id(&self) -> Cow<'static, str> {
66 match self {
67 WarningKind::ContainsEscapeCodes => "contains_escape_codes".into(),
68 WarningKind::InvalidType => "invalid_type".into(),
69 WarningKind::ExceedsMaximumPossibleValue => "exceeds_maximum_possible_value".into(),
70 WarningKind::LessThanMinimumPossibleValue => "less_than_minimum_possible_value".into(),
71 WarningKind::Underflow => "underflow".into(),
72 }
73 }
74}
75
76into_caveat!(Decimal);
77
78impl json::FromJson<'_, '_> for Decimal {
79 type WarningKind = WarningKind;
80
81 fn from_json(elem: &json::Element<'_>) -> crate::Verdict<Self, Self::WarningKind> {
82 let warnings = warning::Set::new();
83 let value = elem.as_value();
84
85 let Some(s) = value.as_number() else {
86 return warnings.bail(WarningKind::InvalidType, elem);
87 };
88
89 let mut decimal = match Decimal::from_str_exact(s) {
90 Ok(v) => v,
91 Err(err) => {
92 let kind = match err {
93 rust_decimal::Error::ExceedsMaximumPossibleValue => {
94 WarningKind::ExceedsMaximumPossibleValue
95 }
96 rust_decimal::Error::LessThanMinimumPossibleValue => {
97 WarningKind::LessThanMinimumPossibleValue
98 }
99 rust_decimal::Error::Underflow => WarningKind::Underflow,
100 rust_decimal::Error::ConversionTo(_) => unreachable!("This is only triggered when converting to numerical types"),
101 rust_decimal::Error::ErrorString(_) => unreachable!("rust_decimal docs state: This is a legacy/deprecated error type retained for backwards compatibility."),
102 rust_decimal::Error::ScaleExceedsMaximumPrecision(_) => unreachable!("`Decimal::from_str_exact` uses a scale of zero")
103 };
104
105 return warnings.bail(kind, elem);
106 }
107 };
108
109 decimal.rescale(SCALE);
110 Ok(decimal.into_caveat(warnings))
111 }
112}
113
114pub(crate) trait FromDecimal {
115 fn from_decimal(d: Decimal) -> Self;
116}
117
118impl FromDecimal for Decimal {
122 fn from_decimal(mut d: Decimal) -> Self {
123 d.rescale(SCALE);
124 d
125 }
126}
127
128pub trait TruncateDecimal {
132 #[must_use]
136 fn truncate_to_ocpi_scale(self) -> Self;
137}
138
139impl TruncateDecimal for Decimal {
140 fn truncate_to_ocpi_scale(self) -> Self {
141 self.trunc_with_scale(SCALE)
142 }
143}
144
145impl<T: TruncateDecimal> TruncateDecimal for Option<T> {
146 fn truncate_to_ocpi_scale(self) -> Self {
147 self.map(TruncateDecimal::truncate_to_ocpi_scale)
148 }
149}
150
151#[doc(hidden)]
158#[macro_export]
159macro_rules! impl_dec_newtype {
160 ($kind:ident, $unit:literal) => {
161 impl $kind {
162 #[must_use]
163 pub fn zero() -> Self {
164 use num_traits::Zero as _;
165
166 Self(Decimal::zero())
167 }
168
169 pub fn is_zero(&self) -> bool {
170 self.0.is_zero()
171 }
172
173 #[must_use]
175 pub fn rescale(mut self) -> Self {
176 self.0.rescale(number::SCALE);
177 Self(self.0)
178 }
179
180 #[must_use]
181 pub fn round_dp(self, digits: u32) -> Self {
182 Self(self.0.round_dp(digits))
183 }
184 }
185
186 impl $crate::number::FromDecimal for $kind {
187 fn from_decimal(mut d: Decimal) -> Self {
188 d.rescale($crate::number::SCALE);
189 Self(d)
190 }
191 }
192
193 impl $crate::number::TruncateDecimal for $kind {
194 fn truncate_to_ocpi_scale(self) -> Self {
195 Self(self.0.truncate_to_ocpi_scale())
196 }
197 }
198
199 impl std::fmt::Display for $kind {
200 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201 if self.0.is_zero() {
203 write!(f, "0{}", $unit)
204 } else {
205 write!(f, "{:.4}{}", self.0, $unit)
206 }
207 }
208 }
209
210 impl From<$kind> for rust_decimal::Decimal {
213 fn from(value: $kind) -> Self {
214 value.0
215 }
216 }
217
218 #[cfg(test)]
219 impl From<u64> for $kind {
220 fn from(value: u64) -> Self {
221 Self(value.into())
222 }
223 }
224
225 #[cfg(test)]
226 impl From<f64> for $kind {
227 fn from(value: f64) -> Self {
228 Self(Decimal::from_f64_retain(value).unwrap())
229 }
230 }
231
232 #[cfg(test)]
233 impl From<rust_decimal::Decimal> for $kind {
234 fn from(value: rust_decimal::Decimal) -> Self {
235 Self(value)
236 }
237 }
238
239 impl $crate::SaturatingAdd for $kind {
240 fn saturating_add(self, other: Self) -> Self {
241 Self(self.0.saturating_add(other.0))
242 }
243 }
244
245 impl $crate::SaturatingSub for $kind {
246 fn saturating_sub(self, other: Self) -> Self {
247 Self(self.0.saturating_sub(other.0))
248 }
249 }
250
251 impl $crate::json::FromJson<'_, '_> for $kind {
252 type WarningKind = $crate::number::WarningKind;
253
254 fn from_json(elem: &json::Element<'_>) -> $crate::Verdict<Self, Self::WarningKind> {
255 rust_decimal::Decimal::from_json(elem).map(|v| v.map(Self))
256 }
257 }
258
259 #[cfg(test)]
260 impl $crate::test::ApproxEq for $kind {
261 fn approx_eq(&self, other: &Self) -> bool {
262 const TOLERANCE: Decimal = rust_decimal_macros::dec!(0.1);
263 const PRECISION: u32 = 2;
264
265 $crate::test::approx_eq_dec(self.0, other.0, TOLERANCE, PRECISION)
266 }
267 }
268 };
269}
270
271#[cfg(test)]
272mod test {
273 use rust_decimal::Decimal;
274 use rust_decimal_macros::dec;
275
276 use crate::test::{approx_eq_dec, ApproxEq};
277
278 impl ApproxEq for Decimal {
279 fn approx_eq(&self, other: &Self) -> bool {
280 const TOLERANCE: Decimal = dec!(0.1);
281 const PRECISION: u32 = 2;
282
283 approx_eq_dec(*self, *other, TOLERANCE, PRECISION)
284 }
285 }
286}
287
288#[cfg(test)]
289mod test_truncate_to_ocpi {
290 use rust_decimal_macros::dec;
291
292 use super::TruncateDecimal;
293
294 #[test]
295 fn test() {
296 let d = dec!(13.951148000000);
297 assert_eq!(d.truncate_to_ocpi_scale(), dec!(13.9511));
298 }
299}