hotshot_query_service/explorer/
monetary_value.rs

1// Copyright (c) 2022 Espresso Systems (espressosys.com)
2// This file is part of the HotShot Query Service library.
3//
4// This program is free software: you can redistribute it and/or modify it under the terms of the GNU
5// General Public License as published by the Free Software Foundation, either version 3 of the
6// License, or (at your option) any later version.
7// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
8// even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9// General Public License for more details.
10// You should have received a copy of the GNU General Public License along with this program. If not,
11// see <https://www.gnu.org/licenses/>.
12
13use 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)]
24/// [MonetaryValue]s is a struct that paris a [CurrencyCode] with a value.
25/// This structure is able to represent both positive and negative currencies.
26pub struct MonetaryValue {
27    pub currency: CurrencyCode,
28    pub value: i128,
29}
30
31impl MonetaryValue {
32    /// new creates a new MonetaryValue instance with the given [CurrencyCode]
33    /// and [i128] value.
34    pub fn new(currency: CurrencyCode, value: i128) -> Self {
35        Self { currency, value }
36    }
37    /// usd is a convenience function to create a [MonetaryValue] with the
38    /// USD currency code.
39    pub fn usd(value: i128) -> Self {
40        Self::new(CurrencyCode::Usd, value)
41    }
42
43    /// btc is a convenience function to create a [MonetaryValue] with the
44    /// BTC currency code.
45    pub fn btc(value: i128) -> Self {
46        Self::new(CurrencyCode::Btc, value)
47    }
48
49    /// eth is a convenience function to create a [MonetaryValue] with the
50    /// ETH currency code.
51    pub fn eth(value: i128) -> Self {
52        Self::new(CurrencyCode::Eth, value)
53    }
54
55    /// esp is a convenience function to create a [MonetaryValue] with the
56    /// ESP currency code.
57    ///
58    /// This is used to represents Espresso Tokens, and is the default that
59    /// is used for Espresso Fees and Rewards
60    pub fn esp(value: i128) -> Self {
61        Self::new(CurrencyCode::Esp, value)
62    }
63}
64
65impl Display for MonetaryValue {
66    /// fmt formats the [MonetaryValue] into a human readable string.  There's
67    /// no official standard for formatting a [MonetaryValue] into a string,
68    /// but there are highly utilized cases that we can use as a reference
69    /// to inform our implementation.
70    ///
71    /// In referencing [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) we
72    /// encounter a section titled
73    /// [Code position in amount formatting](https://en.wikipedia.org/wiki/ISO_4217#Code_position_in_amount_formatting)
74    /// which references a style guide provided by the European Union's
75    /// Publication office which indicates that the value should be prefixed
76    /// with the [CurrencyCode], followed by a "hard space" (non-breaking space)
77    /// and then the value of the currency itself.  It also lists several
78    /// countries which swaps the position of the currency code and the value.
79    ///
80    /// In this case, we opt to use the European Union's style guide, as it
81    /// is at least backed by some form of standardization.
82    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    /// add attempts to add the two [MonetaryValue]s together.  This returns
107    /// a [Result] because this addition **can** fail if the two
108    /// [MonetaryValue]s do not have the same [CurrencyCode].
109    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    /// sub attempts to subtract the two [MonetaryValue]s together.  This returns
128    /// a [Result] because this subtraction **can** fail if the two
129    /// [MonetaryValue]s do not have the same [CurrencyCode].
130    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    /// serialize converts the [MonetaryValue] into a String representation.
147    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
157/// is_ascii_digit is a convenience function for converting char::is_ascii_digit
158/// into a function that conforms to [Pattern].
159fn is_ascii_digit(c: char) -> bool {
160    char::is_ascii_digit(&c)
161}
162
163/// Reorder the currency code and the value strings into the expected order.
164/// This is done to simplify the down-stream parsing logic as we will be able
165/// to assume that the first element of the pair is the string containing the
166/// currency code, and the second part of the pair is the portion of the string
167/// that contains the monetary value.
168///
169/// This function does not attempt to do anything beyond separating these two
170/// representations into a pair of strings.
171fn reorder_currency_code_and_value<E>(first: String, last: String) -> Result<(String, String), E>
172where
173    E: serde::de::Error,
174{
175    // We need to sus out which portion of the split is the currency code
176    // versus which is likely to be the numeric value.
177    // This **should** be fairly easily, just identify which part has any
178    // numerical value within it.
179
180    if first.contains(is_ascii_digit) {
181        Ok((last, first))
182    } else {
183        Ok((first, last))
184    }
185}
186
187/// Split a string into a currency code and a value strings in order to simplify
188/// parsing the string into a [MonetaryValue].
189fn 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
212/// determine_pre_and_post_decimal_strings attempts to determine if there is
213/// a decimal point in the value string, and if so, it will returned the split
214/// string based on the location of the decimal point.
215///
216/// The only supported decimal points are '.', ',', and ' '.
217///
218/// This implementation takes advantage of the notion that the decimal point is
219/// the last symbol in a string representation of a number.  This may
220/// potentially return false positives in some cases, but it's best to assume
221/// that the value is formatted with a decimal rather than a grouping separator
222/// instead.
223///
224/// For convenience the returned strings are pruned of any non-numeric value.
225fn 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    // We need to know the sign
260    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    // We want to see if we can determine the value before the decimal
270    // separator, versus the value after the decimal separator.
271    // This **should** support the ability to omit the decimal separator
272    // and the trailing fractional portion of the value.
273    // For now the only supported decimal separators we need to consider
274    // are '.', ',', and ' '.
275    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    /// We're wanting to deserialize a [MonetaryValue] from a string that can
308    /// be in a Ticker Style format.  It **could** be beneficial to support
309    /// multiple formats for convenience due to different localizations of
310    /// the world.  This would allow for maximum flexibility in representation.
311    /// This does mean that the numerical format can be wide and varied. This
312    /// does not mean that this format does not come without restrictions.
313    ///
314    /// A list of parsing restrictions:
315    ///
316    /// - The value's numerical representation is expected to be formatting
317    ///   using arabic numerals.
318    /// - The value's numerical representation must have the expected number
319    ///   of significant digits of the currency determined by the currency
320    ///   code.
321    /// - The currency code and the numerical value **MUST** be separated
322    ///   by a non-breaking space character.
323    /// - The Currency Code cannot have numeric digits within it.
324    /// - The Currency Code must be a valid ISO 4217 Currency Code for all
325    ///   fiat currencies.
326    /// - The Currency Code may be a valid Crypto Currency Identifier
327    /// - The Currency Code may be a valid Token Identifier
328    /// - The Currency Code **MUST** be at least 3 characters long.
329    ///
330    /// This means that his function should be able to support parsing the
331    /// following representations:
332    ///
333    /// # Supported Representations
334    /// - USD 0.00
335    /// - USD 0,00
336    /// - USD 0 00
337    /// - USD 0
338    /// - 0.00 USD
339    /// - 0,00 USD
340    /// - 0 00 USD
341    /// - 0 USD
342    /// - USD 1,000.00
343    /// - USD 1.000,00
344    /// - USD 1 000,00
345    /// - 1,000.00 USD
346    /// - 1.000,00 USD
347    /// - 1 000,00 USD
348    /// - USD 1,00,000.00
349    /// - 1,00,000.00 USD
350    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(&currency_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    /// deserialize attempts to convert a string into a [MonetaryValue].
373    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    /// from converts an [i128] into a [MonetaryValue] with the USD currency
383    /// code.
384    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}