Skip to main content

openfare_lib/price/
mod.rs

1use anyhow::{format_err, Result};
2use std::str::FromStr;
3
4mod conversions;
5
6#[derive(
7    Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, serde::Serialize, serde::Deserialize,
8)]
9pub enum Currency {
10    USD,
11
12    BTC,
13    SATS,
14}
15
16impl Currency {
17    pub fn decimal_points(&self) -> u32 {
18        match self {
19            Self::USD => 2,
20            Self::BTC => 8,
21            Self::SATS => 0,
22        }
23    }
24
25    pub fn to_symbol(&self) -> String {
26        match self {
27            Self::USD => "$",
28            Self::BTC => "₿",
29            Self::SATS => "sats",
30        }
31        .to_string()
32    }
33}
34
35impl std::default::Default for Currency {
36    fn default() -> Self {
37        Self::USD
38    }
39}
40
41impl std::convert::TryFrom<&str> for Currency {
42    type Error = anyhow::Error;
43    fn try_from(value: &str) -> Result<Self, Self::Error> {
44        let value = value.to_string().to_uppercase();
45        Ok(match value.to_lowercase().as_str() {
46            "usd" => Self::USD,
47            "btc" => Self::BTC,
48            "sats" => Self::SATS,
49            _ => {
50                return Err(format_err!("Unknown currency: {}", value));
51            }
52        })
53    }
54}
55
56impl std::fmt::Display for Currency {
57    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        let currency = match self {
59            Self::USD => "USD",
60            Self::BTC => "BTC",
61            Self::SATS => "SATS",
62        };
63        write!(formatter, "{}", currency)
64    }
65}
66
67pub type Quantity = rust_decimal::Decimal;
68
69#[derive(Debug, Default, Clone, Hash, Ord, PartialOrd, Eq, PartialEq)]
70pub struct Price {
71    pub quantity: Quantity,
72    pub currency: Currency,
73}
74
75impl Price {
76    pub fn to_symbolic(&self) -> String {
77        match self.currency {
78            Currency::USD | Currency::BTC => {
79                format!(
80                    "{currency}{:.1$}",
81                    self.quantity,
82                    self.currency.decimal_points() as usize,
83                    currency = self.currency.to_symbol(),
84                )
85            }
86            Currency::SATS => {
87                format!(
88                    "{:.1$}{currency}",
89                    self.quantity,
90                    self.currency.decimal_points() as usize,
91                    currency = self.currency.to_symbol(),
92                )
93            }
94        }
95    }
96
97    pub fn to_btc(&self) -> Result<Price> {
98        match &self.currency {
99            Currency::USD => conversions::usd_to_btc(&self),
100            Currency::BTC => Ok(self.clone()),
101            Currency::SATS => conversions::sats_to_btc(&self),
102        }
103    }
104
105    pub fn to_sats(&self) -> Result<Price> {
106        match &self.currency {
107            Currency::USD => conversions::usd_to_sats(&self),
108            Currency::BTC => conversions::btc_to_sats(&self),
109            Currency::SATS => Ok(self.clone()),
110        }
111    }
112
113    pub fn to_usd(&self) -> Result<Price> {
114        match &self.currency {
115            Currency::USD => Ok(self.clone()),
116            Currency::BTC => conversions::btc_to_usd(&self),
117            Currency::SATS => conversions::sats_to_usd(&self),
118        }
119    }
120}
121
122impl std::iter::Sum for Price {
123    fn sum<I>(iter: I) -> Self
124    where
125        I: Iterator<Item = Self>,
126    {
127        let mut currency = None;
128        let mut quantity = Quantity::from(0 as i64);
129        for price in iter {
130            quantity += price.quantity;
131            currency = Some(price.currency);
132        }
133        Self {
134            quantity,
135            currency: currency.unwrap_or_default(),
136        }
137    }
138}
139
140impl std::convert::TryFrom<&str> for Price {
141    type Error = anyhow::Error;
142    fn try_from(value: &str) -> Result<Self, Self::Error> {
143        #[derive(Eq, PartialEq, serde::Deserialize)]
144        struct Result {
145            price: Price,
146        }
147        let result: Result =
148            serde_json::from_str(format!("{{\"price\": \"{}\"}}", value).as_str())?;
149        let mut price = result.price;
150        price.quantity = price.quantity.round_dp_with_strategy(
151            price.currency.decimal_points(),
152            rust_decimal::prelude::RoundingStrategy::AwayFromZero,
153        );
154        Ok(price)
155    }
156}
157
158impl std::str::FromStr for Price {
159    type Err = anyhow::Error;
160    fn from_str(s: &str) -> Result<Self, Self::Err> {
161        Price::try_from(s)
162    }
163}
164
165impl std::fmt::Display for Price {
166    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167        write!(
168            formatter,
169            "{:.1$} {currency}",
170            self.quantity,
171            self.currency.decimal_points() as usize,
172            currency = self.currency.to_string()
173        )
174    }
175}
176
177impl serde::Serialize for Price {
178    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
179    where
180        S: serde::Serializer,
181    {
182        serializer.serialize_str(
183            format!(
184                "{:.1$} {currency}",
185                self.quantity,
186                self.currency.decimal_points() as usize,
187                currency = self.currency.to_string(),
188            )
189            .as_str(),
190        )
191    }
192}
193
194struct Visitor {
195    marker: std::marker::PhantomData<fn() -> Price>,
196}
197
198impl Visitor {
199    fn new() -> Self {
200        Visitor {
201            marker: std::marker::PhantomData,
202        }
203    }
204}
205
206impl<'de> serde::de::Visitor<'de> for Visitor {
207    type Value = Price;
208
209    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
210        formatter.write_str("a string such as '50 USD'")
211    }
212
213    fn visit_str<E>(self, v: &str) -> core::result::Result<Self::Value, E>
214    where
215        E: serde::de::Error,
216    {
217        let re = regex::Regex::new(r"([0-9]+[\.]?[0-9]*)\s*([a-zA-Z]+)").map_err(|_| {
218            serde::de::Error::custom(serde::de::Unexpected::Other("Code error: invalid regex."))
219        })?;
220        let captures =
221            re.captures(v)
222                .ok_or(serde::de::Error::custom(serde::de::Unexpected::Other(
223                    format!("No regex captures found: {}", v).as_str(),
224                )))?;
225
226        let quantity = parse_quantity(&captures.get(1)).map_err(|_| {
227            serde::de::Error::custom(serde::de::Unexpected::Other(
228                format!("Failed to parse quantity: {}", v).as_str(),
229            ))
230        })?;
231        let currency = parse_currency(&captures.get(2)).map_err(|_| {
232            serde::de::Error::custom(serde::de::Unexpected::Other(
233                format!("Failed to parse currency: {}", v).as_str(),
234            ))
235        })?;
236
237        Ok(Self::Value { quantity, currency })
238    }
239}
240
241impl<'de> serde::Deserialize<'de> for Price {
242    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
243    where
244        D: serde::Deserializer<'de>,
245    {
246        deserializer.deserialize_str(Visitor::new())
247    }
248}
249
250fn parse_quantity(regex_capture: &Option<regex::Match>) -> Result<rust_decimal::Decimal> {
251    let quantity = regex_capture
252        .ok_or(format_err!("Failed to parse quantity"))?
253        .as_str();
254    let quantity = rust_decimal::Decimal::from_str(quantity)?;
255    Ok(quantity)
256}
257
258fn parse_currency(regex_capture: &Option<regex::Match>) -> Result<Currency> {
259    let error_message = "Failed to parse currency";
260    let currency = regex_capture.ok_or(format_err!(error_message))?.as_str();
261
262    let currency = match currency.to_lowercase().as_str() {
263        "usd" => Currency::USD,
264        "btc" => Currency::BTC,
265        "sats" => Currency::SATS,
266        _ => {
267            return Err(format_err!(error_message));
268        }
269    };
270    Ok(currency)
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    #[test]
278    fn test_to_symbolic_usd() -> anyhow::Result<()> {
279        let price = Price::try_from("50   usd")?;
280        let result = price.to_symbolic();
281        let expected = "$50.00".to_string();
282        assert!(result == expected);
283        Ok(())
284    }
285
286    #[test]
287    fn test_to_symbolic_sat() -> anyhow::Result<()> {
288        let price = Price::try_from("50   sats")?;
289        let result = price.to_symbolic();
290        let expected = "50sats".to_string();
291        assert!(result == expected);
292        Ok(())
293    }
294
295    #[test]
296    fn test_serialize_usd() -> anyhow::Result<()> {
297        #[derive(serde::Serialize)]
298        struct Tmp {
299            price: Price,
300        }
301        let t = Tmp {
302            price: Price::try_from("50   usd")?,
303        };
304        let result = serde_json::to_string(&t)?;
305        let expected = "{\"price\":\"50.00 USD\"}".to_string();
306        assert!(result == expected);
307        Ok(())
308    }
309
310    #[test]
311    fn test_serialize_btc() -> anyhow::Result<()> {
312        #[derive(serde::Serialize)]
313        struct Tmp {
314            price: Price,
315        }
316        let t = Tmp {
317            price: Price::try_from("50   btc")?,
318        };
319        let result = serde_json::to_string(&t)?;
320        let expected = "{\"price\":\"50.00000000 BTC\"}".to_string();
321        println!("{}", result);
322        assert!(result == expected);
323        Ok(())
324    }
325
326    #[test]
327    fn test_str_price_correctly_parsed() -> anyhow::Result<()> {
328        let result = Price::try_from("50   usd")?;
329        let expected = Price {
330            quantity: rust_decimal::Decimal::from(50),
331            currency: Currency::USD,
332        };
333        assert!(result == expected);
334        Ok(())
335    }
336
337    #[test]
338    fn test_decimal_price_correctly_parsed() -> anyhow::Result<()> {
339        let result = Price::try_from("50.02   usd")?;
340        let expected = Price {
341            quantity: rust_decimal::Decimal::from_str("50.02")?,
342            currency: Currency::USD,
343        };
344        assert!(result == expected);
345        Ok(())
346    }
347
348    #[test]
349    fn test_usd_to_btc() -> anyhow::Result<()> {
350        let result = Price::try_from("50.02   usd")?;
351        let result = result.to_btc()?.currency;
352        let expected = Currency::BTC;
353        assert!(result == expected);
354        Ok(())
355    }
356}