1use std::{
14 fmt::{Debug, Display},
15 ops::{Add, Sub},
16};
17
18use itertools::Itertools;
19use serde::{Deserialize, Serialize, Serializer};
20
21use super::currency::{CurrencyCode, CurrencyMismatchError};
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct MonetaryValue {
27 pub currency: CurrencyCode,
28 pub value: i128,
29}
30
31impl MonetaryValue {
32 pub fn new(currency: CurrencyCode, value: i128) -> Self {
35 Self { currency, value }
36 }
37 pub fn usd(value: i128) -> Self {
40 Self::new(CurrencyCode::Usd, value)
41 }
42
43 pub fn btc(value: i128) -> Self {
46 Self::new(CurrencyCode::Btc, value)
47 }
48
49 pub fn eth(value: i128) -> Self {
52 Self::new(CurrencyCode::Eth, value)
53 }
54
55 pub fn esp(value: i128) -> Self {
61 Self::new(CurrencyCode::Esp, value)
62 }
63}
64
65impl Display for MonetaryValue {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 let currency = self.currency;
84 let value = self.value;
85 let significant_figures = currency.significant_digits();
86 let abs_value = value.abs();
87 let sign = if value < 0 { "-" } else { "" };
88
89 let max_post_decimal_digits = 10i128.pow(significant_figures as u32);
90
91 let whole = abs_value / max_post_decimal_digits;
92 let fraction = abs_value % max_post_decimal_digits;
93
94 let fraction_str = format!("{fraction:0significant_figures$}");
95 if fraction == 0 {
96 write!(f, "{currency}\u{00a0}{sign}{whole}")
97 } else {
98 write!(f, "{currency}\u{00a0}{sign}{whole}.{fraction_str}")
99 }
100 }
101}
102
103impl Add for MonetaryValue {
104 type Output = Result<MonetaryValue, CurrencyMismatchError>;
105
106 fn add(self, rhs: Self) -> Self::Output {
110 if self.currency != rhs.currency {
111 return Err(CurrencyMismatchError {
112 currency1: self.currency,
113 currency2: rhs.currency,
114 });
115 }
116
117 Ok(MonetaryValue {
118 currency: self.currency,
119 value: self.value + rhs.value,
120 })
121 }
122}
123
124impl Sub for MonetaryValue {
125 type Output = Result<MonetaryValue, CurrencyMismatchError>;
126
127 fn sub(self, rhs: Self) -> Self::Output {
131 if self.currency != rhs.currency {
132 return Err(CurrencyMismatchError {
133 currency1: self.currency,
134 currency2: rhs.currency,
135 });
136 }
137
138 Ok(MonetaryValue {
139 currency: self.currency,
140 value: self.value - rhs.value,
141 })
142 }
143}
144
145impl Serialize for MonetaryValue {
146 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
148 where
149 S: Serializer,
150 {
151 serializer.serialize_str(&self.to_string())
152 }
153}
154
155struct MonetaryValueVisitor;
156
157fn is_ascii_digit(c: char) -> bool {
160 char::is_ascii_digit(&c)
161}
162
163fn reorder_currency_code_and_value<E>(first: String, last: String) -> Result<(String, String), E>
172where
173 E: serde::de::Error,
174{
175 if first.contains(is_ascii_digit) {
181 Ok((last, first))
182 } else {
183 Ok((first, last))
184 }
185}
186
187fn split_str_into_currency_code_and_value_string<E>(value: &str) -> Result<(String, String), E>
190where
191 E: serde::de::Error,
192{
193 let (index, _) = match value.chars().enumerate().find(|(_, c)| *c == '\u{00a0}') {
194 Some((i, c)) => (i, c),
195 None => {
196 return Err(E::custom(
197 "no non-breaking space found in expected MonetaryValue",
198 ))
199 },
200 };
201
202 let first: String = value.chars().take(index).collect();
203 let last: String = value.chars().dropping(index + 1).collect();
204
205 reorder_currency_code_and_value(first, last)
206}
207
208fn is_possibly_a_decimal_point(c: char) -> bool {
209 c == '.' || c == ',' || char::is_whitespace(c)
210}
211
212fn determine_pre_and_post_decimal_strings(value: &str) -> (String, Option<String>) {
226 let decimal_point = value
227 .chars()
228 .enumerate()
229 .filter(|(_, c)| is_possibly_a_decimal_point(*c))
230 .last();
231
232 match decimal_point {
233 None => (value.chars().filter(char::is_ascii_digit).collect(), None),
234 Some((index, _)) => {
235 let pre_decimal_string: String = value
236 .chars()
237 .take(index)
238 .filter(char::is_ascii_digit)
239 .collect();
240
241 let post_decimal_string: String = value
242 .chars()
243 .dropping(index + 1)
244 .filter(char::is_ascii_digit)
245 .collect();
246
247 (pre_decimal_string, Some(post_decimal_string))
248 },
249 }
250}
251
252fn parse_pre_and_post_decimal_digits<E>(
253 significant_digits: u32,
254 value_raw_str: String,
255) -> Result<i128, E>
256where
257 E: serde::de::Error,
258{
259 let sign = match value_raw_str
261 .trim_start_matches(char::is_whitespace)
262 .chars()
263 .next()
264 {
265 Some('-') => -1,
266 _ => 1,
267 };
268
269 let (pre_decimal_string, post_decimal_string_option) =
276 determine_pre_and_post_decimal_strings(&value_raw_str);
277
278 match post_decimal_string_option {
279 None => match pre_decimal_string.parse::<i128>() {
280 Ok(value) => Ok(sign * value * 10i128.pow(significant_digits)),
281 Err(err) => Err(E::custom(err)),
282 },
283 Some(post_decimal_string) => {
284 let pre_decimal_value = pre_decimal_string.parse::<i128>().map_err(E::custom)?;
285 let post_decimal_value = post_decimal_string.parse::<i128>().map_err(E::custom)?;
286 let num_digits = post_decimal_string.len() as u32;
287
288 let value = sign
289 * (pre_decimal_value * 10i128.pow(significant_digits)
290 + 10i128.pow(significant_digits - num_digits) * post_decimal_value);
291
292 Ok(value)
293 },
294 }
295}
296
297impl serde::de::Visitor<'_> for MonetaryValueVisitor {
298 type Value = MonetaryValue;
299
300 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
301 formatter.write_str("a string in a ticker format, with the required number of significant digits. For example: `USD 100.00` or `ETH 0.000000000000000001`")
302 }
303
304 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
348 where
349 E: serde::de::Error,
350 {
351 let (currency_code_str, value_raw_str) =
352 split_str_into_currency_code_and_value_string::<E>(value)?;
353
354 let currency = match CurrencyCode::try_from(¤cy_code_str[..]) {
355 Ok(currency) => Ok(currency),
356 Err(err) => Err(E::custom(err)),
357 }?;
358
359 let value = parse_pre_and_post_decimal_digits::<E>(
360 currency.significant_digits() as u32,
361 value_raw_str,
362 )?;
363
364 Ok(MonetaryValue { currency, value })
365 }
366}
367
368impl<'de> Deserialize<'de> for MonetaryValue {
369 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
371 where
372 D: serde::Deserializer<'de>,
373 {
374 deserializer.deserialize_str(MonetaryValueVisitor)
375 }
376}
377
378impl From<i128> for MonetaryValue {
379 fn from(value: i128) -> Self {
382 Self::esp(value)
383 }
384}
385
386#[cfg(test)]
387mod test {
388 use crate::explorer::{currency::CurrencyCode, monetary_value::MonetaryValue};
389
390 #[test]
391 fn test_monetary_value_json_deserialization() {
392 {
393 let values = vec![
394 "\"USD 100.00\"",
395 "\"USD 100,00\"",
396 "\"USD 100 00\"",
397 "\"USD 100\"",
398 "\"100.00 USD\"",
399 "\"100,00 USD\"",
400 "\"100 00 USD\"",
401 "\"100 USD\"",
402 ];
403
404 for value in values {
405 let result: serde_json::Result<MonetaryValue> = serde_json::from_str(value);
406
407 let result = match result {
408 Err(err) => {
409 panic!("{value} failed to parse: {err}");
410 },
411 Ok(result) => result,
412 };
413
414 let have = result;
415 let want = MonetaryValue::usd(10000);
416
417 assert_eq!(have, want, "{value} parse result: have {have}, want {want}",);
418 }
419 }
420
421 {
422 let values = vec![
423 "\"USD 100000\"",
424 "\"USD 100,000.00\"",
425 "\"USD 100.000,00\"",
426 "\"USD 100 000,00\"",
427 "\"USD 1,00,000.00\"",
428 ];
429
430 for value in values {
431 let result: serde_json::Result<MonetaryValue> = serde_json::from_str(value);
432
433 let result = match result {
434 Err(err) => {
435 panic!("{value} failed to parse: {err}");
436 },
437 Ok(result) => result,
438 };
439
440 let have = result;
441 let want = MonetaryValue::usd(10000000);
442
443 assert_eq!(have, want, "{value} parse result: have {have}, want {want}",);
444 }
445 }
446
447 assert!(serde_json::from_str::<MonetaryValue>("\"USD 0\"").is_ok());
448 assert!(serde_json::from_str::<MonetaryValue>("\"USD 00\"").is_ok());
449 assert!(serde_json::from_str::<MonetaryValue>("\"USD 100\"").is_err());
450 assert!(serde_json::from_str::<MonetaryValue>("\"BTC 100\"").is_ok());
451 assert!(serde_json::from_str::<MonetaryValue>("\"XBT 100\"").is_ok());
452 assert!(serde_json::from_str::<MonetaryValue>("\"ETH 100\"").is_ok());
453
454 {
455 let cases = [
456 ("\"USD 0.00\"", MonetaryValue::usd(0)),
457 ("\"USD -1.00\"", MonetaryValue::usd(-100)),
458 ("\"USD -1\"", MonetaryValue::usd(-100)),
459 ("\"USD 1.23\"", MonetaryValue::usd(123)),
460 ("\"USD 0.50\"", MonetaryValue::usd(50)),
461 (
462 "\"ETH 0.000000001000000000\"",
463 MonetaryValue::eth(1000000000),
464 ),
465 ("\"ETH 0.000000000000000001\"", MonetaryValue::eth(1)),
466 (
467 "\"ETH 1.000000000000000000\"",
468 MonetaryValue::eth(1000000000000000000),
469 ),
470 ("\"XBT 0.00000001\"", MonetaryValue::btc(1)),
471 ];
472
473 for case in cases {
474 let value = case.0;
475 let have = serde_json::from_str::<MonetaryValue>(value).unwrap();
476 let want = case.1;
477 assert_eq!(have, want, "{value} parse result: have {have}, want {want}");
478 }
479 }
480 }
481
482 #[test]
483 fn test_monetary_value_json_serialization() {
484 let cases = [
485 (MonetaryValue::usd(0), "\"USD 0\""),
486 (MonetaryValue::usd(-100), "\"USD -1\""),
487 (MonetaryValue::usd(123), "\"USD 1.23\""),
488 (MonetaryValue::usd(50), "\"USD 0.50\""),
489 (
490 MonetaryValue::eth(1000000000),
491 "\"ETH 0.000000001000000000\"",
492 ),
493 (MonetaryValue::eth(1), "\"ETH 0.000000000000000001\""),
494 (MonetaryValue::eth(1000000000000000000), "\"ETH 1\""),
495 (MonetaryValue::btc(1), "\"XBT 0.00000001\""),
496 ];
497
498 for case in cases {
499 let value = case.0;
500 let have = serde_json::to_string(&value).unwrap();
501 let want = case.1;
502 assert_eq!(
503 have, want,
504 "{value} encode result: have {have}, want {want}"
505 );
506 }
507 }
508
509 #[test]
510 fn test_serialize_deserialize() {
511 for currency in [
512 CurrencyCode::Usd,
513 CurrencyCode::Eth,
514 CurrencyCode::Btc,
515 CurrencyCode::Jpy,
516 ]
517 .iter()
518 {
519 for i in -100..=1000 {
520 let value = MonetaryValue::new(*currency, i);
521 let serialized = serde_json::to_string(&value).unwrap();
522 let deserialized = serde_json::from_str::<MonetaryValue>(&serialized).unwrap();
523 assert_eq!(
524 value, deserialized,
525 "{currency} {i} encoded result: {serialized}: have {deserialized}, want {value}"
526 );
527 }
528 }
529 }
530
531 #[test]
532 fn test_arithmetic() {
533 {
534 let a = MonetaryValue::usd(100);
535 let b = MonetaryValue::usd(100);
536 let c = a + b;
537 assert!(c.is_ok());
538 assert_eq!(c.unwrap(), MonetaryValue::usd(200));
539 }
540
541 {
542 let a = MonetaryValue::usd(100);
543 let b = MonetaryValue::usd(100);
544 let c = a - b;
545 assert!(c.is_ok());
546 assert_eq!(c.unwrap(), MonetaryValue::usd(0));
547 }
548
549 {
550 let a = MonetaryValue::usd(100);
551 let b = MonetaryValue::eth(100);
552 let c = a + b;
553 assert!(c.is_err());
554 }
555
556 {
557 let a = MonetaryValue::usd(100);
558 let b = MonetaryValue::eth(100);
559 let c = a - b;
560 assert!(c.is_err());
561 }
562 }
563}