#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::{
convert::TryFrom,
fmt,
ops::{Add, Sub},
str::FromStr,
};
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct FixedPrice<const W: u32, const F: u32>(i64);
pub type Price = FixedPrice<10, 8>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParsePriceError {
InvalidFormat,
Overflow,
}
impl fmt::Display for ParsePriceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParsePriceError::InvalidFormat => write!(f, "invalid price format"),
ParsePriceError::Overflow => write!(f, "price overflow"),
}
}
}
impl std::error::Error for ParsePriceError {}
impl<const W: u32, const F: u32> FixedPrice<W, F> {
const SCALE: i64 = {
assert!(
W + F <= 18,
"FixedPrice<W, F>: W + F must be <= 18 to fit in i64"
);
10i64.pow(F)
};
const MAX_WHOLE: i64 = 10i64.pow(W).saturating_sub(1);
const MAX_ABS: i64 = Self::MAX_WHOLE
.checked_mul(Self::SCALE)
.unwrap()
.checked_add(Self::SCALE - 1)
.unwrap();
pub fn from_raw(raw: i64) -> Self {
FixedPrice(raw)
}
pub fn raw(&self) -> i64 {
self.0
}
pub fn new(value: f64) -> Self {
let raw = (value * Self::SCALE as f64).round() as i64;
FixedPrice(raw)
}
pub fn to_f64(self) -> f64 {
self.0 as f64 / Self::SCALE as f64
}
pub fn floor(self, step: Self) -> Self {
let s = step.0;
assert!(s > 0, "step must be positive");
let q = self.0.div_euclid(s);
FixedPrice(q * s)
}
pub fn ceil(self, step: Self) -> Self {
let s = step.0;
assert!(s > 0, "step must be positive");
let rem = self.0.rem_euclid(s);
let q = self.0.div_euclid(s) + if rem != 0 { 1 } else { 0 };
FixedPrice(q * s)
}
}
impl<const W: u32, const F: u32> FromStr for FixedPrice<W, F> {
type Err = ParsePriceError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
if s.is_empty() {
return Err(ParsePriceError::InvalidFormat);
}
let (sign, rest): (i64, &str) = if let Some(r) = s.strip_prefix('-') {
(-1i64, r)
} else if let Some(r) = s.strip_prefix('+') {
(1i64, r)
} else {
(1i64, s)
};
let (whole_str, frac_str) = match rest.find('.') {
Some(pos) => (&rest[..pos], &rest[pos + 1..]),
None => (rest, ""),
};
if whole_str.is_empty() {
return Err(ParsePriceError::InvalidFormat);
}
let whole_u: u64 = whole_str
.parse()
.map_err(|_| ParsePriceError::InvalidFormat)?;
if whole_u > FixedPrice::<W, F>::MAX_WHOLE as u64 {
return Err(ParsePriceError::Overflow);
}
let whole = whole_u as i64;
let frac_len = frac_str.len() as u32;
if frac_len > F {
return Err(ParsePriceError::Overflow);
}
let frac_value: i64 = if frac_len > 0 {
frac_str
.parse()
.map_err(|_| ParsePriceError::InvalidFormat)?
} else {
0
};
let scaled_frac = frac_value
.checked_mul(10i64.pow(F - frac_len))
.ok_or(ParsePriceError::Overflow)?;
let combined = whole
.checked_mul(FixedPrice::<W, F>::SCALE)
.and_then(|w| w.checked_add(scaled_frac))
.ok_or(ParsePriceError::Overflow)?;
let value = sign
.checked_mul(combined)
.ok_or(ParsePriceError::Overflow)?;
if value.abs() > FixedPrice::<W, F>::MAX_ABS {
return Err(ParsePriceError::Overflow);
}
Ok(FixedPrice(value))
}
}
impl<const W: u32, const F: u32> TryFrom<&str> for FixedPrice<W, F> {
type Error = ParsePriceError;
fn try_from(v: &str) -> Result<Self, Self::Error> {
FixedPrice::<W, F>::from_str(v)
}
}
impl<const W: u32, const F: u32> fmt::Display for FixedPrice<W, F> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let raw = self.0;
let negative = raw < 0;
let abs = raw.unsigned_abs();
let scale = Self::SCALE as u64;
let whole = abs / scale;
let frac = abs % scale;
if negative {
f.write_str("-")?;
}
write!(f, "{}", whole)?;
if frac != 0 {
let scale_digits = F as usize;
let mut buf = [b'0'; 18];
let mut x = frac;
for i in (0..scale_digits).rev() {
buf[i] = b'0' + (x % 10) as u8;
x /= 10;
}
let mut end = scale_digits;
while end > 0 && buf[end - 1] == b'0' {
end -= 1;
}
f.write_str(".")?;
f.write_str(std::str::from_utf8(&buf[..end]).expect("ASCII digits"))?;
}
Ok(())
}
}
impl<const W: u32, const F: u32> Add for FixedPrice<W, F> {
type Output = Self;
fn add(self, other: Self) -> Self {
let sum = self
.0
.checked_add(other.0)
.expect("overflow adding FixedPrice");
FixedPrice(sum)
}
}
impl<const W: u32, const F: u32> Sub for FixedPrice<W, F> {
type Output = Self;
fn sub(self, other: Self) -> Self {
let diff = self
.0
.checked_sub(other.0)
.expect("overflow subtracting FixedPrice");
FixedPrice(diff)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_and_raw() {
let raw: i64 = 6220000000;
let p = Price::from_raw(raw);
assert_eq!(p.raw(), raw);
assert_eq!(p.to_string(), "62.2");
}
#[test]
fn test_new_from_f64() {
let p = Price::new(62.2);
assert_eq!(p.raw(), 6220000000);
assert_eq!(p.to_string(), "62.2");
}
#[test]
fn test_parse_and_to_f64() {
let p: Price = "4.32".parse().unwrap();
assert!((p.to_f64() - 4.32).abs() < 1e-12);
}
#[test]
fn test_add_sub() {
let a: Price = "1.23".parse().unwrap();
let b: Price = "2.34".parse().unwrap();
let c = a + b;
assert!((c.to_f64() - 3.57).abs() < 1e-12);
let d = b - a;
assert!((d.to_f64() - 1.11).abs() < 1e-12);
}
#[test]
fn display_handles_negative_and_zero_fractions() {
let zero: Price = "0".parse().unwrap();
assert_eq!(zero.to_string(), "0");
let whole_only: Price = "42".parse().unwrap();
assert_eq!(whole_only.to_string(), "42");
let neg: Price = "-1.25".parse().unwrap();
assert_eq!(neg.to_string(), "-1.25");
let neg_whole: Price = "-7".parse().unwrap();
assert_eq!(neg_whole.to_string(), "-7");
let trail: Price = "1.50".parse().unwrap();
assert_eq!(trail.to_string(), "1.5");
let lead: Price = "1.005".parse().unwrap();
assert_eq!(lead.to_string(), "1.005");
}
#[test]
fn test_floor_ceil() {
let x: Price = "4.32".parse().unwrap();
let step: Price = "0.05".parse().unwrap();
assert!((x.floor(step).to_f64() - 4.30).abs() < 1e-12);
assert!((x.ceil(step).to_f64() - 4.35).abs() < 1e-12);
}
}