use std::cmp::Ordering;
use std::default;
use std::fmt::{self, Write};
use std::ops;
use std::str::FromStr;
use thiserror::Error;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Denomination {
Wownero,
Verywow,
Muchwow,
Suchwow,
Dust,
}
impl Denomination {
fn precision(self) -> i32 {
match self {
Denomination::Wownero => -11,
Denomination::Verywow => -8,
Denomination::Muchwow => -5,
Denomination::Suchwow => -2,
Denomination::Dust => 0,
}
}
}
impl fmt::Display for Denomination {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(match *self {
Denomination::Wownero => "WOW",
Denomination::Verywow => "Verywow",
Denomination::Muchwow => "Muchwow",
Denomination::Suchwow => "Suchwow",
Denomination::Dust => "Dust",
})
}
}
impl FromStr for Denomination {
type Err = ParsingError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"WOW" => Ok(Denomination::Wownero),
"Verywow" => Ok(Denomination::Verywow),
"Muchwow" => Ok(Denomination::Muchwow),
"Suchwow" => Ok(Denomination::Suchwow),
"Dust" => Ok(Denomination::Dust),
d => Err(ParsingError::UnknownDenomination(d.to_owned())),
}
}
}
#[derive(Error, Debug, Clone, PartialEq, Eq)]
pub enum ParsingError {
#[error("Amount is negative")]
Negative,
#[error("Amount is too big to fit inside the type")]
TooBig,
#[error("Amount has higher precision than supported by the type")]
TooPrecise,
#[error("Invalid number format")]
InvalidFormat,
#[error("Input string was too large")]
InputTooLarge,
#[error("Invalid character in input: {0}")]
InvalidCharacter(char),
#[error("The denomination was unknown: {0}")]
UnknownDenomination(String),
}
fn is_too_precise(s: &str, precision: usize) -> bool {
s.contains('.') || precision >= s.len() || s.chars().rev().take(precision).any(|d| d != '0')
}
fn parse_signed_to_dust(mut s: &str, denom: Denomination) -> Result<(bool, u64), ParsingError> {
if s.is_empty() {
return Err(ParsingError::InvalidFormat);
}
if s.len() > 50 {
return Err(ParsingError::InputTooLarge);
}
let is_negative = s.starts_with('-');
if is_negative {
if s.len() == 1 {
return Err(ParsingError::InvalidFormat);
}
s = &s[1..];
}
let max_decimals = {
let precision_diff = -denom.precision();
if precision_diff < 0 {
let last_n = precision_diff.unsigned_abs() as usize;
if is_too_precise(s, last_n) {
return Err(ParsingError::TooPrecise);
}
s = &s[0..s.len() - last_n];
0
} else {
precision_diff
}
};
let mut decimals = None;
let mut value: u64 = 0; for c in s.chars() {
match c {
'0'..='9' => {
match 10_u64.checked_mul(value) {
None => return Err(ParsingError::TooBig),
Some(val) => match val.checked_add((c as u8 - b'0') as u64) {
None => return Err(ParsingError::TooBig),
Some(val) => value = val,
},
}
decimals = match decimals {
None => None,
Some(d) if d < max_decimals => Some(d + 1),
_ => return Err(ParsingError::TooPrecise),
};
}
'.' => match decimals {
None => decimals = Some(0),
_ => return Err(ParsingError::InvalidFormat),
},
c => return Err(ParsingError::InvalidCharacter(c)),
}
}
let scale_factor = max_decimals - decimals.unwrap_or(0);
for _ in 0..scale_factor {
value = match 10_u64.checked_mul(value) {
Some(v) => v,
None => return Err(ParsingError::TooBig),
};
}
Ok((is_negative, value))
}
fn fmt_dust_in(
dust: u64,
negative: bool,
f: &mut dyn fmt::Write,
denom: Denomination,
) -> fmt::Result {
if negative {
f.write_str("-")?;
}
let precision = denom.precision();
match precision.cmp(&0) {
Ordering::Greater => {
let width = precision as usize;
write!(f, "{}{:0width$}", dust, 0, width = width)?;
}
Ordering::Less => {
let nb_decimals = precision.unsigned_abs() as usize;
let real = format!("{:0width$}", dust, width = nb_decimals);
if real.len() == nb_decimals {
write!(f, "0.{}", &real[real.len() - nb_decimals..])?;
} else {
write!(
f,
"{}.{}",
&real[0..(real.len() - nb_decimals)],
&real[real.len() - nb_decimals..]
)?;
}
}
Ordering::Equal => write!(f, "{}", dust)?,
}
Ok(())
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Amount(u64);
impl Amount {
pub const ZERO: Amount = Amount(0);
pub const ONE_DUST: Amount = Amount(1);
pub const ONE_WOW: Amount = Amount(100_000_000_000);
pub fn from_dust(dust: u64) -> Amount {
Amount(dust)
}
pub fn as_dust(self) -> u64 {
self.0
}
pub fn max_value() -> Amount {
Amount(u64::max_value())
}
pub fn min_value() -> Amount {
Amount(u64::min_value())
}
pub fn from_wow(wow: f64) -> Result<Amount, ParsingError> {
Amount::from_float_in(wow, Denomination::Wownero)
}
pub fn from_str_in(s: &str, denom: Denomination) -> Result<Amount, ParsingError> {
let (negative, dust) = parse_signed_to_dust(s, denom)?;
if negative {
return Err(ParsingError::Negative);
}
if dust > i64::max_value() as u64 {
return Err(ParsingError::TooBig);
}
Ok(Amount::from_dust(dust))
}
pub fn from_str_with_denomination(s: &str) -> Result<Amount, ParsingError> {
let mut split = s.splitn(3, ' ');
let amt_str = split.next().unwrap();
let denom_str = split.next().ok_or(ParsingError::InvalidFormat)?;
if split.next().is_some() {
return Err(ParsingError::InvalidFormat);
}
Amount::from_str_in(amt_str, denom_str.parse()?)
}
pub fn to_float_in(self, denom: Denomination) -> f64 {
f64::from_str(&self.to_string_in(denom)).unwrap()
}
pub fn as_wow(self) -> f64 {
self.to_float_in(Denomination::Wownero)
}
pub fn from_float_in(value: f64, denom: Denomination) -> Result<Amount, ParsingError> {
if value < 0.0 {
return Err(ParsingError::Negative);
}
Amount::from_str_in(&value.to_string(), denom)
}
pub fn fmt_value_in(self, f: &mut dyn fmt::Write, denom: Denomination) -> fmt::Result {
fmt_dust_in(self.as_dust(), false, f, denom)
}
pub fn to_string_in(self, denom: Denomination) -> String {
let mut buf = String::new();
self.fmt_value_in(&mut buf, denom).unwrap();
buf
}
pub fn to_string_with_denomination(self, denom: Denomination) -> String {
let mut buf = String::new();
self.fmt_value_in(&mut buf, denom).unwrap();
write!(buf, " {}", denom).unwrap();
buf
}
pub fn checked_add(self, rhs: Amount) -> Option<Amount> {
self.0.checked_add(rhs.0).map(Amount)
}
pub fn checked_sub(self, rhs: Amount) -> Option<Amount> {
self.0.checked_sub(rhs.0).map(Amount)
}
pub fn checked_mul(self, rhs: u64) -> Option<Amount> {
self.0.checked_mul(rhs).map(Amount)
}
pub fn checked_div(self, rhs: u64) -> Option<Amount> {
self.0.checked_div(rhs).map(Amount)
}
pub fn checked_rem(self, rhs: u64) -> Option<Amount> {
self.0.checked_rem(rhs).map(Amount)
}
pub fn to_signed(self) -> Result<SignedAmount, ParsingError> {
if self.as_dust() > SignedAmount::max_value().as_dust() as u64 {
Err(ParsingError::TooBig)
} else {
Ok(SignedAmount::from_dust(self.as_dust() as i64))
}
}
}
impl default::Default for Amount {
fn default() -> Self {
Amount::ZERO
}
}
impl fmt::Debug for Amount {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Amount({:.12} WOW)", self.as_wow())
}
}
impl fmt::Display for Amount {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.fmt_value_in(f, Denomination::Wownero)?;
write!(f, " {}", Denomination::Wownero)
}
}
impl ops::Add for Amount {
type Output = Amount;
fn add(self, rhs: Amount) -> Self::Output {
self.checked_add(rhs).expect("Amount addition error")
}
}
impl ops::AddAssign for Amount {
fn add_assign(&mut self, other: Amount) {
*self = *self + other
}
}
impl ops::Sub for Amount {
type Output = Amount;
fn sub(self, rhs: Amount) -> Self::Output {
self.checked_sub(rhs).expect("Amount subtraction error")
}
}
impl ops::SubAssign for Amount {
fn sub_assign(&mut self, other: Amount) {
*self = *self - other
}
}
impl ops::Rem<u64> for Amount {
type Output = Amount;
fn rem(self, modulus: u64) -> Self {
self.checked_rem(modulus).expect("Amount remainder error")
}
}
impl ops::RemAssign<u64> for Amount {
fn rem_assign(&mut self, modulus: u64) {
*self = *self % modulus
}
}
impl ops::Mul<u64> for Amount {
type Output = Amount;
fn mul(self, rhs: u64) -> Self::Output {
self.checked_mul(rhs).expect("Amount multiplication error")
}
}
impl ops::MulAssign<u64> for Amount {
fn mul_assign(&mut self, rhs: u64) {
*self = *self * rhs
}
}
impl ops::Div<u64> for Amount {
type Output = Amount;
fn div(self, rhs: u64) -> Self::Output {
self.checked_div(rhs).expect("Amount division error")
}
}
impl ops::DivAssign<u64> for Amount {
fn div_assign(&mut self, rhs: u64) {
*self = *self / rhs
}
}
impl FromStr for Amount {
type Err = ParsingError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Amount::from_str_with_denomination(s)
}
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SignedAmount(i64);
impl SignedAmount {
pub const ZERO: SignedAmount = SignedAmount(0);
pub const ONE_DUST: SignedAmount = SignedAmount(1);
pub const ONE_WOW: SignedAmount = SignedAmount(100_000_000_000);
pub fn from_dust(dust: i64) -> SignedAmount {
SignedAmount(dust)
}
pub fn as_dust(self) -> i64 {
self.0
}
pub fn max_value() -> SignedAmount {
SignedAmount(i64::max_value())
}
pub fn min_value() -> SignedAmount {
SignedAmount(i64::min_value())
}
pub fn from_wow(wow: f64) -> Result<SignedAmount, ParsingError> {
SignedAmount::from_float_in(wow, Denomination::Wownero)
}
pub fn from_str_in(s: &str, denom: Denomination) -> Result<SignedAmount, ParsingError> {
let (negative, dust) = parse_signed_to_dust(s, denom)?;
if dust > i64::max_value() as u64 {
return Err(ParsingError::TooBig);
}
Ok(match negative {
true => SignedAmount(-(dust as i64)),
false => SignedAmount(dust as i64),
})
}
pub fn from_str_with_denomination(s: &str) -> Result<SignedAmount, ParsingError> {
let mut split = s.splitn(3, ' ');
let amt_str = split.next().unwrap();
let denom_str = split.next().ok_or(ParsingError::InvalidFormat)?;
if split.next().is_some() {
return Err(ParsingError::InvalidFormat);
}
SignedAmount::from_str_in(amt_str, denom_str.parse()?)
}
pub fn to_float_in(self, denom: Denomination) -> f64 {
f64::from_str(&self.to_string_in(denom)).unwrap()
}
pub fn as_wow(self) -> f64 {
self.to_float_in(Denomination::Wownero)
}
pub fn from_float_in(value: f64, denom: Denomination) -> Result<SignedAmount, ParsingError> {
SignedAmount::from_str_in(&value.to_string(), denom)
}
pub fn fmt_value_in(self, f: &mut dyn fmt::Write, denom: Denomination) -> fmt::Result {
let dusts = self
.as_dust()
.checked_abs()
.map(|a: i64| a as u64)
.unwrap_or_else(|| {
u64::max_value() - self.as_dust() as u64 + 1
});
fmt_dust_in(dusts, self.is_negative(), f, denom)
}
pub fn to_string_in(self, denom: Denomination) -> String {
let mut buf = String::new();
self.fmt_value_in(&mut buf, denom).unwrap();
buf
}
pub fn to_string_with_denomination(self, denom: Denomination) -> String {
let mut buf = String::new();
self.fmt_value_in(&mut buf, denom).unwrap();
write!(buf, " {}", denom).unwrap();
buf
}
pub fn abs(self) -> SignedAmount {
SignedAmount(self.0.abs())
}
pub fn signum(self) -> i64 {
self.0.signum()
}
pub fn is_positive(self) -> bool {
self.0.is_positive()
}
pub fn is_negative(self) -> bool {
self.0.is_negative()
}
pub fn checked_abs(self) -> Option<SignedAmount> {
self.0.checked_abs().map(SignedAmount)
}
pub fn checked_add(self, rhs: SignedAmount) -> Option<SignedAmount> {
self.0.checked_add(rhs.0).map(SignedAmount)
}
pub fn checked_sub(self, rhs: SignedAmount) -> Option<SignedAmount> {
self.0.checked_sub(rhs.0).map(SignedAmount)
}
pub fn checked_mul(self, rhs: i64) -> Option<SignedAmount> {
self.0.checked_mul(rhs).map(SignedAmount)
}
pub fn checked_div(self, rhs: i64) -> Option<SignedAmount> {
self.0.checked_div(rhs).map(SignedAmount)
}
pub fn checked_rem(self, rhs: i64) -> Option<SignedAmount> {
self.0.checked_rem(rhs).map(SignedAmount)
}
pub fn positive_sub(self, rhs: SignedAmount) -> Option<SignedAmount> {
if self.is_negative() || rhs.is_negative() || rhs > self {
None
} else {
self.checked_sub(rhs)
}
}
pub fn to_unsigned(self) -> Result<Amount, ParsingError> {
if self.is_negative() {
Err(ParsingError::Negative)
} else {
Ok(Amount::from_dust(self.as_dust() as u64))
}
}
}
impl default::Default for SignedAmount {
fn default() -> Self {
SignedAmount::ZERO
}
}
impl fmt::Debug for SignedAmount {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "SignedAmount({:.12} WOW)", self.as_wow())
}
}
impl fmt::Display for SignedAmount {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.fmt_value_in(f, Denomination::Wownero)?;
write!(f, " {}", Denomination::Wownero)
}
}
impl ops::Add for SignedAmount {
type Output = SignedAmount;
fn add(self, rhs: SignedAmount) -> Self::Output {
self.checked_add(rhs).expect("SignedAmount addition error")
}
}
impl ops::AddAssign for SignedAmount {
fn add_assign(&mut self, other: SignedAmount) {
*self = *self + other
}
}
impl ops::Sub for SignedAmount {
type Output = SignedAmount;
fn sub(self, rhs: SignedAmount) -> Self::Output {
self.checked_sub(rhs)
.expect("SignedAmount subtraction error")
}
}
impl ops::SubAssign for SignedAmount {
fn sub_assign(&mut self, other: SignedAmount) {
*self = *self - other
}
}
impl ops::Rem<i64> for SignedAmount {
type Output = SignedAmount;
fn rem(self, modulus: i64) -> Self {
self.checked_rem(modulus)
.expect("SignedAmount remainder error")
}
}
impl ops::RemAssign<i64> for SignedAmount {
fn rem_assign(&mut self, modulus: i64) {
*self = *self % modulus
}
}
impl ops::Mul<i64> for SignedAmount {
type Output = SignedAmount;
fn mul(self, rhs: i64) -> Self::Output {
self.checked_mul(rhs)
.expect("SignedAmount multiplication error")
}
}
impl ops::MulAssign<i64> for SignedAmount {
fn mul_assign(&mut self, rhs: i64) {
*self = *self * rhs
}
}
impl ops::Div<i64> for SignedAmount {
type Output = SignedAmount;
fn div(self, rhs: i64) -> Self::Output {
self.checked_div(rhs).expect("SignedAmount division error")
}
}
impl ops::DivAssign<i64> for SignedAmount {
fn div_assign(&mut self, rhs: i64) {
*self = *self / rhs
}
}
impl FromStr for SignedAmount {
type Err = ParsingError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
SignedAmount::from_str_with_denomination(s)
}
}
#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
pub mod serde {
use super::{Amount, Denomination, SignedAmount};
use sealed::sealed;
use serde_crate::{Deserialize, Deserializer, Serialize, Serializer};
#[sealed]
pub trait SerdeAmount: Copy + Sized {
fn ser_dust<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error>;
fn des_dust<'d, D: Deserializer<'d>>(d: D) -> Result<Self, D::Error>;
fn ser_wow<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error>;
fn des_wow<'d, D: Deserializer<'d>>(d: D) -> Result<Self, D::Error>;
}
#[sealed]
pub trait SerdeAmountForOpt: Copy + Sized + SerdeAmount {
fn type_prefix() -> &'static str;
fn ser_dust_opt<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error>;
fn ser_wow_opt<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error>;
}
#[sealed]
impl SerdeAmount for Amount {
fn ser_dust<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error> {
u64::serialize(&self.as_dust(), s)
}
fn des_dust<'d, D: Deserializer<'d>>(d: D) -> Result<Self, D::Error> {
Ok(Amount::from_dust(u64::deserialize(d)?))
}
fn ser_wow<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error> {
String::serialize(&self.to_string_in(Denomination::Wownero), s)
}
fn des_wow<'d, D: Deserializer<'d>>(d: D) -> Result<Self, D::Error> {
use serde_crate::de::Error;
Amount::from_str_in(&String::deserialize(d)?, Denomination::Wownero)
.map_err(D::Error::custom)
}
}
#[sealed]
impl SerdeAmountForOpt for Amount {
fn type_prefix() -> &'static str {
"u"
}
fn ser_dust_opt<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_some(&self.as_dust())
}
fn ser_wow_opt<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_some(&self.to_string_in(Denomination::Wownero))
}
}
#[sealed]
impl SerdeAmount for SignedAmount {
fn ser_dust<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error> {
i64::serialize(&self.as_dust(), s)
}
fn des_dust<'d, D: Deserializer<'d>>(d: D) -> Result<Self, D::Error> {
Ok(SignedAmount::from_dust(i64::deserialize(d)?))
}
fn ser_wow<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error> {
String::serialize(&self.to_string_in(Denomination::Wownero), s)
}
fn des_wow<'d, D: Deserializer<'d>>(d: D) -> Result<Self, D::Error> {
use serde_crate::de::Error;
SignedAmount::from_str_in(&String::deserialize(d)?, Denomination::Wownero)
.map_err(D::Error::custom)
}
}
#[sealed]
impl SerdeAmountForOpt for SignedAmount {
fn type_prefix() -> &'static str {
"i"
}
fn ser_dust_opt<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_some(&self.as_dust())
}
fn ser_wow_opt<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_some(&self.to_string_in(Denomination::Wownero))
}
}
pub mod as_dust {
#![allow(missing_docs)]
use super::SerdeAmount;
use serde_crate::{Deserializer, Serializer};
pub fn serialize<A: SerdeAmount, S: Serializer>(a: &A, s: S) -> Result<S::Ok, S::Error> {
a.ser_dust(s)
}
pub fn deserialize<'d, A: SerdeAmount, D: Deserializer<'d>>(d: D) -> Result<A, D::Error> {
A::des_dust(d)
}
pub mod opt {
use super::super::SerdeAmountForOpt;
use core::fmt;
use core::marker::PhantomData;
use serde_crate::{de, Deserializer, Serializer};
pub fn serialize<A: SerdeAmountForOpt, S: Serializer>(
a: &Option<A>,
s: S,
) -> Result<S::Ok, S::Error> {
match *a {
Some(a) => a.ser_dust_opt(s),
None => s.serialize_none(),
}
}
pub fn deserialize<'d, A: SerdeAmountForOpt, D: Deserializer<'d>>(
d: D,
) -> Result<Option<A>, D::Error> {
struct VisitOptAmt<X>(PhantomData<X>);
impl<'de, X: SerdeAmountForOpt> de::Visitor<'de> for VisitOptAmt<X> {
type Value = Option<X>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "An Option<{}64>", X::type_prefix())
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(None)
}
fn visit_some<D>(self, d: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
Ok(Some(X::des_dust(d)?))
}
}
d.deserialize_option(VisitOptAmt::<A>(PhantomData))
}
}
}
pub mod as_wow {
#![allow(missing_docs)]
use super::SerdeAmount;
use serde_crate::{Deserializer, Serializer};
pub fn serialize<A: SerdeAmount, S: Serializer>(a: &A, s: S) -> Result<S::Ok, S::Error> {
a.ser_wow(s)
}
pub fn deserialize<'d, A: SerdeAmount, D: Deserializer<'d>>(d: D) -> Result<A, D::Error> {
A::des_wow(d)
}
pub mod opt {
use super::super::SerdeAmountForOpt;
use core::fmt;
use core::marker::PhantomData;
use serde_crate::{de, Deserializer, Serializer};
pub fn serialize<A: SerdeAmountForOpt, S: Serializer>(
a: &Option<A>,
s: S,
) -> Result<S::Ok, S::Error> {
match *a {
Some(a) => a.ser_wow_opt(s),
None => s.serialize_none(),
}
}
pub fn deserialize<'d, A: SerdeAmountForOpt, D: Deserializer<'d>>(
d: D,
) -> Result<Option<A>, D::Error> {
struct VisitOptAmt<X>(PhantomData<X>);
impl<'de, X: SerdeAmountForOpt> de::Visitor<'de> for VisitOptAmt<X> {
type Value = Option<X>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "An Option<String>")
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(None)
}
fn visit_some<D>(self, d: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
Ok(Some(X::des_wow(d)?))
}
}
d.deserialize_option(VisitOptAmt::<A>(PhantomData))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::panic;
use std::str::FromStr;
#[cfg(feature = "serde")]
use serde_test;
#[test]
fn add_sub_mul_div() {
let dust = Amount::from_dust;
let sdust = SignedAmount::from_dust;
assert_eq!(dust(15) + dust(15), dust(30));
assert_eq!(dust(15) - dust(15), dust(0));
assert_eq!(dust(14) * 3, dust(42));
assert_eq!(dust(14) / 2, dust(7));
assert_eq!(dust(14) % 3, dust(2));
assert_eq!(sdust(15) - sdust(20), sdust(-5));
assert_eq!(sdust(-14) * 3, sdust(-42));
assert_eq!(sdust(-14) / 2, sdust(-7));
assert_eq!(sdust(-14) % 3, sdust(-2));
let mut b = sdust(-5);
b += sdust(13);
assert_eq!(b, sdust(8));
b -= sdust(3);
assert_eq!(b, sdust(5));
b *= 6;
assert_eq!(b, sdust(30));
b /= 3;
assert_eq!(b, sdust(10));
b %= 3;
assert_eq!(b, sdust(1));
let result = panic::catch_unwind(|| Amount::max_value() + Amount::from_dust(1));
assert!(result.is_err());
let result = panic::catch_unwind(|| Amount::from_dust(8446744073709551615) * 3);
assert!(result.is_err());
}
#[test]
fn checked_arithmetic() {
let dust = Amount::from_dust;
let sdust = SignedAmount::from_dust;
assert_eq!(dust(42).checked_add(dust(1)), Some(dust(43)));
assert_eq!(SignedAmount::max_value().checked_add(sdust(1)), None);
assert_eq!(SignedAmount::min_value().checked_sub(sdust(1)), None);
assert_eq!(Amount::max_value().checked_add(dust(1)), None);
assert_eq!(Amount::min_value().checked_sub(dust(1)), None);
assert_eq!(dust(5).checked_sub(dust(3)), Some(dust(2)));
assert_eq!(dust(5).checked_sub(dust(6)), None);
assert_eq!(sdust(5).checked_sub(sdust(6)), Some(sdust(-1)));
assert_eq!(dust(5).checked_rem(2), Some(dust(1)));
assert_eq!(dust(5).checked_div(2), Some(dust(2))); assert_eq!(sdust(-6).checked_div(2), Some(sdust(-3)));
assert_eq!(sdust(-5).positive_sub(sdust(3)), None);
assert_eq!(sdust(5).positive_sub(sdust(-3)), None);
assert_eq!(sdust(3).positive_sub(sdust(5)), None);
assert_eq!(sdust(3).positive_sub(sdust(3)), Some(sdust(0)));
assert_eq!(sdust(5).positive_sub(sdust(3)), Some(sdust(2)));
}
#[test]
#[allow(clippy::float_cmp)]
fn floating_point() {
use super::Denomination as D;
let f = Amount::from_float_in;
let sf = SignedAmount::from_float_in;
let dust = Amount::from_dust;
let sdust = SignedAmount::from_dust;
assert_eq!(f(112.2, D::Wownero), Ok(dust(11220000000000)));
assert_eq!(sf(-112.2, D::Verywow), Ok(sdust(-11220000000)));
assert_eq!(f(112.2, D::Muchwow), Ok(dust(11220000)));
assert_eq!(f(0.001234, D::Wownero), Ok(dust(123400000)));
assert_eq!(sf(-0.0012345, D::Wownero), Ok(sdust(-123450000)));
assert_eq!(f(-1000.0, D::Dust), Err(ParsingError::Negative));
assert_eq!(f(112.2, D::Dust), Err(ParsingError::TooPrecise));
assert_eq!(sf(-0.1, D::Dust), Err(ParsingError::TooPrecise));
assert_eq!(
f(42.000_000_000_000_1, D::Wownero),
Err(ParsingError::TooPrecise)
);
assert_eq!(sf(-184467440738.0, D::Wownero), Err(ParsingError::TooBig));
assert_eq!(
f(18446744073709551617.0, D::Dust),
Err(ParsingError::TooBig)
);
assert_eq!(
f(
SignedAmount::max_value().to_float_in(D::Dust) + 1.0,
D::Dust
),
Err(ParsingError::TooBig)
);
assert_eq!(
f(Amount::max_value().to_float_in(D::Dust) + 1.0, D::Dust),
Err(ParsingError::TooBig)
);
let wow = move |f| SignedAmount::from_wow(f).unwrap();
assert_eq!(wow(2.5).to_float_in(D::Wownero), 2.5);
assert_eq!(wow(-2.5).to_float_in(D::Verywow), -2500.0);
assert_eq!(wow(-2.5).to_float_in(D::Muchwow), -2500000.0);
assert_eq!(wow(-2.5).to_float_in(D::Suchwow), -2500000000.0);
assert_eq!(wow(2.5).to_float_in(D::Dust), 250000000000.0);
let wow = move |f| Amount::from_wow(f).unwrap();
assert_eq!(&wow(0.0012).to_float_in(D::Wownero).to_string(), "0.0012")
}
#[test]
fn parsing() {
use super::ParsingError as E;
let wow = Denomination::Wownero;
let dust = Denomination::Dust;
let p = Amount::from_str_in;
let sp = SignedAmount::from_str_in;
assert_eq!(p("x", wow), Err(E::InvalidCharacter('x')));
assert_eq!(p("-", wow), Err(E::InvalidFormat));
assert_eq!(sp("-", wow), Err(E::InvalidFormat));
assert_eq!(p("-1.0x", wow), Err(E::InvalidCharacter('x')));
assert_eq!(p("0.0 ", wow), Err(ParsingError::InvalidCharacter(' ')));
assert_eq!(p("0.000.000", wow), Err(E::InvalidFormat));
let more_than_max = format!("1{}", Amount::max_value());
assert_eq!(p(&more_than_max, wow), Err(E::TooBig));
assert_eq!(p("0.0000000000042", wow), Err(E::TooPrecise));
assert_eq!(p("1", wow), Ok(Amount::from_dust(100_000_000_000)));
assert_eq!(sp("-.5", wow), Ok(SignedAmount::from_dust(-50_000_000_000)));
assert_eq!(p("1.1", wow), Ok(Amount::from_dust(110_000_000_000)));
assert_eq!(p("100", dust), Ok(Amount::from_dust(100)));
assert_eq!(p("55", dust), Ok(Amount::from_dust(55)));
assert_eq!(
p("5500000000000000000", dust),
Ok(Amount::from_dust(5_500_000_000_000_000_000))
);
assert_eq!(
p("5500000000000000000.", dust),
Ok(Amount::from_dust(5_500_000_000_000_000_000))
);
assert_eq!(
p("12345671.2345678912", wow),
Ok(Amount::from_dust(1_234_567_123_456_789_120))
);
let amount = Amount::from_dust(i64::max_value() as u64);
assert_eq!(
Amount::from_str_in(&amount.to_string_in(dust), dust),
Ok(amount)
);
assert_eq!(
Amount::from_str_in(&(amount + Amount(1)).to_string_in(dust), dust),
Err(E::TooBig)
);
assert_eq!(
p(
"100000000000000.0000000000000000000000000000000000",
Denomination::Wownero
),
Err(E::TooBig)
);
assert_eq!(
p(
"100000000000000.00000000000000000000000000000000000",
Denomination::Wownero
),
Err(E::InputTooLarge)
);
}
#[test]
fn to_string() {
use super::Denomination as D;
assert_eq!(Amount::ONE_WOW.to_string_in(D::Wownero), "1.00000000000");
assert_eq!(Amount::ONE_WOW.to_string_in(D::Dust), "100000000000");
assert_eq!(Amount::ONE_DUST.to_string_in(D::Wownero), "0.00000000001");
assert_eq!(
SignedAmount::from_dust(-42).to_string_in(D::Wownero),
"-0.00000000042"
);
assert_eq!(
Amount::ONE_WOW.to_string_with_denomination(D::Wownero),
"1.00000000000 WOW"
);
assert_eq!(
SignedAmount::ONE_WOW.to_string_with_denomination(D::Dust),
"100000000000 Dust"
);
assert_eq!(
Amount::ONE_DUST.to_string_with_denomination(D::Wownero),
"0.00000000001 WOW"
);
assert_eq!(
SignedAmount::from_dust(-42).to_string_with_denomination(D::Wownero),
"-0.00000000042 WOW"
);
}
#[test]
fn test_unsigned_signed_conversion() {
use super::ParsingError as E;
let p = Amount::from_dust;
let sp = SignedAmount::from_dust;
assert_eq!(Amount::max_value().to_signed(), Err(E::TooBig));
assert_eq!(
p(i64::max_value() as u64).to_signed(),
Ok(sp(i64::max_value()))
);
assert_eq!(p(0).to_signed(), Ok(sp(0)));
assert_eq!(p(1).to_signed(), Ok(sp(1)));
assert_eq!(p(1).to_signed(), Ok(sp(1)));
assert_eq!(p(i64::max_value() as u64 + 1).to_signed(), Err(E::TooBig));
assert_eq!(sp(-1).to_unsigned(), Err(E::Negative));
assert_eq!(
sp(i64::max_value()).to_unsigned(),
Ok(p(i64::max_value() as u64))
);
assert_eq!(sp(0).to_unsigned().unwrap().to_signed(), Ok(sp(0)));
assert_eq!(sp(1).to_unsigned().unwrap().to_signed(), Ok(sp(1)));
assert_eq!(
sp(i64::max_value()).to_unsigned().unwrap().to_signed(),
Ok(sp(i64::max_value()))
);
}
#[test]
fn from_str() {
use super::ParsingError as E;
let p = Amount::from_str;
let sp = SignedAmount::from_str;
assert_eq!(p("x WOW"), Err(E::InvalidCharacter('x')));
assert_eq!(p("5 WOW WOW"), Err(E::InvalidFormat));
assert_eq!(p("5 5 WOW"), Err(E::InvalidFormat));
assert_eq!(p("5 BCH"), Err(E::UnknownDenomination("BCH".to_owned())));
assert_eq!(p("-1 WOW"), Err(E::Negative));
assert_eq!(p("-0.0 WOW"), Err(E::Negative));
assert_eq!(p("0.1234567891234 WOW"), Err(E::TooPrecise));
assert_eq!(sp("-0.1 Dust"), Err(E::TooPrecise));
assert_eq!(p("0.1234567 Muchwow"), Err(E::TooPrecise));
assert_eq!(sp("-1.0001 Suchwow"), Err(E::TooPrecise));
assert_eq!(sp("-200000000000 WOW"), Err(E::TooBig));
assert_eq!(p("18446744073709551616 Dust"), Err(E::TooBig));
assert_eq!(p(".5 Suchwow"), Ok(Amount::from_dust(50)));
assert_eq!(sp("-.5 Suchwow"), Ok(SignedAmount::from_dust(-50)));
assert_eq!(p("0.00000253583 WOW"), Ok(Amount::from_dust(253583)));
assert_eq!(sp("-5 Dust"), Ok(SignedAmount::from_dust(-5)));
assert_eq!(
p("0.10000000000 WOW"),
Ok(Amount::from_dust(10_000_000_000))
);
assert_eq!(sp("-10 Suchwow"), Ok(SignedAmount::from_dust(-1_000)));
assert_eq!(sp("-10 Muchwow"), Ok(SignedAmount::from_dust(-1_000_000)));
assert_eq!(
sp("-10 Verywow"),
Ok(SignedAmount::from_dust(-1_000_000_000))
);
}
#[test]
fn to_from_string_in() {
use super::Denomination as D;
let ua_str = Amount::from_str_in;
let ua_sat = Amount::from_dust;
let sa_str = SignedAmount::from_str_in;
let sa_sat = SignedAmount::from_dust;
assert_eq!("0.50", Amount::from_dust(50).to_string_in(D::Suchwow));
assert_eq!(
"-0.50",
SignedAmount::from_dust(-50).to_string_in(D::Suchwow)
);
assert_eq!(
"0.02535830000",
Amount::from_dust(2535830000).to_string_in(D::Wownero)
);
assert_eq!("-5", SignedAmount::from_dust(-5).to_string_in(D::Dust));
assert_eq!(
"0.10000000000",
Amount::from_dust(10_000_000_000).to_string_in(D::Wownero)
);
assert_eq!(
"-10.00",
SignedAmount::from_dust(-1_000).to_string_in(D::Suchwow)
);
assert_eq!(
ua_str(&ua_sat(0).to_string_in(D::Dust), D::Dust),
Ok(ua_sat(0))
);
assert_eq!(
ua_str(&ua_sat(500).to_string_in(D::Wownero), D::Wownero),
Ok(ua_sat(500))
);
assert_eq!(
ua_str(&ua_sat(21_000_000).to_string_in(D::Suchwow), D::Suchwow),
Ok(ua_sat(21_000_000))
);
assert_eq!(
ua_str(&ua_sat(1).to_string_in(D::Muchwow), D::Muchwow),
Ok(ua_sat(1))
);
assert_eq!(
ua_str(
&ua_sat(1_000_000_000_000).to_string_in(D::Verywow),
D::Verywow
),
Ok(ua_sat(1_000_000_000_000))
);
assert_eq!(
ua_str(
&ua_sat(u64::max_value()).to_string_in(D::Verywow),
D::Verywow
),
Err(ParsingError::TooBig)
);
assert_eq!(
sa_str(&sa_sat(-1).to_string_in(D::Muchwow), D::Muchwow),
Ok(sa_sat(-1))
);
assert_eq!(
sa_str(&sa_sat(i64::max_value()).to_string_in(D::Dust), D::Muchwow),
Err(ParsingError::TooBig)
);
assert_eq!(
sa_str(&sa_sat(i64::min_value()).to_string_in(D::Dust), D::Muchwow),
Err(ParsingError::TooBig)
);
}
#[test]
fn to_string_with_denomination_from_str_roundtrip() {
use super::Denomination as D;
let amt = Amount::from_dust(42);
let denom = Amount::to_string_with_denomination;
assert_eq!(Amount::from_str(&denom(amt, D::Wownero)), Ok(amt));
assert_eq!(Amount::from_str(&denom(amt, D::Verywow)), Ok(amt));
assert_eq!(Amount::from_str(&denom(amt, D::Muchwow)), Ok(amt));
assert_eq!(Amount::from_str(&denom(amt, D::Suchwow)), Ok(amt));
assert_eq!(Amount::from_str(&denom(amt, D::Dust)), Ok(amt));
assert_eq!(
Amount::from_str("42 Dust WOW"),
Err(ParsingError::InvalidFormat)
);
assert_eq!(
SignedAmount::from_str("-42 Dust WOW"),
Err(ParsingError::InvalidFormat)
);
}
#[cfg(feature = "serde")]
#[test]
fn serde_as_dust() {
use serde_crate::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug)]
#[cfg_attr(feature = "serde", serde(crate = "serde_crate"))]
struct T {
#[serde(with = "super::serde::as_dust")]
pub amt: Amount,
#[serde(with = "super::serde::as_dust")]
pub samt: SignedAmount,
}
serde_test::assert_tokens(
&T {
amt: Amount::from_dust(123456789),
samt: SignedAmount::from_dust(-123456789),
},
&[
serde_test::Token::Struct { name: "T", len: 2 },
serde_test::Token::Str("amt"),
serde_test::Token::U64(123456789),
serde_test::Token::Str("samt"),
serde_test::Token::I64(-123456789),
serde_test::Token::StructEnd,
],
);
}
#[cfg(feature = "serde")]
#[test]
fn serde_as_dust_opt() {
use serde_crate::{Deserialize, Serialize};
use serde_json;
#[derive(Serialize, Deserialize, PartialEq, Debug, Eq)]
#[cfg_attr(feature = "serde", serde(crate = "serde_crate"))]
struct T {
#[serde(default, with = "super::serde::as_dust::opt")]
pub amt: Option<Amount>,
#[serde(default, with = "super::serde::as_dust::opt")]
pub samt: Option<SignedAmount>,
}
let with = T {
amt: Some(Amount::from_dust(2_500_000_000_000)),
samt: Some(SignedAmount::from_dust(-2_500_000_000_000)),
};
let without = T {
amt: None,
samt: None,
};
for s in [&with, &without].iter() {
let v = serde_json::to_string(s).unwrap();
let w: T = serde_json::from_str(&v).unwrap();
assert_eq!(w, **s);
}
let t: T =
serde_json::from_str("{\"amt\": 2500000000000, \"samt\": -2500000000000}").unwrap();
assert_eq!(t, with);
let t: T = serde_json::from_str("{}").unwrap();
assert_eq!(t, without);
let value_with: serde_json::Value =
serde_json::from_str("{\"amt\": 2500000000000, \"samt\": -2500000000000}").unwrap();
assert_eq!(with, serde_json::from_value(value_with).unwrap());
let value_without: serde_json::Value = serde_json::from_str("{}").unwrap();
assert_eq!(without, serde_json::from_value(value_without).unwrap());
}
#[cfg(feature = "serde")]
#[test]
fn serde_as_wow() {
use serde_crate::{Deserialize, Serialize};
use serde_json;
#[derive(Serialize, Deserialize, PartialEq, Debug)]
#[cfg_attr(feature = "serde", serde(crate = "serde_crate"))]
struct T {
#[serde(with = "super::serde::as_wow")]
pub amt: Amount,
#[serde(with = "super::serde::as_wow")]
pub samt: SignedAmount,
}
let orig = T {
amt: Amount::from_dust(900_000_000_000_000_001),
samt: SignedAmount::from_dust(-900_000_000_000_000_001),
};
let json = "{\"amt\": \"9000000.00000000001\", \
\"samt\": \"-9000000.00000000001\"}";
let t: T = serde_json::from_str(json).unwrap();
assert_eq!(t, orig);
let value: serde_json::Value = serde_json::from_str(json).unwrap();
assert_eq!(t, serde_json::from_value(value).unwrap());
let t: Result<T, serde_json::Error> =
serde_json::from_str("{\"amt\": \"1000000.0000000000001\", \"samt\": \"1\"}");
assert!(t
.unwrap_err()
.to_string()
.contains(&ParsingError::TooPrecise.to_string()));
let t: Result<T, serde_json::Error> =
serde_json::from_str("{\"amt\": \"-1\", \"samt\": \"1\"}");
assert!(t
.unwrap_err()
.to_string()
.contains(&ParsingError::Negative.to_string()));
}
#[cfg(feature = "serde")]
#[test]
fn serde_as_wow_opt() {
use serde_crate::{Deserialize, Serialize};
use serde_json;
#[derive(Serialize, Deserialize, PartialEq, Debug, Eq)]
#[cfg_attr(feature = "serde", serde(crate = "serde_crate"))]
struct T {
#[serde(default, with = "super::serde::as_wow::opt")]
pub amt: Option<Amount>,
#[serde(default, with = "super::serde::as_wow::opt")]
pub samt: Option<SignedAmount>,
}
let with = T {
amt: Some(Amount::from_dust(250_000_000_000)),
samt: Some(SignedAmount::from_dust(-250_000_000_000)),
};
let without = T {
amt: None,
samt: None,
};
for s in [&with, &without].iter() {
let v = serde_json::to_string(s).unwrap();
let w: T = serde_json::from_str(&v).unwrap();
assert_eq!(w, **s);
}
let t: T = serde_json::from_str("{\"amt\": \"2.5\", \"samt\": \"-2.5\"}").unwrap();
assert_eq!(t, with);
let t: T = serde_json::from_str("{}").unwrap();
assert_eq!(t, without);
let value_with: serde_json::Value =
serde_json::from_str("{\"amt\": \"2.5\", \"samt\": \"-2.5\"}").unwrap();
assert_eq!(with, serde_json::from_value(value_with).unwrap());
let value_without: serde_json::Value = serde_json::from_str("{}").unwrap();
assert_eq!(without, serde_json::from_value(value_without).unwrap());
}
}