use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
const WEBCASH_SYMBOL_BYTES: usize = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Amount {
pub wats: i64,
}
impl Amount {
pub const DECIMALS: u32 = 8;
pub const UNIT: i64 = 10_i64.pow(Self::DECIMALS);
pub const ZERO: Amount = Amount { wats: 0 };
pub const fn from_wats(wats: i64) -> Self {
Amount { wats }
}
pub const fn from_sats(wats: i64) -> Self {
Amount { wats }
}
fn parse_scientific_notation(s: &str) -> Result<Self> {
let parts: Vec<&str> = s.split(&['E', 'e'][..]).collect();
if parts.len() != 2 {
return Err(Error::amount("invalid scientific notation format"));
}
let coefficient: f64 = parts[0]
.parse()
.map_err(|_| Error::amount("invalid coefficient in scientific notation"))?;
let exponent: i32 = parts[1]
.parse()
.map_err(|_| Error::amount("invalid exponent in scientific notation"))?;
let result = if exponent >= 0 {
coefficient * 10_f64.powi(exponent)
} else {
coefficient / 10_f64.powi(-exponent)
};
Self::from_webcash(result)
}
pub fn from_webcash(webcash: f64) -> Result<Self> {
if webcash < 0.0 {
return Err(Error::amount("negative amounts not allowed"));
}
let wats = (webcash * Self::UNIT as f64).round() as i64;
if wats < 0 {
return Err(Error::amount("amount too large"));
}
Ok(Amount { wats })
}
pub fn to_decimal_string(&self) -> String {
self.to_string_with_decimals(Self::DECIMALS)
}
pub fn to_string_with_decimals(&self, decimals: u32) -> String {
if self.wats == 0 {
return "0".to_string();
}
let divisor = 10_i64.pow(decimals);
let integer_part = self.wats / divisor;
let fractional_part = (self.wats % divisor).abs();
if fractional_part == 0 {
format!("{}", integer_part)
} else {
let fractional_str = format!("{:0width$}", fractional_part, width = decimals as usize);
let trimmed = fractional_str.trim_end_matches('0');
if trimmed.is_empty() {
format!("{}", integer_part)
} else {
format!("{}.{}", integer_part, trimmed)
}
}
}
pub fn to_webcash(&self) -> f64 {
self.wats as f64 / Self::UNIT as f64
}
pub fn to_wats_string(&self) -> String {
self.wats.to_string()
}
pub fn is_valid(&self) -> bool {
self.wats >= 0
}
pub fn is_zero(&self) -> bool {
self.wats == 0
}
pub fn is_positive(&self) -> bool {
self.wats > 0
}
pub fn is_negative(&self) -> bool {
self.wats < 0
}
pub fn abs(&self) -> Self {
Amount {
wats: self.wats.abs(),
}
}
pub fn saturating_add(&self, other: &Amount) -> Amount {
Amount {
wats: self.wats.saturating_add(other.wats),
}
}
pub fn saturating_sub(&self, other: &Amount) -> Amount {
Amount {
wats: self.wats.saturating_sub(other.wats),
}
}
pub fn checked_add(&self, other: &Amount) -> Option<Amount> {
self.wats
.checked_add(other.wats)
.map(|wats| Amount { wats })
}
pub fn checked_sub(&self, other: &Amount) -> Option<Amount> {
self.wats
.checked_sub(other.wats)
.map(|wats| Amount { wats })
}
pub fn checked_mul(&self, other: i64) -> Option<Amount> {
self.wats.checked_mul(other).map(|wats| Amount { wats })
}
pub fn checked_div(&self, other: i64) -> Option<Amount> {
self.wats.checked_div(other).map(|wats| Amount { wats })
}
}
impl Default for Amount {
fn default() -> Self {
Amount::ZERO
}
}
impl fmt::Display for Amount {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_decimal_string())
}
}
impl FromStr for Amount {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
if s.is_empty() {
return Err(Error::amount("empty string"));
}
if s.contains('E') || s.contains('e') {
return Self::parse_scientific_notation(s);
}
let s = if let Some(stripped) = s.strip_prefix('e') {
stripped
} else if s.starts_with('₩') {
&s[WEBCASH_SYMBOL_BYTES..]
} else {
s
};
if s == "0" {
return Ok(Amount::ZERO);
}
let parts: Vec<&str> = s.split('.').collect();
if parts.len() > 2 {
return Err(Error::amount("too many decimal points"));
}
let integer_part = parts[0];
let fractional_part = if parts.len() == 2 { parts[1] } else { "" };
if integer_part.is_empty() && !fractional_part.is_empty() {
return Err(Error::amount("missing integer part"));
}
let mut wats = if integer_part.is_empty() {
0
} else {
integer_part
.parse::<i64>()
.map_err(|_| Error::amount("invalid integer part"))?
};
if !fractional_part.is_empty() {
if fractional_part.len() > Amount::DECIMALS as usize {
return Err(Error::amount("too many decimal places"));
}
let frac_value = fractional_part
.parse::<i64>()
.map_err(|_| Error::amount("invalid fractional part"))?;
let multiplier = 10_i64.pow(Amount::DECIMALS - fractional_part.len() as u32);
let fractional_sats = frac_value * multiplier;
wats = wats
.checked_mul(Amount::UNIT)
.and_then(|s| s.checked_add(fractional_sats))
.ok_or_else(|| Error::amount("amount too large"))?;
} else {
wats = wats
.checked_mul(Amount::UNIT)
.ok_or_else(|| Error::amount("amount too large"))?;
}
if wats < 0 {
return Err(Error::amount("negative amounts not allowed"));
}
Ok(Amount { wats })
}
}
impl std::ops::Add for Amount {
type Output = Amount;
fn add(self, other: Amount) -> Amount {
Amount {
wats: self.wats.saturating_add(other.wats),
}
}
}
impl std::ops::Sub for Amount {
type Output = Amount;
fn sub(self, other: Amount) -> Amount {
Amount {
wats: self.wats.saturating_sub(other.wats),
}
}
}
impl std::ops::Mul<i64> for Amount {
type Output = Amount;
fn mul(self, rhs: i64) -> Amount {
Amount {
wats: self.wats.saturating_mul(rhs),
}
}
}
impl std::ops::Div<i64> for Amount {
type Output = Amount;
fn div(self, rhs: i64) -> Amount {
Amount {
wats: self.wats / rhs, }
}
}
impl std::ops::AddAssign for Amount {
fn add_assign(&mut self, other: Amount) {
self.wats = self.wats.saturating_add(other.wats);
}
}
impl std::ops::SubAssign for Amount {
fn sub_assign(&mut self, other: Amount) {
self.wats = self.wats.saturating_sub(other.wats);
}
}