123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186 |
- 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
- 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<Self, Error> {
- 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::<AmountCents>()
- .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<Self> {
- 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");
- }
- }
|