use alloc::string::String;
use alloc::vec::Vec;
pub(crate) const MAX_DIGITS: usize = 31;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Decimal {
negative: bool,
digits: Vec<u8>,
scale: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DecimalError {
NotDecimal,
DivisionByZero,
TooLarge,
}
impl Decimal {
pub(crate) fn parse(text: &str, scale: u8) -> Result<Self, DecimalError> {
let (negative, magnitude) = match text.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, text),
};
if magnitude.is_empty() || !magnitude.bytes().all(|b| b.is_ascii_digit()) {
return Err(DecimalError::NotDecimal);
}
let digits = magnitude.bytes().map(|b| b - b'0').collect();
Ok(Self {
negative,
digits,
scale: usize::from(scale),
}
.normalized())
}
pub(crate) fn to_parts(&self) -> Result<(String, u8), DecimalError> {
let scale = u8::try_from(self.scale).map_err(|_| DecimalError::TooLarge)?;
let mut text = String::with_capacity(self.digits.len() + 1);
if self.negative && !self.is_zero() {
text.push('-');
}
for _ in self.digits.len()..self.scale {
text.push('0');
}
text.extend(self.digits.iter().map(|d| char::from(b'0' + d)));
Ok((text, scale))
}
fn is_zero(&self) -> bool {
self.digits.iter().all(|d| *d == 0)
}
fn normalized(mut self) -> Self {
let first = self
.digits
.iter()
.position(|d| *d != 0)
.unwrap_or(self.digits.len().saturating_sub(1));
self.digits.drain(..first);
if self.digits.is_empty() {
self.digits.push(0);
}
if self.is_zero() {
self.negative = false;
}
self
}
fn rescaled(&self, scale: usize) -> Vec<u8> {
let mut digits = self.digits.clone();
digits.resize(digits.len() + (scale - self.scale), 0);
digits
}
pub(crate) fn negated(mut self) -> Self {
if !self.is_zero() {
self.negative = !self.negative;
}
self
}
pub(crate) fn add(&self, other: &Self) -> Result<Self, DecimalError> {
let scale = self.scale.max(other.scale);
let a = self.rescaled(scale);
let b = other.rescaled(scale);
let (negative, digits) = if self.negative == other.negative {
(self.negative, add_magnitudes(&a, &b))
} else {
match compare(&a, &b) {
core::cmp::Ordering::Less => (other.negative, subtract_magnitudes(&b, &a)),
_ => (self.negative, subtract_magnitudes(&a, &b)),
}
};
Self {
negative,
digits,
scale,
}
.normalized()
.kept()
}
pub(crate) fn subtract(&self, other: &Self) -> Result<Self, DecimalError> {
self.add(&other.clone().negated())
}
pub(crate) fn multiply(&self, other: &Self) -> Result<Self, DecimalError> {
let mut product = alloc::vec![0u32; self.digits.len() + other.digits.len()];
for (i, a) in self.digits.iter().enumerate().rev() {
for (j, b) in other.digits.iter().enumerate().rev() {
product[i + j + 1] += u32::from(*a) * u32::from(*b);
}
}
for at in (1..product.len()).rev() {
let carry = product[at] / 10;
product[at] %= 10;
product[at - 1] += carry;
}
Self {
negative: self.negative != other.negative,
digits: product.into_iter().map(|d| d as u8).collect(),
scale: self.scale + other.scale,
}
.normalized()
.kept()
}
pub(crate) fn divide(&self, other: &Self) -> Result<Self, DecimalError> {
if other.is_zero() {
return Err(DecimalError::DivisionByZero);
}
let mut dividend = self.digits.clone();
dividend.resize(dividend.len() + other.scale, 0);
let divisor = &other.digits;
let mut quotient = Vec::new();
let mut remainder: Vec<u8> = alloc::vec![0];
let mut places = 0usize;
let mut next = dividend.into_iter();
loop {
let digit = match next.next() {
Some(d) => d,
None => {
let significant = quotient.iter().skip_while(|d| **d == 0).count();
if remainder.iter().all(|d| *d == 0) || significant > MAX_DIGITS {
break;
}
places += 1;
0
}
};
remainder.push(digit);
remainder = trimmed(remainder);
let mut count = 0u8;
while compare(&remainder, divisor) != core::cmp::Ordering::Less {
remainder = trimmed(subtract_magnitudes(&remainder, divisor));
count += 1;
}
quotient.push(count);
}
Self {
negative: self.negative != other.negative,
digits: quotient,
scale: self.scale + places,
}
.normalized()
.kept()
}
fn kept(mut self) -> Result<Self, DecimalError> {
while self.scale > 0 && self.digits.len() > 1 && self.digits.last() == Some(&0) {
self.digits.pop();
self.scale -= 1;
}
if self.is_zero() {
self.scale = 0;
}
if self.digits.len() > MAX_DIGITS {
let excess = self.digits.len() - MAX_DIGITS;
if excess > self.scale {
return Err(DecimalError::TooLarge);
}
self.digits.truncate(MAX_DIGITS);
self.scale -= excess;
}
Ok(self.normalized())
}
pub(crate) fn fitted(&self, digits: u8, scale: u8) -> Option<Self> {
let scale = usize::from(scale);
let mut cut = self.clone();
if cut.scale > scale {
let drop = cut.scale - scale;
let keep = cut.digits.len().saturating_sub(drop);
cut.digits.truncate(keep);
if cut.digits.is_empty() {
cut.digits.push(0);
}
cut.scale = scale;
cut = cut.normalized();
}
let whole = cut.digits.len().saturating_sub(cut.scale);
let whole = if cut.is_zero() { 0 } else { whole };
(whole <= usize::from(digits).saturating_sub(scale)).then_some(cut)
}
}
fn compare(a: &[u8], b: &[u8]) -> core::cmp::Ordering {
let a = &a[a.iter().position(|d| *d != 0).unwrap_or(a.len())..];
let b = &b[b.iter().position(|d| *d != 0).unwrap_or(b.len())..];
a.len().cmp(&b.len()).then_with(|| a.cmp(b))
}
fn trimmed(mut digits: Vec<u8>) -> Vec<u8> {
let first = digits
.iter()
.position(|d| *d != 0)
.unwrap_or(digits.len().saturating_sub(1));
digits.drain(..first);
if digits.is_empty() {
digits.push(0);
}
digits
}
fn add_magnitudes(a: &[u8], b: &[u8]) -> Vec<u8> {
let width = a.len().max(b.len()) + 1;
let mut out = alloc::vec![0u8; width];
let mut carry = 0u8;
for at in 0..width {
let x = a.len().checked_sub(at + 1).map_or(0, |i| a[i]);
let y = b.len().checked_sub(at + 1).map_or(0, |i| b[i]);
let sum = x + y + carry;
out[width - 1 - at] = sum % 10;
carry = sum / 10;
}
out
}
fn subtract_magnitudes(a: &[u8], b: &[u8]) -> Vec<u8> {
let mut out = alloc::vec![0u8; a.len()];
let mut borrow = 0i8;
for at in 0..a.len() {
let x = i8::try_from(a[a.len() - 1 - at]).unwrap_or(0);
let y = b
.len()
.checked_sub(at + 1)
.map_or(0, |i| i8::try_from(b[i]).unwrap_or(0));
let mut difference = x - y - borrow;
borrow = 0;
if difference < 0 {
difference += 10;
borrow = 1;
}
out[a.len() - 1 - at] = u8::try_from(difference).unwrap_or(0);
}
out
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
fn d(text: &str, scale: u8) -> Decimal {
Decimal::parse(text, scale).unwrap()
}
fn parts(value: &Decimal) -> (String, u8) {
value.to_parts().unwrap()
}
#[test]
fn sums_and_differences_line_the_points_up() {
assert_eq!(
parts(&d("150", 2).add(&d("25", 1)).unwrap()),
("4".into(), 0)
);
assert_eq!(
parts(&d("1", 0).subtract(&d("15", 1)).unwrap()),
("-5".into(), 1)
);
assert_eq!(
parts(&d("-25", 1).add(&d("25", 1)).unwrap()),
("0".into(), 0)
);
}
#[test]
fn a_product_carries_both_scales() {
assert_eq!(
parts(&d("125", 2).multiply(&d("-4", 0)).unwrap()),
("-5".into(), 0)
);
assert_eq!(
parts(&d("11", 1).multiply(&d("11", 1)).unwrap()),
("121".into(), 2)
);
}
#[test]
fn a_quotient_runs_to_31_digits_and_is_cut_not_rounded() {
let (digits, scale) = parts(&d("2", 0).divide(&d("3", 0)).unwrap());
assert_eq!(digits, "6".repeat(31));
assert_eq!(scale, 31);
assert_eq!(
parts(&d("1", 0).divide(&d("8", 0)).unwrap()),
("125".into(), 3)
);
assert_eq!(
parts(&d("75", 1).divide(&d("25", 1)).unwrap()),
("3".into(), 0)
);
assert_eq!(
d("1", 0).divide(&d("0", 0)),
Err(DecimalError::DivisionByZero)
);
}
#[test]
fn more_than_31_whole_digits_is_too_large() {
let big = d(&"9".repeat(31), 0);
assert_eq!(big.add(&d("1", 0)), Err(DecimalError::TooLarge));
assert!(big.add(&d("0", 0)).is_ok());
}
#[test]
fn a_declared_type_cuts_on_the_right_and_refuses_on_the_left() {
let value = d("123456", 3); assert_eq!(parts(&value.fitted(5, 2).unwrap()), ("12345".into(), 2));
assert_eq!(value.fitted(4, 2), None);
}
}