amount.rs 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. use crate::Asset;
  2. use serde::{ser::SerializeMap, Serialize, Serializer};
  3. /// The raw storage for cents, the more the better
  4. pub type AmountCents = i128;
  5. #[derive(Clone, Debug, Eq, PartialEq, thiserror::Error, Serialize)]
  6. pub enum Error {
  7. #[error("{0} is not a valid number")]
  8. NoANumber(String),
  9. }
  10. /// Amount
  11. ///
  12. /// The cents are stored in their lowest denomination, or their "cents". For
  13. /// instance a Bitcoin would be represented with a `precision` of 8, or dollars
  14. /// in a 2 or 3.
  15. ///
  16. /// For instance, if the dollar Asset has a `precision` of `2`, an `cents` of
  17. /// 1255 will represent `12.55 USD`.
  18. ///
  19. /// Amount will abstract all the math operations that can be performed with the
  20. /// amounts, guarding that the `Asset` are the same before performing any
  21. /// operation.
  22. ///
  23. ///
  24. /// The `cents` and `Asset.id` must be used to store amounts in the storage
  25. /// layer. Float or string representations should be used to display
  26. #[derive(Clone, Debug, Eq, PartialEq)]
  27. pub struct Amount {
  28. asset: Asset,
  29. cents: AmountCents,
  30. }
  31. impl Serialize for Amount {
  32. fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
  33. where
  34. S: Serializer,
  35. {
  36. let amount = self.to_string();
  37. let mut s = serializer.serialize_map(Some(2))?;
  38. s.serialize_entry("amount", &amount)?;
  39. s.serialize_entry("asset", self.asset.name.to_string().as_str())?;
  40. s.end()
  41. }
  42. }
  43. impl Amount {
  44. /// Creates a new amount from an asset and cents
  45. pub fn new(asset: Asset, cents: AmountCents) -> Self {
  46. Self { asset, cents }
  47. }
  48. /// Creates a new amount from a human amount (a floating number serialized
  49. /// as string to avoid loss precision)
  50. pub fn from_human(asset: Asset, human_amount: &str) -> Result<Self, Error> {
  51. let mut dot_at = None;
  52. for (pos, i) in human_amount.chars().enumerate() {
  53. match i {
  54. '-' => {
  55. if pos != 0 {
  56. return Err(Error::NoANumber(human_amount.to_owned()));
  57. }
  58. }
  59. '.' => {
  60. if dot_at.is_some() {
  61. return Err(Error::NoANumber(human_amount.to_owned()));
  62. }
  63. dot_at = Some(pos);
  64. }
  65. '0'..='9' => {}
  66. _ => {
  67. return Err(Error::NoANumber(human_amount.to_owned()));
  68. }
  69. }
  70. }
  71. let (whole, fractional_part) = if let Some(dot_at) = dot_at {
  72. let (whole, fractional_part) = human_amount.split_at(dot_at);
  73. (whole, fractional_part[1..].to_owned())
  74. } else {
  75. (human_amount, "".to_owned())
  76. };
  77. let fractional_part = fractional_part + &"0".repeat(asset.precision.into());
  78. let cents = (whole.to_owned() + &fractional_part[..asset.precision.into()])
  79. .parse::<AmountCents>()
  80. .map_err(|_| Error::NoANumber(format!("{}.{}", whole, fractional_part)))?;
  81. Ok(Self { asset, cents })
  82. }
  83. #[inline]
  84. /// Returns the asset for this amount
  85. pub fn asset(&self) -> &Asset {
  86. &self.asset
  87. }
  88. #[inline]
  89. /// Return the cents
  90. pub fn cents(&self) -> AmountCents {
  91. self.cents
  92. }
  93. /// Attempts to do a checked addition of two amounts
  94. /// This will fails if assets are not the same or it overflows or underflows
  95. pub fn checked_add(&self, other: &Self) -> Option<Self> {
  96. if self.asset != other.asset {
  97. return None;
  98. }
  99. self.cents.checked_add(other.cents).map(|cents| Self {
  100. asset: self.asset.clone(),
  101. cents,
  102. })
  103. }
  104. }
  105. impl ToString for Amount {
  106. fn to_string(&self) -> String {
  107. let str = self.cents.abs().to_string();
  108. let precision: usize = self.asset.precision.into();
  109. let str = if str.len() < precision + 1 {
  110. format!("{}{}", "0".repeat(precision - str.len() + 1), str)
  111. } else {
  112. str
  113. };
  114. let (left, right) = str.split_at(str.len() - precision);
  115. format!(
  116. "{}{}.{}",
  117. if self.cents.is_negative() { "-" } else { "" },
  118. left,
  119. right
  120. )
  121. .trim_end_matches("0")
  122. .trim_end_matches(".")
  123. .to_owned()
  124. }
  125. }
  126. #[cfg(test)]
  127. mod test {
  128. use super::*;
  129. #[test]
  130. fn dollar() {
  131. let usd = Asset {
  132. id: 1,
  133. precision: 4,
  134. name: "BTC".into(),
  135. };
  136. let amount = usd.new_amount(1022100);
  137. assert_eq!(amount.to_string(), "102.21");
  138. }
  139. #[test]
  140. fn bitcoin() {
  141. let btc = Asset {
  142. id: 1,
  143. precision: 8,
  144. name: "BTC".into(),
  145. };
  146. assert_eq!(btc.new_amount(1022100).to_string(), "0.010221");
  147. assert_eq!(btc.new_amount(10).to_string(), "0.0000001");
  148. assert_eq!(btc.new_amount(10000000).to_string(), "0.1");
  149. assert_eq!(btc.new_amount(100000000).to_string(), "1");
  150. assert_eq!(
  151. btc.new_amount(100000000)
  152. .checked_add(&btc.new_amount(100000000))
  153. .unwrap()
  154. .to_string(),
  155. "2",
  156. );
  157. assert_eq!(btc.new_amount(1000000000).to_string(), "10");
  158. }
  159. #[test]
  160. fn from_human() {
  161. let btc = Asset {
  162. id: 1,
  163. precision: 8,
  164. name: "BTC".into(),
  165. };
  166. let parsed_amount = Amount::from_human(btc.clone(), "0.1").expect("valid amount");
  167. assert_eq!(parsed_amount.to_string(), "0.1");
  168. let parsed_amount = Amount::from_human(btc.clone(), "-0.1").expect("valid amount");
  169. assert_eq!(parsed_amount.to_string(), "-0.1");
  170. let parsed_amount = Amount::from_human(btc.clone(), "-0.000001").expect("valid amount");
  171. assert_eq!(parsed_amount.to_string(), "-0.000001");
  172. let parsed_amount = Amount::from_human(btc.clone(), "-0.000000001").expect("valid amount");
  173. assert_eq!(parsed_amount.to_string(), "0");
  174. let parsed_amount = Amount::from_human(btc.clone(), "0.000001").expect("valid amount");
  175. assert_eq!(parsed_amount.to_string(), "0.000001");
  176. let parsed_amount =
  177. Amount::from_human(btc.clone(), "-0.000000100001").expect("valid amount");
  178. assert_eq!(parsed_amount.to_string(), "-0.0000001");
  179. let parsed_amount = Amount::from_human(btc, "100000").expect("valid amount");
  180. assert_eq!(parsed_amount.to_string(), "100000");
  181. }
  182. }