use std::cmp::Ordering;
use std::fmt;
pub const MAX_PRICE_TYPE: i32 = 19;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PriceError {
PriceTypeOutOfRange(i32),
}
impl fmt::Display for PriceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::PriceTypeOutOfRange(t) => write!(
f,
"price_type {t} outside valid range [0, {MAX_PRICE_TYPE}]"
),
}
}
}
impl std::error::Error for PriceError {}
static POW10_I64: [i64; 20] = [
1,
10,
100,
1_000,
10_000,
100_000,
1_000_000,
10_000_000,
100_000_000,
1_000_000_000,
10_000_000_000,
100_000_000_000,
1_000_000_000_000,
10_000_000_000_000,
100_000_000_000_000,
1_000_000_000_000_000,
10_000_000_000_000_000,
100_000_000_000_000_000,
1_000_000_000_000_000_000,
i64::MAX,
];
static POW10_F64: [f64; 20] = [
1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16,
1e17, 1e18, 1e19,
];
#[derive(Clone, Copy, Default)]
pub struct Price {
pub value: i32,
pub price_type: i32,
}
impl Price {
pub const ZERO: Self = Self {
value: 0,
price_type: 0,
};
#[inline]
#[must_use]
pub fn new(value: i32, price_type: i32) -> Self {
Self {
value,
price_type: price_type.clamp(0, MAX_PRICE_TYPE),
}
}
#[inline]
pub fn with_value_and_type(value: i32, price_type: i32) -> Result<Self, PriceError> {
if !(0..=MAX_PRICE_TYPE).contains(&price_type) {
return Err(PriceError::PriceTypeOutOfRange(price_type));
}
Ok(Self { value, price_type })
}
#[inline]
#[must_use]
pub const fn value(&self) -> i32 {
self.value
}
#[inline]
#[must_use]
pub const fn price_type(&self) -> i32 {
self.price_type
}
#[inline]
#[must_use]
pub const fn is_unset(&self) -> bool {
self.price_type == 0
}
#[inline]
#[must_use]
pub const fn is_zero_value(&self) -> bool {
self.value == 0 && self.price_type != 0
}
#[must_use]
pub fn is_zero(&self) -> bool {
self.value == 0 || self.price_type == 0
}
#[allow(clippy::cast_sign_loss)]
#[inline]
#[must_use]
pub fn to_f64(&self) -> f64 {
if self.price_type == 0 {
return 0.0;
}
debug_assert!(
(self.price_type as usize) < POW10_F64.len(),
"price_type {} outside POW10_F64 table",
self.price_type
);
let exp = self.price_type - 10;
if exp >= 0 {
debug_assert!(
(exp as usize) < POW10_F64.len(),
"exp {exp} outside POW10_F64 table"
);
f64::from(self.value) * POW10_F64[exp as usize]
} else {
debug_assert!(
((-exp) as usize) < POW10_F64.len(),
"(-exp) {} outside POW10_F64 table",
-exp
);
f64::from(self.value) / POW10_F64[(-exp) as usize]
}
}
#[allow(clippy::cast_sign_loss, clippy::trivially_copy_pass_by_ref)]
#[inline]
fn compare(&self, other: &Self) -> Ordering {
if self.price_type == other.price_type {
return self.value.cmp(&other.value);
}
if self.price_type > other.price_type {
let exp = (self.price_type - other.price_type) as usize;
if exp > 18 {
return self.to_f64().total_cmp(&other.to_f64());
}
debug_assert!(exp < POW10_I64.len(), "exp {exp} outside POW10_I64 table");
let scaled = i64::from(self.value).checked_mul(POW10_I64[exp]);
match scaled {
Some(s) => s.cmp(&i64::from(other.value)),
None => self.to_f64().total_cmp(&other.to_f64()),
}
} else {
let exp = (other.price_type - self.price_type) as usize;
if exp > 18 {
return self.to_f64().total_cmp(&other.to_f64());
}
debug_assert!(exp < POW10_I64.len(), "exp {exp} outside POW10_I64 table");
let scaled = i64::from(other.value).checked_mul(POW10_I64[exp]);
match scaled {
Some(s) => i64::from(self.value).cmp(&s),
None => self.to_f64().total_cmp(&other.to_f64()),
}
}
}
}
impl PartialEq for Price {
fn eq(&self, other: &Self) -> bool {
self.compare(other) == Ordering::Equal
}
}
impl Eq for Price {}
impl PartialOrd for Price {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Price {
fn cmp(&self, other: &Self) -> Ordering {
self.compare(other)
}
}
impl fmt::Debug for Price {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Price({self})")
}
}
impl fmt::Display for Price {
#[allow(clippy::cast_sign_loss)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.price_type == 0 {
return write!(f, "0.0");
}
debug_assert!(
(0..=MAX_PRICE_TYPE).contains(&self.price_type),
"price_type {} outside valid range",
self.price_type
);
if self.price_type == 10 {
return write!(f, "{}.0", self.value);
}
if self.price_type > 10 {
let zeros = "0".repeat((self.price_type - 10) as usize);
return write!(f, "{}{}.0", self.value, zeros);
}
let is_neg = self.value < 0;
let abs_str = if is_neg {
format!("{}", -i64::from(self.value))
} else {
format!("{}", self.value)
};
let frac_digits = (10 - self.price_type) as usize;
let padded = if abs_str.len() <= frac_digits {
let pad = "0".repeat(frac_digits - abs_str.len() + 1);
format!("{pad}{abs_str}")
} else {
abs_str
};
let split = padded.len() - frac_digits;
let result = format!("{}.{}", &padded[..split], &padded[split..]);
if is_neg {
write!(f, "-{result}")
} else {
write!(f, "{result}")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_price_display() {
assert_eq!(Price::new(0, 0).to_string(), "0.0");
assert_eq!(Price::new(15025, 8).to_string(), "150.25");
assert_eq!(Price::new(100, 10).to_string(), "100.0");
assert_eq!(Price::new(5, 12).to_string(), "500.0");
assert_eq!(Price::new(-15025, 8).to_string(), "-150.25");
assert_eq!(Price::new(5, 7).to_string(), "0.005");
}
#[test]
fn test_price_display_i32_min_does_not_overflow() {
assert_eq!(Price::new(i32::MIN, 8).to_string(), "-21474836.48");
assert_eq!(Price::new(i32::MIN, 10).to_string(), "-2147483648.0");
assert_eq!(Price::new(i32::MIN, 7).to_string(), "-2147483.648");
}
#[test]
fn test_price_to_f64() {
let p = Price::new(15025, 8);
assert!((p.to_f64() - 150.25).abs() < 1e-10);
}
#[test]
fn test_price_comparison() {
let a = Price::new(15025, 8); let b = Price::new(15000, 8); let c = Price::new(1502500, 6); assert!(a > b);
assert_eq!(a, c);
}
#[test]
fn every_valid_price_type_renders_and_converts_finitely() {
for pt in 0..=MAX_PRICE_TYPE {
let p = Price::new(12345, pt);
let rendered = p.to_string();
assert!(
!rendered.is_empty(),
"Display must produce a non-empty string for price_type {pt}"
);
let f = p.to_f64();
assert!(
f.is_finite(),
"to_f64 must be finite for price_type {pt}; got {f}"
);
}
}
#[test]
fn with_value_and_type_enforces_range() {
for pt in 0..=MAX_PRICE_TYPE {
assert!(
Price::with_value_and_type(1, pt).is_ok(),
"price_type {pt} must be accepted"
);
}
assert!(matches!(
Price::with_value_and_type(1, -1),
Err(PriceError::PriceTypeOutOfRange(-1))
));
assert!(matches!(
Price::with_value_and_type(1, 20),
Err(PriceError::PriceTypeOutOfRange(20))
));
assert!(matches!(
Price::with_value_and_type(1, 99),
Err(PriceError::PriceTypeOutOfRange(99))
));
assert!(matches!(
Price::with_value_and_type(1, i32::MAX),
Err(PriceError::PriceTypeOutOfRange(_))
));
}
#[test]
fn is_unset_vs_is_zero_value() {
let unset = Price::new(0, 0);
assert!(unset.is_unset());
assert!(!unset.is_zero_value());
let real_zero = Price::new(0, 8);
assert!(!real_zero.is_unset());
assert!(real_zero.is_zero_value());
let nonzero = Price::new(15025, 8);
assert!(!nonzero.is_unset());
assert!(!nonzero.is_zero_value());
}
#[test]
fn new_clamps_out_of_range_price_type() {
let p = Price::new(1, 99);
assert_eq!(p.price_type, MAX_PRICE_TYPE);
let p = Price::new(1, -3);
assert_eq!(p.price_type, 0);
let _ = Price::new(1, i32::MIN);
let _ = Price::new(1, i32::MAX);
}
#[test]
fn accessors_match_fields() {
let p = Price::new(15025, 8);
assert_eq!(p.value(), p.value);
assert_eq!(p.price_type(), p.price_type);
}
}