use crate::Asset; use serde::{Serialize, Serializer}; /// The raw storage for cents, the more the better pub type AmountCents = i128; #[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)] pub enum Error { #[error("{0} is not a valid number")] NoANumber(String), } /// Amount /// /// The cents are stored in their lowest denomination, or their "cents". For /// instance a Bitcoin would be represented with a `precision` of 8, or dollars /// in a 2 or 3. /// /// For instance, if the dollar Asset has a `precision` of `2`, an `cents` of /// 1255 will represent `12.55 USD`. /// /// Amount will abstract all the math operations that can be performed with the /// amounts, guarding that the `Asset` are the same before performing any /// operation. /// /// /// The `cents` and `Asset.id` must be used to store amounts in the storage /// layer. Float or string representations should be used to display #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct Amount { asset: Asset, cents: AmountCents, } impl Serialize for Amount { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let serialized = self.to_string(); serializer.serialize_str(&serialized) } } impl Amount { pub fn new(asset: Asset, cents: AmountCents) -> Self { Self { asset, cents } } pub fn from_human(asset: Asset, human_amount: &str) -> Result { let mut dot_at = None; for (pos, i) in human_amount.chars().enumerate() { match i { '-' => { if pos != 0 { return Err(Error::NoANumber(human_amount.to_owned())); } } '.' => { if dot_at.is_some() { return Err(Error::NoANumber(human_amount.to_owned())); } dot_at = Some(pos); } '0'..='9' => {} _ => { return Err(Error::NoANumber(human_amount.to_owned())); } } } let (whole, fractional_part) = if let Some(dot_at) = dot_at { let (whole, fractional_part) = human_amount.split_at(dot_at); (whole, fractional_part[1..].to_owned()) } else { (human_amount, "".to_owned()) }; let fractional_part = fractional_part + &"0".repeat(asset.precision.into()); let cents = (whole.to_owned() + &fractional_part[..asset.precision.into()]) .parse::() .map_err(|_| Error::NoANumber(format!("{}.{}", whole, fractional_part)))?; Ok(Self { asset, cents }) } #[inline] pub fn asset(&self) -> &Asset { &self.asset } #[inline] pub fn cents(&self) -> AmountCents { self.cents } pub fn checked_add(&self, other: &Self) -> Option { if self.asset != other.asset { return None; } self.cents.checked_add(other.cents).map(|cents| Self { asset: self.asset, cents, }) } } impl ToString for Amount { fn to_string(&self) -> String { let str = self.cents.abs().to_string(); let precision: usize = self.asset.precision.into(); let str = if str.len() < precision + 1 { format!("{}{}", "0".repeat(precision - str.len() + 1), str) } else { str }; let (left, right) = str.split_at(str.len() - precision); format!( "{}{}.{}", if self.cents.is_negative() { "-" } else { "" }, left, right ) .trim_end_matches("0") .trim_end_matches(".") .to_owned() } } #[cfg(test)] mod test { use super::*; #[test] fn dollar() { let usd = Asset { id: 1, precision: 4, }; let amount = usd.new_amount(1022100); assert_eq!(amount.to_string(), "102.21"); } #[test] fn bitcoin() { let btc = Asset { id: 1, precision: 8, }; assert_eq!(btc.new_amount(1022100).to_string(), "0.010221"); assert_eq!(btc.new_amount(10).to_string(), "0.0000001"); assert_eq!(btc.new_amount(10000000).to_string(), "0.1"); assert_eq!(btc.new_amount(100000000).to_string(), "1"); assert_eq!( btc.new_amount(100000000) .checked_add(&btc.new_amount(100000000)) .unwrap() .to_string(), "2", ); assert_eq!(btc.new_amount(1000000000).to_string(), "10"); } #[test] fn from_human() { let btc = Asset { id: 1, precision: 8, }; let parsed_amount = Amount::from_human(btc, "0.1").expect("valid amount"); assert_eq!(parsed_amount.to_string(), "0.1"); let parsed_amount = Amount::from_human(btc, "-0.1").expect("valid amount"); assert_eq!(parsed_amount.to_string(), "-0.1"); let parsed_amount = Amount::from_human(btc, "-0.000001").expect("valid amount"); assert_eq!(parsed_amount.to_string(), "-0.000001"); let parsed_amount = Amount::from_human(btc, "-0.000000001").expect("valid amount"); assert_eq!(parsed_amount.to_string(), "0"); let parsed_amount = Amount::from_human(btc, "0.000001").expect("valid amount"); assert_eq!(parsed_amount.to_string(), "0.000001"); let parsed_amount = Amount::from_human(btc, "-0.000000100001").expect("valid amount"); assert_eq!(parsed_amount.to_string(), "-0.0000001"); } }