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(
302 "a string in a ticker format, with the required number of significant digits. For \
303 example: `USD 100.00` or `ETH 0.000000000000000001`",
304 )
305 }
306
307 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
351 where
352 E: serde::de::Error,
353 {
354 let (currency_code_str, value_raw_str) =
355 split_str_into_currency_code_and_value_string::<E>(value)?;
356
357 let currency = match CurrencyCode::try_from(¤cy_code_str[..]) {
358 Ok(currency) => Ok(currency),
359 Err(err) => Err(E::custom(err)),
360 }?;
361
362 let value = parse_pre_and_post_decimal_digits::<E>(
363 currency.significant_digits() as u32,
364 value_raw_str,
365 )?;
366
367 Ok(MonetaryValue { currency, value })
368 }
369}
370
371impl<'de> Deserialize<'de> for MonetaryValue {
372 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
374 where
375 D: serde::Deserializer<'de>,
376 {
377 deserializer.deserialize_str(MonetaryValueVisitor)
378 }
379}
380
381impl From<i128> for MonetaryValue {
382 fn from(value: i128) -> Self {
385 Self::esp(value)
386 }
387}
388
389#[cfg(test)]
390mod test {
391 use crate::explorer::{currency::CurrencyCode, monetary_value::MonetaryValue};
392
393 #[test]
394 fn test_monetary_value_json_deserialization() {
395 {
396 let values = vec![
397 "\"USD 100.00\"",
398 "\"USD 100,00\"",
399 "\"USD 100 00\"",
400 "\"USD 100\"",
401 "\"100.00 USD\"",
402 "\"100,00 USD\"",
403 "\"100 00 USD\"",
404 "\"100 USD\"",
405 ];
406
407 for value in values {
408 let result: serde_json::Result<MonetaryValue> = serde_json::from_str(value);
409
410 let result = match result {
411 Err(err) => {
412 panic!("{value} failed to parse: {err}");
413 },
414 Ok(result) => result,
415 };
416
417 let have = result;
418 let want = MonetaryValue::usd(10000);
419
420 assert_eq!(have, want, "{value} parse result: have {have}, want {want}",);
421 }
422 }
423
424 {
425 let values = vec![
426 "\"USD 100000\"",
427 "\"USD 100,000.00\"",
428 "\"USD 100.000,00\"",
429 "\"USD 100 000,00\"",
430 "\"USD 1,00,000.00\"",
431 ];
432
433 for value in values {
434 let result: serde_json::Result<MonetaryValue> = serde_json::from_str(value);
435
436 let result = match result {
437 Err(err) => {
438 panic!("{value} failed to parse: {err}");
439 },
440 Ok(result) => result,
441 };
442
443 let have = result;
444 let want = MonetaryValue::usd(10000000);
445
446 assert_eq!(have, want, "{value} parse result: have {have}, want {want}",);
447 }
448 }
449
450 assert!(serde_json::from_str::<MonetaryValue>("\"USD 0\"").is_ok());
451 assert!(serde_json::from_str::<MonetaryValue>("\"USD 00\"").is_ok());
452 assert!(serde_json::from_str::<MonetaryValue>("\"USD 100\"").is_err());
453 assert!(serde_json::from_str::<MonetaryValue>("\"BTC 100\"").is_ok());
454 assert!(serde_json::from_str::<MonetaryValue>("\"XBT 100\"").is_ok());
455 assert!(serde_json::from_str::<MonetaryValue>("\"ETH 100\"").is_ok());
456
457 {
458 let cases = [
459 ("\"USD 0.00\"", MonetaryValue::usd(0)),
460 ("\"USD -1.00\"", MonetaryValue::usd(-100)),
461 ("\"USD -1\"", MonetaryValue::usd(-100)),
462 ("\"USD 1.23\"", MonetaryValue::usd(123)),
463 ("\"USD 0.50\"", MonetaryValue::usd(50)),
464 (
465 "\"ETH 0.000000001000000000\"",
466 MonetaryValue::eth(1000000000),
467 ),
468 ("\"ETH 0.000000000000000001\"", MonetaryValue::eth(1)),
469 (
470 "\"ETH 1.000000000000000000\"",
471 MonetaryValue::eth(1000000000000000000),
472 ),
473 ("\"XBT 0.00000001\"", MonetaryValue::btc(1)),
474 ];
475
476 for case in cases {
477 let value = case.0;
478 let have = serde_json::from_str::<MonetaryValue>(value).unwrap();
479 let want = case.1;
480 assert_eq!(have, want, "{value} parse result: have {have}, want {want}");
481 }
482 }
483 }
484
485 #[test]
486 fn test_monetary_value_json_serialization() {
487 let cases = [
488 (MonetaryValue::usd(0), "\"USD 0\""),
489 (MonetaryValue::usd(-100), "\"USD -1\""),
490 (MonetaryValue::usd(123), "\"USD 1.23\""),
491 (MonetaryValue::usd(50), "\"USD 0.50\""),
492 (
493 MonetaryValue::eth(1000000000),
494 "\"ETH 0.000000001000000000\"",
495 ),
496 (MonetaryValue::eth(1), "\"ETH 0.000000000000000001\""),
497 (MonetaryValue::eth(1000000000000000000), "\"ETH 1\""),
498 (MonetaryValue::btc(1), "\"XBT 0.00000001\""),
499 ];
500
501 for case in cases {
502 let value = case.0;
503 let have = serde_json::to_string(&value).unwrap();
504 let want = case.1;
505 assert_eq!(
506 have, want,
507 "{value} encode result: have {have}, want {want}"
508 );
509 }
510 }
511
512 #[test]
513 fn test_serialize_deserialize() {
514 for currency in [
515 CurrencyCode::Usd,
516 CurrencyCode::Eth,
517 CurrencyCode::Btc,
518 CurrencyCode::Jpy,
519 ]
520 .iter()
521 {
522 for i in -100..=1000 {
523 let value = MonetaryValue::new(*currency, i);
524 let serialized = serde_json::to_string(&value).unwrap();
525 let deserialized = serde_json::from_str::<MonetaryValue>(&serialized).unwrap();
526 assert_eq!(
527 value, deserialized,
528 "{currency} {i} encoded result: {serialized}: have {deserialized}, want \
529 {value}"
530 );
531 }
532 }
533 }
534
535 #[test]
536 fn test_arithmetic() {
537 {
538 let a = MonetaryValue::usd(100);
539 let b = MonetaryValue::usd(100);
540 let c = a + b;
541 assert!(c.is_ok());
542 assert_eq!(c.unwrap(), MonetaryValue::usd(200));
543 }
544
545 {
546 let a = MonetaryValue::usd(100);
547 let b = MonetaryValue::usd(100);
548 let c = a - b;
549 assert!(c.is_ok());
550 assert_eq!(c.unwrap(), MonetaryValue::usd(0));
551 }
552
553 {
554 let a = MonetaryValue::usd(100);
555 let b = MonetaryValue::eth(100);
556 let c = a + b;
557 assert!(c.is_err());
558 }
559
560 {
561 let a = MonetaryValue::usd(100);
562 let b = MonetaryValue::eth(100);
563 let c = a - b;
564 assert!(c.is_err());
565 }
566 }
567}