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("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    /// We're wanting to deserialize a [MonetaryValue] from a string that can
305    /// be in a Ticker Style format.  It **could** be beneficial to support
306    /// multiple formats for convenience due to different localizations of
307    /// the world.  This would allow for maximum flexibility in representation.
308    /// This does mean that the numerical format can be wide and varied. This
309    /// does not mean that this format does not come without restrictions.
310    ///
311    /// A list of parsing restrictions:
312    ///
313    /// - The value's numerical representation is expected to be formatting
314    ///   using arabic numerals.
315    /// - The value's numerical representation must have the expected number
316    ///   of significant digits of the currency determined by the currency
317    ///   code.
318    /// - The currency code and the numerical value **MUST** be separated
319    ///   by a non-breaking space character.
320    /// - The Currency Code cannot have numeric digits within it.
321    /// - The Currency Code must be a valid ISO 4217 Currency Code for all
322    ///   fiat currencies.
323    /// - The Currency Code may be a valid Crypto Currency Identifier
324    /// - The Currency Code may be a valid Token Identifier
325    /// - The Currency Code **MUST** be at least 3 characters long.
326    ///
327    /// This means that his function should be able to support parsing the
328    /// following representations:
329    ///
330    /// # Supported Representations
331    /// - USD 0.00
332    /// - USD 0,00
333    /// - USD 0 00
334    /// - USD 0
335    /// - 0.00 USD
336    /// - 0,00 USD
337    /// - 0 00 USD
338    /// - 0 USD
339    /// - USD 1,000.00
340    /// - USD 1.000,00
341    /// - USD 1 000,00
342    /// - 1,000.00 USD
343    /// - 1.000,00 USD
344    /// - 1 000,00 USD
345    /// - USD 1,00,000.00
346    /// - 1,00,000.00 USD
347    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(&currency_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    /// deserialize attempts to convert a string into a [MonetaryValue].
370    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    /// from converts an [i128] into a [MonetaryValue] with the USD currency
380    /// code.
381    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}