amount.rs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. use crate::Asset;
  2. use serde::{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)]
  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(Copy, 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 serialized = self.to_string();
  37. serializer.serialize_str(&serialized)
  38. }
  39. }
  40. impl Amount {
  41. pub fn new(asset: Asset, cents: AmountCents) -> Self {
  42. Self { asset, cents }
  43. }
  44. pub fn from_human(asset: Asset, human_amount: &str) -> Result<Self, Error> {
  45. let mut dot_at = None;
  46. for (pos, i) in human_amount.chars().enumerate() {
  47. match i {
  48. '-' => {
  49. if pos != 0 {
  50. return Err(Error::NoANumber(human_amount.to_owned()));
  51. }
  52. }
  53. '.' => {
  54. if dot_at.is_some() {
  55. return Err(Error::NoANumber(human_amount.to_owned()));
  56. }
  57. dot_at = Some(pos);
  58. }
  59. '0'..='9' => {}
  60. _ => {
  61. return Err(Error::NoANumber(human_amount.to_owned()));
  62. }
  63. }
  64. }
  65. let (whole, fractional_part) = if let Some(dot_at) = dot_at {
  66. let (whole, fractional_part) = human_amount.split_at(dot_at);
  67. (whole, fractional_part[1..].to_owned())
  68. } else {
  69. (human_amount, "".to_owned())
  70. };
  71. let fractional_part = fractional_part + &"0".repeat(asset.precision.into());
  72. let cents = (whole.to_owned() + &fractional_part[..asset.precision.into()])
  73. .parse::<AmountCents>()
  74. .map_err(|_| Error::NoANumber(format!("{}.{}", whole, fractional_part)))?;
  75. Ok(Self { asset, cents })
  76. }
  77. #[inline]
  78. pub fn asset(&self) -> &Asset {
  79. &self.asset
  80. }
  81. #[inline]
  82. pub fn cents(&self) -> AmountCents {
  83. self.cents
  84. }
  85. pub fn checked_add(&self, other: &Self) -> Option<Self> {
  86. if self.asset != other.asset {
  87. return None;
  88. }
  89. self.cents.checked_add(other.cents).map(|cents| Self {
  90. asset: self.asset,
  91. cents,
  92. })
  93. }
  94. }
  95. impl ToString for Amount {
  96. fn to_string(&self) -> String {
  97. let str = self.cents.abs().to_string();
  98. let precision: usize = self.asset.precision.into();
  99. let str = if str.len() < precision + 1 {
  100. format!("{}{}", "0".repeat(precision - str.len() + 1), str)
  101. } else {
  102. str
  103. };
  104. let (left, right) = str.split_at(str.len() - precision);
  105. format!(
  106. "{}{}.{}",
  107. if self.cents.is_negative() { "-" } else { "" },
  108. left,
  109. right
  110. )
  111. .trim_end_matches("0")
  112. .trim_end_matches(".")
  113. .to_owned()
  114. }
  115. }
  116. #[cfg(test)]
  117. mod test {
  118. use super::*;
  119. #[test]
  120. fn dollar() {
  121. let usd = Asset {
  122. id: 1,
  123. precision: 4,
  124. };
  125. let amount = usd.new_amount(1022100);
  126. assert_eq!(amount.to_string(), "102.21");
  127. }
  128. #[test]
  129. fn bitcoin() {
  130. let btc = Asset {
  131. id: 1,
  132. precision: 8,
  133. };
  134. assert_eq!(btc.new_amount(1022100).to_string(), "0.010221");
  135. assert_eq!(btc.new_amount(10).to_string(), "0.0000001");
  136. assert_eq!(btc.new_amount(10000000).to_string(), "0.1");
  137. assert_eq!(btc.new_amount(100000000).to_string(), "1");
  138. assert_eq!(
  139. btc.new_amount(100000000)
  140. .checked_add(&btc.new_amount(100000000))
  141. .unwrap()
  142. .to_string(),
  143. "2",
  144. );
  145. assert_eq!(btc.new_amount(1000000000).to_string(), "10");
  146. }
  147. #[test]
  148. fn from_human() {
  149. let btc = Asset {
  150. id: 1,
  151. precision: 8,
  152. };
  153. let parsed_amount = Amount::from_human(btc, "0.1").expect("valid amount");
  154. assert_eq!(parsed_amount.to_string(), "0.1");
  155. let parsed_amount = Amount::from_human(btc, "-0.1").expect("valid amount");
  156. assert_eq!(parsed_amount.to_string(), "-0.1");
  157. let parsed_amount = Amount::from_human(btc, "-0.000001").expect("valid amount");
  158. assert_eq!(parsed_amount.to_string(), "-0.000001");
  159. let parsed_amount = Amount::from_human(btc, "-0.000000001").expect("valid amount");
  160. assert_eq!(parsed_amount.to_string(), "0");
  161. let parsed_amount = Amount::from_human(btc, "0.000001").expect("valid amount");
  162. assert_eq!(parsed_amount.to_string(), "0.000001");
  163. let parsed_amount = Amount::from_human(btc, "-0.000000100001").expect("valid amount");
  164. assert_eq!(parsed_amount.to_string(), "-0.0000001");
  165. }
  166. }