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}