1use core::fmt;
4use core::str::FromStr;
5
6use rust_decimal::Decimal;
7use rust_decimal::prelude::ToPrimitive;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10use super::validate::{Validate, Validator, ViolationCode};
11
12#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
54pub struct Number(Decimal);
55
56impl Number {
57 pub const ZERO: Self = Self(Decimal::ZERO);
59 pub const ONE: Self = Self(Decimal::ONE);
61
62 #[must_use]
64 pub const fn new(value: Decimal) -> Self {
65 Self(value)
66 }
67
68 #[must_use]
70 pub const fn get(self) -> Decimal {
71 self.0
72 }
73
74 #[must_use]
76 pub fn scale(self) -> u32 {
77 self.0.scale()
78 }
79
80 #[must_use]
82 pub fn round_dp(self, dp: u32) -> Self {
83 Self(self.0.round_dp_with_strategy(dp, rust_decimal::RoundingStrategy::MidpointAwayFromZero))
84 }
85
86 #[must_use]
91 pub fn json_round_trips(self) -> bool {
92 if self.0.is_integer() && self.0.to_i64().is_some() {
93 return true;
94 }
95 self.0.to_f64().and_then(decimal_from_f64).is_some_and(|d| d == self.0.normalize())
96 }
97
98 #[must_use]
100 pub fn is_zero(self) -> bool {
101 self.0.is_zero()
102 }
103
104 #[must_use]
106 pub fn is_negative(self) -> bool {
107 self.0.is_sign_negative() && !self.0.is_zero()
108 }
109}
110
111impl Validate for Number {
112 fn validate_in(&self, v: &mut Validator) {
113 if !self.json_round_trips() {
114 v.report(
115 ViolationCode::Imprecise,
116 format!("{} carries more significant digits than a JSON number round-trip preserves", self.0),
117 );
118 }
119 }
120}
121
122impl fmt::Display for Number {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 fmt::Display::fmt(&self.0, f)
125 }
126}
127
128impl fmt::Debug for Number {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 write!(f, "Number({})", self.0)
131 }
132}
133
134impl From<Decimal> for Number {
135 fn from(value: Decimal) -> Self {
136 Self(value)
137 }
138}
139impl From<Number> for Decimal {
140 fn from(value: Number) -> Self {
141 value.0
142 }
143}
144
145macro_rules! from_int {
146 ($($t:ty),*) => {$(
147 impl From<$t> for Number {
148 fn from(value: $t) -> Self { Self(Decimal::from(value)) }
149 }
150 )*};
151}
152from_int!(i8, i16, i32, i64, u8, u16, u32, u64);
153
154impl core::ops::Add for Number {
155 type Output = Self;
156 fn add(self, rhs: Self) -> Self {
157 Self(self.0 + rhs.0)
158 }
159}
160impl core::ops::Sub for Number {
161 type Output = Self;
162 fn sub(self, rhs: Self) -> Self {
163 Self(self.0 - rhs.0)
164 }
165}
166impl core::ops::Mul for Number {
167 type Output = Self;
168 fn mul(self, rhs: Self) -> Self {
169 Self(self.0 * rhs.0)
170 }
171}
172impl core::ops::Div for Number {
173 type Output = Self;
174 fn div(self, rhs: Self) -> Self {
175 Self(self.0 / rhs.0)
176 }
177}
178impl core::ops::Neg for Number {
179 type Output = Self;
180 fn neg(self) -> Self {
181 Self(-self.0)
182 }
183}
184impl core::iter::Sum for Number {
185 fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
186 iter.fold(Self::ZERO, core::ops::Add::add)
187 }
188}
189
190fn decimal_from_f64(value: f64) -> Option<Decimal> {
206 if !value.is_finite() {
207 return None;
208 }
209 Decimal::from_str_exact(&value.to_string()).ok()
211}
212
213#[derive(Clone, Debug, PartialEq, Eq)]
215pub struct InvalidNumber(String);
216
217impl fmt::Display for InvalidNumber {
218 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219 write!(f, "invalid OCPI number: {}", self.0)
220 }
221}
222impl std::error::Error for InvalidNumber {}
223
224impl FromStr for Number {
225 type Err = InvalidNumber;
226 fn from_str(s: &str) -> Result<Self, Self::Err> {
227 Decimal::from_str_exact(s).map(Self).map_err(|e| InvalidNumber(format!("{s:?}: {e}")))
228 }
229}
230
231impl Serialize for Number {
232 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
233 use serde::ser::Error as _;
234 if self.0.is_integer() {
235 if let Some(i) = self.0.to_i64() {
236 return serializer.serialize_i64(i);
237 }
238 if let Some(u) = self.0.to_u64() {
239 return serializer.serialize_u64(u);
240 }
241 }
242 let f = self
243 .0
244 .to_f64()
245 .ok_or_else(|| S::Error::custom(format!("{} is not representable as a JSON number", self.0)))?;
246 serializer.serialize_f64(f)
247 }
248}
249
250impl<'de> Deserialize<'de> for Number {
251 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
252 struct V;
253 impl serde::de::Visitor<'_> for V {
254 type Value = Number;
255 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256 f.write_str("a JSON number")
257 }
258 fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<Number, E> {
259 Ok(Number(Decimal::from(v)))
260 }
261 fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Number, E> {
262 Ok(Number(Decimal::from(v)))
263 }
264 fn visit_i128<E: serde::de::Error>(self, v: i128) -> Result<Number, E> {
265 Ok(Number(Decimal::from(v)))
266 }
267 fn visit_u128<E: serde::de::Error>(self, v: u128) -> Result<Number, E> {
268 Ok(Number(Decimal::from(v)))
269 }
270 fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<Number, E> {
271 decimal_from_f64(v)
272 .map(Number)
273 .ok_or_else(|| E::custom(format!("{v} is not representable as an OCPI number")))
274 }
275 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Number, E> {
276 Number::from_str(v).map_err(E::custom)
278 }
279 }
280 deserializer.deserialize_any(V)
281 }
282}
283
284#[cfg(feature = "schema")]
285impl schemars::JsonSchema for Number {
286 fn schema_name() -> std::borrow::Cow<'static, str> {
287 "Number".into()
288 }
289 fn json_schema(_g: &mut schemars::SchemaGenerator) -> schemars::Schema {
290 schemars::json_schema!({
291 "type": "number",
292 "description": "OCPI number: an exact decimal, serialised as a JSON number",
293 })
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 fn n(s: &str) -> Number {
302 s.parse().unwrap()
303 }
304
305 #[test]
306 fn integers_stay_integers_on_the_wire() {
307 assert_eq!(serde_json::to_string(&n("20")).unwrap(), "20");
308 assert_eq!(serde_json::to_string(&n("0")).unwrap(), "0");
309 assert_eq!(serde_json::to_string(&n("-7")).unwrap(), "-7");
310 }
311
312 #[test]
313 fn realistic_ocpi_values_round_trip_exactly() {
314 for text in ["0.25", "0.0295", "2.5", "20.45", "0.0002", "123456789.1234", "-1.05"] {
315 let parsed: Number = serde_json::from_str(text).unwrap();
316 assert_eq!(serde_json::to_string(&parsed).unwrap(), text, "round-trip of {text}");
317 assert!(parsed.json_round_trips());
318 assert!(parsed.validate().is_ok());
319 }
320 }
321
322 #[test]
323 fn trailing_zeros_are_normalised_away() {
324 let parsed: Number = "0.2500".parse().unwrap();
326 assert_eq!(serde_json::to_string(&parsed).unwrap(), "0.25");
327 assert_eq!(parsed, n("0.25"));
328 }
329
330 #[test]
331 fn a_fractional_number_decodes_to_exactly_what_the_peer_wrote() {
332 for text in ["4106.9638", "4112.654", "4130.8379", "4136.529", "4163.9629", "4291.154"] {
336 let parsed: Number = serde_json::from_str(text).unwrap();
337 assert_eq!(parsed, n(text), "decoding {text}");
338 assert_eq!(serde_json::to_string(&parsed).unwrap(), text, "re-encoding {text}");
339 assert!(parsed.json_round_trips(), "{text} does survive a round-trip");
340 assert!(parsed.validate().is_ok(), "{text} is a perfectly ordinary number");
341 }
342 }
343
344 #[test]
345 fn every_four_decimal_value_in_ocpi_range_survives_the_boundary() {
346 let mut mantissa = 1i64;
349 while mantissa < 100_000_000 {
350 let value = Number::new(Decimal::new(mantissa, 4));
351 let json = serde_json::to_string(&value).unwrap();
352 let back: Number = serde_json::from_str(&json).unwrap();
353 assert_eq!(back, value, "{value} round-tripped through {json} as {back}");
354 assert!(value.json_round_trips(), "{value}");
355 mantissa += 1237;
356 }
357 }
358
359 #[test]
360 fn a_value_that_is_not_a_number_is_refused() {
361 assert!(decimal_from_f64(f64::NAN).is_none());
362 assert!(decimal_from_f64(f64::INFINITY).is_none());
363 assert!(decimal_from_f64(f64::MAX).is_none(), "beyond what a Decimal can hold");
364 assert_eq!(decimal_from_f64(0.0), Some(Decimal::ZERO));
365 }
366
367 #[test]
368 fn excess_precision_is_flagged_rather_than_hidden() {
369 let precise = n("0.123456789012345678901234");
370 assert!(!precise.json_round_trips());
371 assert_eq!(precise.validate().unwrap_err().as_slice()[0].code, ViolationCode::Imprecise);
372 }
373
374 #[test]
375 fn quoted_numbers_are_tolerated_on_input() {
376 let parsed: Number = serde_json::from_str("\"0.25\"").unwrap();
377 assert_eq!(parsed, n("0.25"));
378 assert_eq!(serde_json::to_string(&parsed).unwrap(), "0.25");
379 }
380
381 #[test]
382 fn arithmetic_is_exact() {
383 let sum: Number = ["0.1", "0.2"].into_iter().map(n).sum();
384 assert_eq!(sum, n("0.3"), "0.1 + 0.2 is exactly 0.3 in decimal");
385 }
386}