use crate::error::{Error, InvalidInputKind};
use std::fmt::{Display, Formatter};
use std::marker::PhantomData;
mod arithmetic;
pub mod error;
mod macros;
pub mod math;
pub mod round;
#[cfg(feature = "serde")]
pub mod serde;
pub trait ScaleMetrics {
const SCALE: u8;
const SCALE_FACTOR: u64;
const REQUIRED_BUFFER_LEN: usize;
}
gen_scale!(U0, 0, 20);
gen_scale!(U1, 1, 21);
gen_scale!(U2, 2, 21);
gen_scale!(U3, 3, 21);
gen_scale!(U4, 4, 21);
gen_scale!(U5, 5, 21);
gen_scale!(U6, 6, 21);
gen_scale!(U7, 7, 21);
gen_scale!(U8, 8, 21);
const SCALE_FACTORS: [u64; 9] = [1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000];
const POW5_U128: [u128; 9] = [1, 5, 25, 125, 625, 3125, 15625, 78125, 390625];
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[repr(transparent)]
pub struct DecimalU64<S>(pub u64, PhantomData<S>);
impl<S: ScaleMetrics> Display for DecimalU64<S> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut buf = [0u8; 64];
let len = self.write_to(&mut buf);
let s = unsafe { std::str::from_utf8_unchecked(&buf[..len]) };
f.write_str(s)
}
}
impl<S: ScaleMetrics> DecimalU64<S> {
#[inline]
pub const fn new(unscaled: u64) -> Self {
Self(unscaled, PhantomData)
}
pub const ZERO: Self = DecimalU64::new(0);
pub const ONE: Self = DecimalU64::new(S::SCALE_FACTOR);
pub const TWO: Self = DecimalU64::new(2 * S::SCALE_FACTOR);
pub const THREE: Self = DecimalU64::new(3 * S::SCALE_FACTOR);
pub const FOUR: Self = DecimalU64::new(4 * S::SCALE_FACTOR);
pub const FIVE: Self = DecimalU64::new(5 * S::SCALE_FACTOR);
pub const SIX: Self = DecimalU64::new(6 * S::SCALE_FACTOR);
pub const SEVEN: Self = DecimalU64::new(7 * S::SCALE_FACTOR);
pub const EIGHT: Self = DecimalU64::new(8 * S::SCALE_FACTOR);
pub const NINE: Self = DecimalU64::new(9 * S::SCALE_FACTOR);
pub const TEN: Self = DecimalU64::new(10 * S::SCALE_FACTOR);
pub const MAX: Self = DecimalU64::new(u64::MAX);
#[inline]
pub const fn from_slice(bytes: &[u8]) -> Result<Self, Error> {
let mut unscaled: u64 = 0;
let mut fractional_part_flag: u8 = 0;
let mut scale_counter: u8 = 0;
let mut index: usize = 0;
while index < bytes.len() {
let byte = bytes[index];
match byte {
b'0'..=b'9' => {
let next = match unscaled.checked_mul(10) {
Some(value) => value,
None => return Err(Error::Overflow),
};
let digit = (byte - b'0') as u64;
unscaled = match next.checked_add(digit) {
Some(value) => value,
None => return Err(Error::Overflow),
};
scale_counter += fractional_part_flag;
}
b'.' => fractional_part_flag = 1,
other => return Err(Error::InvalidInput(InvalidInputKind::InvalidCharacter(other as char))),
}
index += 1;
}
let remaining_scale = match S::SCALE.checked_sub(scale_counter) {
Some(remaining_scale) => remaining_scale,
None => return Err(Error::Overflow),
};
let factor = SCALE_FACTORS[remaining_scale as usize];
let unscaled = match unscaled.checked_mul(factor) {
Some(unscaled) => unscaled,
None => return Err(Error::Overflow),
};
Ok(Self(unscaled, PhantomData))
}
pub const fn from_str(s: &str) -> Result<Self, Error> {
Self::from_slice(s.as_bytes())
}
pub const fn to_f64(self) -> f64 {
self.0 as f64 / S::SCALE_FACTOR as f64
}
pub const fn from_f64(value: f64) -> Result<Self, Error> {
const EXP_BITS: u64 = 0x7ff;
const EXP_BIAS: i32 = 1023;
const MANTISSA_BITS: u32 = 52;
const MANTISSA_MASK: u64 = (1u64 << MANTISSA_BITS) - 1;
let bits = value.to_bits();
let sign = bits >> 63;
let exp_bits = ((bits >> MANTISSA_BITS) & EXP_BITS) as u16;
let frac_bits = bits & MANTISSA_MASK;
if exp_bits == EXP_BITS as u16 {
return Err(Error::InvalidInput(InvalidInputKind::InfiniteNumber));
}
if sign == 1 && (exp_bits != 0 || frac_bits != 0) {
return Err(Error::InvalidInput(InvalidInputKind::NegativeNumber));
}
if exp_bits == 0 && frac_bits == 0 {
return Ok(Self::ZERO);
}
let (mantissa, exp2) = if exp_bits == 0 {
(frac_bits as u128, 1 - EXP_BIAS - MANTISSA_BITS as i32)
} else {
let mantissa = ((1u64 << MANTISSA_BITS) | frac_bits) as u128;
(mantissa, exp_bits as i32 - EXP_BIAS - MANTISSA_BITS as i32)
};
let base = mantissa * POW5_U128[S::SCALE as usize];
let exp2 = exp2 + S::SCALE as i32;
if exp2 >= 0 {
let shift = exp2 as u32;
if shift >= 128 {
return Err(Error::Overflow);
}
if base > (u64::MAX as u128 >> shift) {
return Err(Error::Overflow);
}
let unscaled = base << shift;
Ok(DecimalU64::new(unscaled as u64))
} else {
let shift = (-exp2) as u32;
if shift >= 128 {
return Ok(Self::ZERO);
}
let denom = 1u128 << shift;
let mut unscaled = base / denom;
let remainder = base % denom;
if remainder != 0 && (remainder << 1) >= denom {
if unscaled == u64::MAX as u128 {
return Err(Error::Overflow);
}
unscaled += 1;
}
if unscaled > u64::MAX as u128 {
return Err(Error::Overflow);
}
Ok(DecimalU64::new(unscaled as u64))
}
}
pub const fn rescale<T: ScaleMetrics>(&self) -> Result<DecimalU64<T>, self::Error> {
if T::SCALE >= S::SCALE {
let factor = match 10u64.checked_pow((T::SCALE - S::SCALE) as u32) {
Some(value) => value,
None => return Err(Error::Overflow),
};
let unscaled = match self.0.checked_mul(factor) {
Some(value) => value,
None => return Err(Error::Overflow),
};
Ok(DecimalU64::<T>::new(unscaled))
} else {
let factor = match 10u64.checked_pow((S::SCALE - T::SCALE) as u32) {
Some(value) => value,
None => return Err(Error::Overflow),
};
let truncated = self.0 / factor;
let remainder = self.0 % factor;
let mut rounded = truncated;
if remainder != 0 {
let double = (remainder as u128) * 2;
if double >= factor as u128 {
rounded = match truncated.checked_add(1) {
Some(value) => value,
None => return Err(Error::Overflow),
};
}
}
Ok(DecimalU64::<T>::new(rounded))
}
}
#[inline]
pub const fn split(&self) -> (u64, u64) {
let integer_part = self.0 / S::SCALE_FACTOR;
let fractional_part = self.0 % S::SCALE_FACTOR;
(integer_part, fractional_part)
}
#[inline]
pub fn write_to(&self, buffer: &mut [u8]) -> usize {
#[cold]
#[inline(never)]
fn insufficient_buffer_len(len: usize, required: usize) -> ! {
panic!("provided buffer length {} is too small, requires at least {} bytes", len, required);
}
if S::REQUIRED_BUFFER_LEN > buffer.len() {
insufficient_buffer_len(buffer.len(), S::REQUIRED_BUFFER_LEN)
}
let (int_part, frac_part) = self.split();
let mut pos = 0;
if int_part == 0 {
unsafe {
*buffer.get_unchecked_mut(pos) = b'0';
}
pos += 1;
} else {
let mut tmp = int_part;
let mut digit_count = 0;
while tmp != 0 {
digit_count += 1;
tmp /= 10;
}
pos += digit_count;
let mut idx = pos;
tmp = int_part;
while tmp != 0 {
idx -= 1;
unsafe {
*buffer.get_unchecked_mut(idx) = b'0' + (tmp % 10) as u8;
}
tmp /= 10;
}
}
if S::SCALE > 0 {
unsafe {
*buffer.get_unchecked_mut(pos) = b'.';
}
pos += 1;
let mut divisor = 10u64.pow((S::SCALE - 1) as u32);
let mut frac = frac_part;
for _ in 0..S::SCALE {
let digit = frac / divisor;
unsafe {
*buffer.get_unchecked_mut(pos) = b'0' + (digit as u8);
}
pos += 1;
frac %= divisor;
divisor /= 10;
}
}
pos
}
pub fn write_to_trimmed(&self, buffer: &mut [u8]) -> usize {
let len = self.write_to(buffer);
if S::SCALE == 0 {
return len;
}
let mut end = len;
while end > 0 {
let byte = unsafe { *buffer.get_unchecked(end - 1) };
if byte != b'0' {
break;
}
end -= 1;
}
if end > 0 {
let byte = unsafe { *buffer.get_unchecked(end - 1) };
if byte == b'.' {
end -= 1;
}
}
end
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_not_increase_size() {
assert_eq!(std::mem::size_of::<u64>(), std::mem::size_of::<DecimalU64<U0>>());
assert_eq!(std::mem::size_of::<u64>(), std::mem::size_of::<DecimalU64<U4>>());
assert_eq!(std::mem::size_of::<u64>(), std::mem::size_of::<DecimalU64<U8>>());
}
#[test]
fn should_parse_from_bytes() -> anyhow::Result<()> {
assert_eq!(18446744073709551615, DecimalU64::<U0>::from_str("18446744073709551615")?.0);
assert_eq!(18446744073709551615, DecimalU64::<U8>::from_str("184467440737.09551615")?.0);
assert_eq!(12345000000, DecimalU64::<U8>::from_str("123.45000000")?.0);
assert_eq!(12300000000, DecimalU64::<U8>::from_str("123")?.0);
assert_eq!(12300000000, DecimalU64::<U8>::from_str("123.")?.0);
assert_eq!(12300000000, DecimalU64::<U8>::from_str("123.0")?.0);
assert_eq!(18446744073709551615, DecimalU64::<U8>::from_str("184467440737.09551615")?.0);
assert_eq!(0, DecimalU64::<U8>::from_str("0.0")?.0);
assert_eq!(0, DecimalU64::<U8>::from_str("0")?.0);
Ok(())
}
#[test]
fn should_use_target_scale() -> anyhow::Result<()> {
assert_eq!(12345600000, DecimalU64::<U8>::from_str("123.456")?.0);
assert_eq!(1234560000, DecimalU64::<U7>::from_str("123.456")?.0);
assert_eq!(123456000, DecimalU64::<U6>::from_str("123.456")?.0);
assert_eq!(12345600, DecimalU64::<U5>::from_str("123.456")?.0);
assert_eq!(1234560, DecimalU64::<U4>::from_str("123.456")?.0);
assert_eq!(123456, DecimalU64::<U3>::from_str("123.456")?.0);
assert!(DecimalU64::<U2>::from_str("123.456").is_err());
assert!(DecimalU64::<U1>::from_str("123.456").is_err());
assert!(DecimalU64::<U0>::from_str("123.456").is_err());
Ok(())
}
#[test]
fn should_split() -> anyhow::Result<()> {
assert_eq!((123, 45000000), DecimalU64::<U8>::from_str("123.45000000")?.split());
assert_eq!((0, 45000000), DecimalU64::<U8>::from_str("0.45000000")?.split());
assert_eq!((0, 0), DecimalU64::<U8>::from_str("0.0")?.split());
assert_eq!((123, 45000001), DecimalU64::<U8>::from_str("123.45000001")?.split());
assert_eq!((123, 45100000), DecimalU64::<U8>::from_str("123.451")?.split());
Ok(())
}
#[test]
fn should_compare_for_eq() -> anyhow::Result<()> {
let one = DecimalU64::<U8>::from_str("123.45000000")?;
let two = DecimalU64::<U8>::from_str("123.45000000")?;
assert_eq!(one, two);
let one = DecimalU64::<U8>::from_str("123.45000000")?;
let two = DecimalU64::<U8>::from_str("123.45000001")?;
assert_ne!(one, two);
let one = DecimalU64::<U8>::from_str("0.0")?;
let two = DecimalU64::<U8>::from_str("0.0")?;
assert_eq!(one, two);
Ok(())
}
#[test]
fn should_compare_for_ord() -> anyhow::Result<()> {
let one = DecimalU64::<U8>::from_str("123.45000001")?;
let two = DecimalU64::<U8>::from_str("123.45000000")?;
assert!(one > two);
let one = DecimalU64::<U8>::from_str("123.45000000")?;
let two = DecimalU64::<U8>::from_str("123.45000001")?;
assert!(one < two);
let one = DecimalU64::<U8>::from_str("0.0")?;
let two = DecimalU64::<U8>::from_str("0.0")?;
assert!(one >= two);
let one = DecimalU64::<U8>::from_str("0.0")?;
let two = DecimalU64::<U8>::from_str("0.0")?;
assert!(one <= two);
Ok(())
}
#[test]
fn should_err_if_number_too_large() {
let err = DecimalU64::<U8>::from_str("184467440737.09551616");
assert!(err.is_err());
if let Err(err) = err {
assert!(matches!(err, Error::Overflow));
}
}
#[test]
fn should_create_from_str() {
assert_eq!(12345000001, DecimalU64::<U8>::from_str("123.45000001").unwrap().0);
}
#[test]
fn should_error_on_from_f64_overflow() {
let err = DecimalU64::<U0>::from_f64(1e30);
assert!(matches!(err, Err(Error::Overflow)));
}
#[test]
fn should_write_to_buffer() -> anyhow::Result<()> {
let mut buf = [0u8; 1024];
let dec = DecimalU64::<U8>::from_str("123.45000001")?;
assert_eq!(12, dec.write_to(&mut buf));
assert_eq!("123.45000001", std::str::from_utf8(&buf[..12])?);
let dec = DecimalU64::<U6>::from_str("123.45")?;
assert_eq!(10, dec.write_to(&mut buf));
assert_eq!("123.450000", std::str::from_utf8(&buf[..10])?);
let dec = DecimalU64::<U0>::from_str("12345")?;
assert_eq!(5, dec.write_to(&mut buf));
assert_eq!("12345", std::str::from_utf8(&buf[..5])?);
let dec = DecimalU64::<U0>::from_str("0")?;
assert_eq!(1, dec.write_to(&mut buf));
assert_eq!("0", std::str::from_utf8(&buf[..1])?);
let dec = DecimalU64::<U8>::from_str("0")?;
assert_eq!(10, dec.write_to(&mut buf));
assert_eq!("0.00000000", std::str::from_utf8(&buf[..10])?);
Ok(())
}
#[test]
fn should_display_to_string() -> anyhow::Result<()> {
assert_eq!("123.450000", DecimalU64::<U6>::from_str("123.45")?.to_string());
assert_eq!("123.45", DecimalU64::<U2>::from_str("123.45")?.to_string());
assert_eq!("123.45000000", DecimalU64::<U8>::from_str("123.45")?.to_string());
assert_eq!("0.00000000", DecimalU64::<U8>::from_str("0")?.to_string());
assert_eq!("0", DecimalU64::<U0>::from_str("0")?.to_string());
assert_eq!("10", DecimalU64::<U0>::from_str("10")?.to_string());
Ok(())
}
#[test]
fn should_default_to_zero() {
assert_eq!("0.00000000", DecimalU64::<U8>::default().to_string());
assert_eq!("0.0000000", DecimalU64::<U7>::default().to_string());
assert_eq!("0.000000", DecimalU64::<U6>::default().to_string());
assert_eq!("0.00000", DecimalU64::<U5>::default().to_string());
assert_eq!("0.0000", DecimalU64::<U4>::default().to_string());
assert_eq!("0.000", DecimalU64::<U3>::default().to_string());
assert_eq!("0.00", DecimalU64::<U2>::default().to_string());
assert_eq!("0.0", DecimalU64::<U1>::default().to_string());
assert_eq!("0", DecimalU64::<U0>::default().to_string());
}
#[test]
fn should_create_from_raw() {
assert_eq!("0.00000123", DecimalU64::<U8>::new(123).to_string());
assert_eq!("0.0000123", DecimalU64::<U7>::new(123).to_string());
assert_eq!("123", DecimalU64::<U0>::new(123).to_string());
}
#[test]
fn should_use_constant_zero() {
assert_eq!("0.00000000", DecimalU64::<U8>::ZERO.to_string());
assert_eq!("0.0000000", DecimalU64::<U7>::ZERO.to_string());
assert_eq!("0.000000", DecimalU64::<U6>::ZERO.to_string());
assert_eq!("0.00000", DecimalU64::<U5>::ZERO.to_string());
assert_eq!("0.0000", DecimalU64::<U4>::ZERO.to_string());
assert_eq!("0.000", DecimalU64::<U3>::ZERO.to_string());
assert_eq!("0.00", DecimalU64::<U2>::ZERO.to_string());
assert_eq!("0.0", DecimalU64::<U1>::ZERO.to_string());
assert_eq!("0", DecimalU64::<U0>::ZERO.to_string());
}
#[test]
fn should_use_constant_one() {
assert_eq!("1.00000000", DecimalU64::<U8>::ONE.to_string());
assert_eq!("1.0000000", DecimalU64::<U7>::ONE.to_string());
assert_eq!("1.000000", DecimalU64::<U6>::ONE.to_string());
assert_eq!("1.00000", DecimalU64::<U5>::ONE.to_string());
assert_eq!("1.0000", DecimalU64::<U4>::ONE.to_string());
assert_eq!("1.000", DecimalU64::<U3>::ONE.to_string());
assert_eq!("1.00", DecimalU64::<U2>::ONE.to_string());
assert_eq!("1.0", DecimalU64::<U1>::ONE.to_string());
assert_eq!("1", DecimalU64::<U0>::ONE.to_string());
}
#[test]
fn should_use_constant_three() {
assert_eq!("3.00000000", DecimalU64::<U8>::THREE.to_string());
assert_eq!("3.0000000", DecimalU64::<U7>::THREE.to_string());
assert_eq!("3.000000", DecimalU64::<U6>::THREE.to_string());
assert_eq!("3.00000", DecimalU64::<U5>::THREE.to_string());
assert_eq!("3.0000", DecimalU64::<U4>::THREE.to_string());
assert_eq!("3.000", DecimalU64::<U3>::THREE.to_string());
assert_eq!("3.00", DecimalU64::<U2>::THREE.to_string());
assert_eq!("3.0", DecimalU64::<U1>::THREE.to_string());
assert_eq!("3", DecimalU64::<U0>::THREE.to_string());
}
#[test]
fn should_use_constant_max() {
assert_eq!("184467440737.09551615", DecimalU64::<U8>::MAX.to_string());
assert_eq!("1844674407370.9551615", DecimalU64::<U7>::MAX.to_string());
assert_eq!("18446744073709.551615", DecimalU64::<U6>::MAX.to_string());
assert_eq!("184467440737095.51615", DecimalU64::<U5>::MAX.to_string());
assert_eq!("1844674407370955.1615", DecimalU64::<U4>::MAX.to_string());
assert_eq!("18446744073709551.615", DecimalU64::<U3>::MAX.to_string());
assert_eq!("184467440737095516.15", DecimalU64::<U2>::MAX.to_string());
assert_eq!("1844674407370955161.5", DecimalU64::<U1>::MAX.to_string());
assert_eq!("18446744073709551615", DecimalU64::<U0>::MAX.to_string());
}
#[test]
fn should_write_max_to_buffer() {
fn write_max<S: ScaleMetrics>(buffer: &mut [u8], value: DecimalU64<S>) -> usize {
value.write_to(buffer)
}
let mut buffer = [0u8; 1024];
assert_eq!(21, write_max(&mut buffer, DecimalU64::<U8>::MAX));
assert_eq!(21, write_max(&mut buffer, DecimalU64::<U7>::MAX));
assert_eq!(21, write_max(&mut buffer, DecimalU64::<U6>::MAX));
assert_eq!(21, write_max(&mut buffer, DecimalU64::<U5>::MAX));
assert_eq!(21, write_max(&mut buffer, DecimalU64::<U4>::MAX));
assert_eq!(21, write_max(&mut buffer, DecimalU64::<U3>::MAX));
assert_eq!(21, write_max(&mut buffer, DecimalU64::<U2>::MAX));
assert_eq!(21, write_max(&mut buffer, DecimalU64::<U1>::MAX));
assert_eq!(20, write_max(&mut buffer, DecimalU64::<U0>::MAX));
}
#[test]
#[should_panic(expected = "provided buffer length 1 is too small, requires at least 21 bytes")]
fn should_panic_if_buffer_too_small() {
let mut buffer = [0u8; 1];
DecimalU64::<U8>::MAX.write_to(&mut buffer);
}
#[test]
fn should_write_if_buffer_is_of_exact_size() {
let mut buffer = [0u8; U8::REQUIRED_BUFFER_LEN];
DecimalU64::<U8>::MAX.write_to(&mut buffer);
}
}
#[cfg(test)]
mod rescale_tests {
use crate::error::Error;
use crate::{DecimalU64, ScaleMetrics, U0, U1, U2, U3, U4, U5, U7, U8};
use rstest_macros::rstest;
fn rescale<S1: ScaleMetrics, S2: ScaleMetrics>(s: &'static str) {
let s1 = DecimalU64::<S1>::from_str(s).unwrap();
let s2 = s1.rescale::<S2>().unwrap();
assert_eq!(
s1.to_string().trim_end_matches('0').trim_end_matches('.'),
s2.to_string().trim_end_matches('0').trim_end_matches('.')
);
}
#[rstest]
#[case("0")]
#[case("1")]
#[case("0.01")]
#[case("1.25")]
#[case("123.45")]
fn should_rescale_up(#[case] s: &'static str) {
rescale::<U2, U5>(s);
rescale::<U2, U8>(s);
rescale::<U3, U5>(s);
rescale::<U5, U8>(s);
}
#[rstest]
#[case("0")]
#[case("1")]
#[case("10")]
#[case("123")]
#[case("1.20")]
#[case("123.450")]
fn should_rescale_down(#[case] s: &'static str) {
rescale::<U8, U5>(s);
rescale::<U8, U2>(s);
rescale::<U5, U2>(s);
rescale::<U7, U4>(s);
}
#[rstest]
#[case("50", "50")]
#[case("12345", "12345")]
fn should_not_rescale_with_same_base(#[case] s: &'static str, #[case] expected: &str) {
let d = DecimalU64::<U4>::from_str(s).unwrap();
let res = d.rescale::<U4>().unwrap();
assert_eq!(res.to_string().trim_end_matches('0').trim_end_matches('.'), expected);
}
#[rstest]
#[case("12345")]
#[case("123400")]
fn should_round_trip_invariant(#[case] s: &'static str) {
let d = DecimalU64::<U2>::from_str(s).unwrap();
let up: DecimalU64<U8> = d.rescale().unwrap();
let down: DecimalU64<U2> = up.rescale().unwrap();
assert_eq!(d.to_string().trim_end_matches('0'), down.to_string().trim_end_matches('0'));
}
#[rstest]
#[case("1.234", "1.23")]
#[case("1.235", "1.24")]
#[case("1.236", "1.24")]
#[case("9.995", "10.00")]
#[case("0.004", "0.00")]
#[case("0.005", "0.01")]
fn should_round_downscale_u3_to_u2(#[case] input: &str, #[case] expected: &str) {
let d = DecimalU64::<U3>::from_str(input).unwrap();
let result: DecimalU64<U2> = d.rescale().unwrap();
assert_eq!(expected, result.to_string());
}
#[test]
fn should_round_downscale_u8_to_u0() {
let d = DecimalU64::<U8>::from_str("1.50000000").unwrap();
let result: DecimalU64<U0> = d.rescale().unwrap();
assert_eq!("2", result.to_string());
let d = DecimalU64::<U8>::from_str("1.49999999").unwrap();
let result: DecimalU64<U0> = d.rescale().unwrap();
assert_eq!("1", result.to_string());
}
#[test]
fn should_round_on_downscale() {
let d = DecimalU64::<U4>::from_str("101.2038").unwrap(); let result = d.rescale::<U2>().unwrap(); assert_eq!("101.20", result.to_string());
let d = DecimalU64::<U4>::from_str("101.2050").unwrap(); let result = d.rescale::<U2>().unwrap();
assert_eq!("101.21", result.to_string());
}
#[test]
fn should_error_on_overflow() {
let d = DecimalU64::<U0>::MAX;
let result: Result<DecimalU64<U1>, Error> = d.rescale();
assert!(result.is_err());
match result {
Err(Error::Overflow) => {}
_ => panic!("Expected Overflow error"),
}
}
#[test]
fn should_handle_upscale_overflow_boundary() {
let max_ok = DecimalU64::<U0>::new(u64::MAX / 10);
let up: DecimalU64<U1> = max_ok.rescale().unwrap();
assert_eq!(max_ok.0 * 10, up.0);
let too_big = DecimalU64::<U0>::new(u64::MAX / 10 + 1);
let result: Result<DecimalU64<U1>, Error> = too_big.rescale();
assert!(matches!(result, Err(Error::Overflow)));
}
}
#[cfg(test)]
mod f64_tests {
use crate::error::{Error, InvalidInputKind};
use crate::{DecimalU64, U2};
#[test]
fn should_convert_to_f64() {
const VALUE: f64 = DecimalU64::<U2>::new(1234).to_f64();
assert!((VALUE - 12.34).abs() < f64::EPSILON);
}
#[test]
fn should_create_from_f64() -> anyhow::Result<()> {
let dec = DecimalU64::<U2>::from_f64(12.25)?;
assert_eq!("12.25", dec.to_string());
Ok(())
}
#[test]
fn should_create_from_f64_as_const() {
const VALUE: Result<DecimalU64<U2>, Error> = DecimalU64::from_f64(12.25);
assert_eq!("12.25", VALUE.unwrap().to_string());
}
#[test]
fn should_round_from_f64_half_up() -> anyhow::Result<()> {
let dec = DecimalU64::<U2>::from_f64(0.125)?;
assert_eq!("0.13", dec.to_string());
Ok(())
}
#[test]
fn should_error_on_from_f64_infinity() {
let err = DecimalU64::<U2>::from_f64(f64::INFINITY);
assert!(matches!(err, Err(Error::InvalidInput(InvalidInputKind::InfiniteNumber))));
}
#[test]
fn should_error_on_from_f64_negative() {
let err = DecimalU64::<U2>::from_f64(-1.0);
assert!(matches!(err, Err(Error::InvalidInput(InvalidInputKind::NegativeNumber))));
}
}