1#[cfg(test)]
7pub mod test;
8
9#[cfg(test)]
10mod test_approx_eq;
11
12#[cfg(test)]
13mod test_round_to_ocpi;
14
15#[cfg(test)]
16mod test_from_schema;
17
18use std::{fmt, num::IntErrorKind};
19
20use rust_decimal::Decimal;
21
22use crate::{
23 json, schema,
24 warning::{self, GatherWarnings as _, IntoCaveat as _},
25 FromSchema,
26};
27
28pub const SCALE: u32 = 4;
32
33#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
35pub enum Warning {
36 ContainsEscapeCodes,
38
39 Decimal(String),
41
42 Decode(json::decode::Warning),
44
45 ExceedsMaximumPossibleValue,
47
48 ExcessivePrecision,
50
51 LessThanMinimumPossibleValue,
53
54 Underflow,
56}
57
58impl From<json::decode::Warning> for Warning {
59 fn from(warning: json::decode::Warning) -> Self {
60 Self::Decode(warning)
61 }
62}
63
64impl fmt::Display for Warning {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 match self {
67 Self::ContainsEscapeCodes => f.write_str("The value contains escape codes but it does not need them"),
68 Self::Decimal(msg) => write!(f, "{msg}"),
69 Self::Decode(warning) => fmt::Display::fmt(warning, f),
70 Self::ExcessivePrecision => f.write_str("The number given has more than the four decimal precision required by the OCPI spec."),
71 Self::ExceedsMaximumPossibleValue => {
72 f.write_str("The value provided exceeds `79,228,162,514,264,337,593,543,950,335`.")
73 }
74 Self::LessThanMinimumPossibleValue => f.write_str("The value provided is less than `-79,228,162,514,264,337,593,543,950,335`."),
75 Self::Underflow => f.write_str("An underflow is when there are more than 28 fractional digits"),
76 }
77 }
78}
79
80impl crate::Warning for Warning {
81 fn id(&self) -> warning::Id {
82 match self {
83 Self::ContainsEscapeCodes => warning::Id::from_static("contains_escape_codes"),
84 Self::Decimal(_) => warning::Id::from_static("decimal"),
85 Self::Decode(warning) => warning.id(),
86 Self::ExcessivePrecision => warning::Id::from_static("excessive_precision"),
87 Self::ExceedsMaximumPossibleValue => {
88 warning::Id::from_static("exceeds_maximum_possible_value")
89 }
90 Self::LessThanMinimumPossibleValue => {
91 warning::Id::from_static("less_than_minimum_possible_value")
92 }
93 Self::Underflow => warning::Id::from_static("underflow"),
94 }
95 }
96}
97
98pub(crate) fn int_error_kind_as_str(kind: IntErrorKind) -> &'static str {
99 match kind {
100 IntErrorKind::Empty => "empty",
101 IntErrorKind::InvalidDigit => "invalid digit",
102 IntErrorKind::PosOverflow => "positive overflow",
103 IntErrorKind::NegOverflow => "negative overflow",
104 IntErrorKind::Zero => "zero",
105 _ => "unknown",
106 }
107}
108
109impl<'buf> FromSchema<'buf, schema::Number<'buf>> for Decimal {
110 type Warning = Warning;
111
112 fn from_schema(source: &schema::Number<'buf>) -> crate::Verdict<Self, Self::Warning> {
113 let mut warnings = warning::Set::new();
114
115 let (elem, digits) = match source {
119 schema::Number::Number { elem, digits } => (elem, *digits),
120 schema::Number::StringEncoded { elem, value } => {
121 let pending_str = value.has_escapes(elem).gather_warnings_into(&mut warnings);
122
123 match pending_str {
124 json::PendingStr::NoEscapes(s) => (elem, s),
125 json::PendingStr::HasEscapes(_) => {
126 return warnings.bail(elem, Warning::ContainsEscapeCodes);
127 }
128 }
129 }
130 };
131
132 let decimal = match Decimal::from_str_exact(digits) {
133 Ok(v) => v,
134 Err(err) => {
135 let kind = match err {
136 rust_decimal::Error::ExceedsMaximumPossibleValue => {
137 Warning::ExceedsMaximumPossibleValue
138 }
139 rust_decimal::Error::LessThanMinimumPossibleValue => {
140 Warning::LessThanMinimumPossibleValue
141 }
142 rust_decimal::Error::Underflow => Warning::Underflow,
143 rust_decimal::Error::ConversionTo(_) => {
144 unreachable!("This is only triggered when converting to numerical types")
145 }
146 rust_decimal::Error::ErrorString(msg) => Warning::Decimal(msg),
147 rust_decimal::Error::ScaleExceedsMaximumPrecision(_) => {
148 unreachable!("`Decimal::from_str_exact` uses a scale of zero")
149 }
150 };
151
152 return warnings.bail(elem, kind);
153 }
154 };
155
156 if decimal.scale() > SCALE {
157 warnings.insert(elem, Warning::ExcessivePrecision);
158 }
159
160 Ok(decimal.into_caveat(warnings))
161 }
162}
163
164pub(crate) trait FromDecimal {
165 fn from_decimal(d: Decimal) -> Self;
166}
167
168impl FromDecimal for Decimal {
172 fn from_decimal(mut d: Decimal) -> Self {
173 d.rescale(SCALE);
174 d
175 }
176}
177
178pub trait RoundDecimal {
182 #[must_use]
186 fn round_to_ocpi_scale(self) -> Self;
187}
188
189impl RoundDecimal for Decimal {
190 fn round_to_ocpi_scale(self) -> Self {
191 self.round_dp_with_strategy(SCALE, rust_decimal::RoundingStrategy::MidpointNearestEven)
192 }
193}
194
195impl<T: RoundDecimal> RoundDecimal for Option<T> {
196 fn round_to_ocpi_scale(self) -> Self {
197 self.map(RoundDecimal::round_to_ocpi_scale)
198 }
199}
200
201pub(crate) trait IsZero {
206 fn is_zero(&self) -> bool;
208}
209
210pub(crate) fn approx_eq_dec(a: &Decimal, b: &Decimal, tolerance: Decimal) -> bool {
212 let Some(diff) = a.checked_sub(*b) else {
215 return false;
216 };
217 diff.abs() <= tolerance
219}
220
221#[doc(hidden)]
228#[macro_export]
229macro_rules! impl_dec_newtype {
230 ($kind:ident, $unit:literal) => {
231 impl $kind {
232 #[must_use]
234 pub fn rescale(mut self) -> Self {
235 self.0.rescale(number::SCALE);
236 Self(self.0)
237 }
238
239 #[must_use]
240 pub fn round_dp(self, digits: u32) -> Self {
242 Self(self.0.round_dp(digits))
243 }
244 }
245
246 impl $crate::number::FromDecimal for $kind {
247 fn from_decimal(d: Decimal) -> Self {
248 Self(d)
249 }
250 }
251
252 impl $crate::number::RoundDecimal for $kind {
253 fn round_to_ocpi_scale(self) -> Self {
254 Self(self.0.round_to_ocpi_scale())
255 }
256 }
257
258 impl std::fmt::Display for $kind {
259 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260 if self.0.is_zero() {
262 if f.alternate() {
263 write!(f, "0")
264 } else {
265 write!(f, "0{}", $unit)
266 }
267 } else {
268 if f.alternate() {
269 write!(f, "{:.4}", self.0)
270 } else {
271 write!(f, "{:.4}{}", self.0, $unit)
272 }
273 }
274 }
275 }
276
277 impl From<$kind> for rust_decimal::Decimal {
280 fn from(value: $kind) -> Self {
281 value.0
282 }
283 }
284
285 #[cfg(test)]
286 impl From<u64> for $kind {
287 fn from(value: u64) -> Self {
288 Self(value.into())
289 }
290 }
291
292 #[cfg(test)]
293 impl From<f64> for $kind {
294 fn from(value: f64) -> Self {
295 Self(Decimal::from_f64_retain(value).unwrap())
296 }
297 }
298
299 #[cfg(test)]
300 impl From<rust_decimal::Decimal> for $kind {
301 fn from(value: rust_decimal::Decimal) -> Self {
302 Self(value)
303 }
304 }
305
306 impl $crate::SaturatingAdd for $kind {
307 fn saturating_add(self, other: Self) -> Self {
308 Self(self.0.saturating_add(other.0))
309 }
310 }
311
312 impl $crate::SaturatingSub for $kind {
313 fn saturating_sub(self, other: Self) -> Self {
314 Self(self.0.saturating_sub(other.0))
315 }
316 }
317
318 impl<'buf> $crate::FromSchema<'buf, $crate::schema::Number<'buf>> for $kind {
319 type Warning = $crate::number::Warning;
320
321 fn from_schema(
322 source: &$crate::schema::Number<'buf>,
323 ) -> $crate::Verdict<Self, Self::Warning> {
324 let decimal: $crate::Verdict<rust_decimal::Decimal, $crate::number::Warning> =
325 $crate::FromSchema::from_schema(source);
326 decimal.map(|v| v.map(Self))
327 }
328 }
329
330 #[cfg(test)]
331 impl $crate::test::ApproxEq for $kind {
332 type Tolerance = Decimal;
333
334 fn default_tolerance() -> Self::Tolerance {
335 rust_decimal_macros::dec!(0.1)
336 }
337
338 fn approx_eq_tolerance(&self, other: &Self, tolerance: Decimal) -> bool {
339 $crate::number::approx_eq_dec(&self.0, &other.0, tolerance)
340 }
341 }
342 };
343}