val 0.4.0

An arbitrary precision calculator language
Documentation
use super::*;

#[derive(Clone, Debug)]
pub(crate) struct Decimal {
  digits: String,
  negative: bool,
  point: i64,
}

impl Decimal {
  pub(crate) fn display(self, significant_digits: NonZeroUsize) -> String {
    if self.is_zero() {
      return "0".into();
    }

    let exponent = self.point - 1;

    let significant_digits = i64::try_from(significant_digits.get()).unwrap();

    if exponent < -4 || exponent >= significant_digits {
      self.scientific_string(exponent)
    } else {
      self.fixed_string()
    }
  }

  fn fixed_string(&self) -> String {
    let digits_len = i64::try_from(self.digits.len()).unwrap();

    let unsigned = match self.point {
      point if point <= 0 => {
        format!(
          "0.{}{}",
          "0".repeat(usize::try_from(-point).unwrap()),
          self.digits
        )
      }
      point if point >= digits_len => {
        format!(
          "{}{}",
          self.digits,
          "0".repeat(usize::try_from(point - digits_len).unwrap())
        )
      }
      point => {
        let (integer, fraction) =
          self.digits.split_at(usize::try_from(point).unwrap());

        format!("{integer}.{fraction}")
      }
    };

    self.with_sign(Self::trim_zeros(unsigned))
  }

  fn format_exponent(exponent: i64) -> String {
    format!(
      "{}{:02}",
      if exponent.is_negative() { '-' } else { '+' },
      exponent.abs()
    )
  }

  pub(crate) fn from_rational(number: &Rational) -> Option<Self> {
    let mut denominator = number.denom().clone();

    let (twos, fives) = (
      Self::remove_factor(&mut denominator, 2),
      Self::remove_factor(&mut denominator, 5),
    );

    if denominator != 1 {
      return None;
    }

    let places = twos.max(fives);

    let mut scaled = number.numer().clone();

    for _ in 0..places.saturating_sub(twos) {
      scaled *= 2;
    }

    for _ in 0..places.saturating_sub(fives) {
      scaled *= 5;
    }

    let negative = scaled.is_negative();

    let scaled = if negative { -scaled } else { scaled };

    let digits = scaled.to_string();

    Some(Self {
      point: i64::try_from(digits.len()).ok()? - i64::try_from(places).ok()?,
      digits,
      negative,
    })
  }

  fn is_zero(&self) -> bool {
    self.digits.bytes().all(|digit| digit == b'0')
  }

  pub(crate) fn new(digits: String, negative: bool, point: i64) -> Self {
    Self {
      digits,
      negative,
      point,
    }
  }

  fn remove_factor(number: &mut Integer, factor: u32) -> usize {
    let mut count = 0;

    while number.is_divisible_u(factor) {
      *number /= factor;
      count += 1;
    }

    count
  }

  fn scientific_string(&self, exponent: i64) -> String {
    let mantissa = if self.digits.len() == 1 {
      self.digits.clone()
    } else {
      let (integer, fraction) = self.digits.split_at(1);
      Self::trim_zeros(format!("{integer}.{fraction}"))
    };

    format!(
      "{}e{}",
      self.with_sign(mantissa),
      Self::format_exponent(exponent),
    )
  }

  fn trim_zeros(mut string: String) -> String {
    if !string.contains('.') {
      return string;
    }

    while string.ends_with('0') {
      string.pop();
    }

    if string.ends_with('.') {
      string.pop();
    }

    string
  }

  fn with_sign(&self, string: String) -> String {
    if self.negative {
      format!("-{string}")
    } else {
      string
    }
  }
}

#[cfg(test)]
mod tests {
  use {super::*, pretty_assertions::assert_eq};

  fn digits(value: usize) -> NonZeroUsize {
    NonZeroUsize::new(value).unwrap()
  }

  #[test]
  fn configured_digits_exponent_when_point_exceeds_digits() {
    assert_eq!(
      Decimal::new("1234567890".to_owned(), false, 11).display(digits(10)),
      "1.23456789e+10"
    );
  }

  #[test]
  fn configured_digits_fixed_when_exponent_within_digits() {
    assert_eq!(
      Decimal::new("1234567890".to_owned(), false, 10).display(digits(10)),
      "1234567890"
    );
  }

  #[test]
  fn display_adds_trailing_zeros() {
    assert_eq!(
      Decimal::new("123".to_owned(), false, 5).display(digits(16)),
      "12300"
    );
  }

  #[test]
  fn display_fraction_with_leading_zero() {
    assert_eq!(
      Decimal::new("123".to_owned(), false, 0).display(digits(16)),
      "0.123"
    );
  }

  #[test]
  fn display_large_fixed_boundary() {
    assert_eq!(
      Decimal::new("1".to_owned(), false, 16).display(digits(16)),
      "1000000000000000"
    );
  }

  #[test]
  fn display_large_scientific() {
    assert_eq!(
      Decimal::new("1".to_owned(), false, 17).display(digits(16)),
      "1e+16"
    );
  }

  #[test]
  fn display_positive_integer() {
    assert_eq!(
      Decimal::new("123".to_owned(), false, 3).display(digits(16)),
      "123"
    );
  }

  #[test]
  fn display_scientific_large_digits() {
    assert_eq!(
      Decimal::new("1234567890123456".to_owned(), false, 17)
        .display(digits(16)),
      "1.234567890123456e+16"
    );
  }

  #[test]
  fn display_scientific_small_digits() {
    assert_eq!(
      Decimal::new("3600216012960922".to_owned(), false, -12)
        .display(digits(16)),
      "3.600216012960922e-13"
    );
  }

  #[test]
  fn display_small_fixed_fraction() {
    assert_eq!(
      Decimal::new("123".to_owned(), false, -3).display(digits(16)),
      "0.000123"
    );
  }

  #[test]
  fn display_small_scientific_fraction() {
    assert_eq!(
      Decimal::new("123".to_owned(), false, -4).display(digits(16)),
      "1.23e-05"
    );
  }

  #[test]
  fn display_trims_fractional_zeros() {
    assert_eq!(
      Decimal::new("2300".to_owned(), false, 2).display(digits(16)),
      "23"
    );
  }

  #[test]
  fn display_with_decimal_point() {
    assert_eq!(
      Decimal::new("123".to_owned(), false, 1).display(digits(16)),
      "1.23"
    );
  }

  #[test]
  fn display_zero() {
    assert_eq!(
      Decimal::new("0".to_owned(), false, 1).display(digits(16)),
      "0"
    );
  }

  #[test]
  fn display_zero_ignores_negative_sign() {
    assert_eq!(
      Decimal::new("0".to_owned(), true, 1).display(digits(16)),
      "0"
    );
  }

  #[test]
  fn from_rational_decimal_fraction() {
    let actual = Decimal::from_rational(&Rational::from((1234, 100)))
      .map(|decimal| decimal.display(digits(16)));

    assert_eq!(actual.as_deref(), Some("12.34"));
  }

  #[test]
  fn from_rational_integer() {
    let actual = Decimal::from_rational(&Rational::from(123))
      .map(|decimal| decimal.display(digits(16)));

    assert_eq!(actual.as_deref(), Some("123"));
  }

  #[test]
  fn from_rational_negative_fraction() {
    let actual = Decimal::from_rational(&Rational::from((-1, 40)))
      .map(|decimal| decimal.display(digits(16)));

    assert_eq!(actual.as_deref(), Some("-0.025"));
  }

  #[test]
  fn from_rational_non_terminating() {
    let actual = Decimal::from_rational(&Rational::from((1, 3)))
      .map(|decimal| decimal.display(digits(16)));

    assert_eq!(actual.as_deref(), None);
  }

  #[test]
  fn from_rational_small_fraction() {
    let actual = Decimal::from_rational(&Rational::from((1, 1000)))
      .map(|decimal| decimal.display(digits(16)));

    assert_eq!(actual.as_deref(), Some("0.001"));
  }

  #[test]
  fn from_rational_small_scientific() {
    let actual = Decimal::from_rational(&Rational::from((1, 100_000)))
      .map(|decimal| decimal.display(digits(16)));

    assert_eq!(actual.as_deref(), Some("1e-05"));
  }

  #[test]
  fn from_rational_twentieth() {
    let actual = Decimal::from_rational(&Rational::from((1, 20)))
      .map(|decimal| decimal.display(digits(16)));

    assert_eq!(actual.as_deref(), Some("0.05"));
  }
}