use std::fmt::{self, Display};
#[derive(Clone, Copy, Debug)]
pub struct FixedPoint {
value: i128,
decimals: u32,
}
impl FixedPoint {
#[must_use]
pub fn new(value: i128, decimals: u32) -> Self {
Self { value, decimals }
}
}
impl Display for FixedPoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.decimals == 0 {
return write!(f, "{}", self.value);
}
let decimals = self.decimals as usize;
let digits = self.value.unsigned_abs().to_string();
let digits = if digits.len() <= decimals {
format!("{digits:0>width$}", width = decimals + 1)
} else {
digits
};
let point = digits.len() - decimals;
let integer_part = &digits[..point];
let fractional_part = digits[point..].trim_end_matches('0');
let sign = if self.value < 0 { "-" } else { "" };
if fractional_part.is_empty() {
write!(f, "{sign}{integer_part}")
} else {
write!(f, "{sign}{integer_part}.{fractional_part}")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fmt(value: i128, decimals: u32) -> String {
FixedPoint::new(value, decimals).to_string()
}
#[test]
#[allow(clippy::unreadable_literal)]
fn formats_typical_values() {
assert_eq!(fmt(0, 7), "0");
assert_eq!(fmt(1234567, 7), "0.1234567");
assert_eq!(fmt(12345000, 7), "1.2345");
assert_eq!(fmt(10000000, 7), "1");
assert_eq!(fmt(123456789012345, 7), "12345678.9012345");
assert_eq!(fmt(1, 7), "0.0000001");
assert_eq!(fmt(12345, 0), "12345");
assert_eq!(fmt(12345, 1), "1234.5");
}
#[test]
#[allow(clippy::unreadable_literal)]
fn formats_negative_values() {
assert_eq!(fmt(-1234567, 7), "-0.1234567");
assert_eq!(fmt(-12345000, 7), "-1.2345");
assert_eq!(fmt(-10000000, 7), "-1");
assert_eq!(fmt(-5, 0), "-5");
}
#[test]
fn handles_i128_bounds_without_overflow() {
assert_eq!(
fmt(i128::MIN, 18),
"-170141183460469231731.687303715884105728"
);
assert_eq!(
fmt(i128::MAX, 18),
"170141183460469231731.687303715884105727"
);
}
#[test]
fn large_decimals_do_not_overflow() {
assert_eq!(fmt(1, 39), "0.000000000000000000000000000000000000001");
assert_eq!(fmt(123, 255).chars().take(2).collect::<String>(), "0.");
}
}