dntk 3.1.0

Command line's multi-platform interactive calculator, GNU bc wrapper.
Documentation
use std::iter;

use dashu::base::{Abs, Approximation, Sign, UnsignedAbs};
use dashu::Decimal;
use num_traits::{ToPrimitive, Zero};

use super::error::BcError;

impl super::BcExecuter {
    pub(super) fn show_limits(&self) -> String {
        format!(
            "BC_BASE_MAX     = {}\n\
             BC_DIM_MAX      = {}\n\
             BC_SCALE_MAX    = {}\n\
             BC_STRING_MAX   = {}\n\
             MAX Exponent    = {}\n\
             Number of vars  = {}",
            u32::MAX,
            65535,
            i32::MAX,
            i32::MAX,
            1024,
            i32::MAX
        )
    }

    pub(super) fn format_result(&self, value: Decimal) -> String {
        if self.runtime.obase() == 10 {
            return self.format_result_decimal(&value);
        }
        if let Some(formatted) = self.format_result_obase(&value) {
            formatted
        } else {
            self.format_result_decimal(&value)
        }
    }

    pub(super) fn decimal_from_f64(&self, value: f64, err: &str) -> Result<Decimal, BcError> {
        let decimal = Self::decimal_from_f64_static(value, err)?;
        Ok(self.promote_precision(decimal))
    }

    pub(super) fn decimal_from_f64_static(value: f64, err: &str) -> Result<Decimal, BcError> {
        value
            .to_string()
            .parse::<Decimal>()
            .map_err(|_| BcError::Error(err.to_string()))
    }

    pub(super) fn promote_precision(&self, value: Decimal) -> Decimal {
        const PRECISION_PADDING: usize = 4;
        let digits = value.repr().digits().max(1);
        let target = digits
            .saturating_mul(2)
            .saturating_add(self.runtime.scale() as usize)
            .saturating_add(PRECISION_PADDING)
            .max(1);
        match value.with_precision(target) {
            Approximation::Exact(v) | Approximation::Inexact(v, _) => v,
        }
    }

    pub(crate) fn truncate_decimal_to_scale(value: &Decimal, scale: u32) -> Decimal {
        if scale == 0 {
            return value.trunc();
        }

        let factor = Decimal::from(10).powi(scale.into());
        let truncated = (value.clone() * &factor).trunc();
        truncated / factor
    }

    pub(super) fn decimal_to_plain_string(value: &Decimal) -> String {
        let repr = value.repr();
        if repr.significand().is_zero() {
            return "0".to_string();
        }

        let mut digits = repr.significand().unsigned_abs().to_string();
        let exponent = repr.exponent();

        if exponent >= 0 {
            digits.extend(iter::repeat_n('0', exponent as usize));
        } else {
            let shift = (-exponent) as usize;
            if digits.len() <= shift {
                let mut buffer = String::from("0.");
                buffer.extend(iter::repeat_n('0', shift - digits.len()));
                buffer.push_str(&digits);
                digits = buffer;
            } else {
                let split = digits.len() - shift;
                let (int_part, frac_part) = digits.split_at(split);
                let mut buffer = int_part.to_string();
                buffer.push('.');
                buffer.push_str(frac_part);
                digits = buffer;
            }
        }

        if repr.sign() == Sign::Negative {
            format!("-{digits}")
        } else {
            digits
        }
    }

    pub(crate) fn format_result_decimal(&self, value: &Decimal) -> String {
        let scale = self.runtime.scale();
        let truncated = Self::truncate_decimal_to_scale(value, scale);
        let mut formatted = Self::decimal_to_plain_string(&truncated);

        if let Some(point_index) = formatted.find('.') {
            if scale == 0 {
                formatted.truncate(point_index);
            } else {
                let frac_len = formatted.len() - point_index - 1;
                if frac_len < scale as usize {
                    formatted.extend(iter::repeat_n('0', scale as usize - frac_len));
                }
            }
        }

        if formatted.starts_with("0.") {
            formatted = formatted.trim_start_matches('0').to_string();
        } else if formatted.starts_with("-0.") {
            formatted = format!("-{}", formatted.trim_start_matches("-0"));
        }

        if formatted.is_empty() || formatted == "." || formatted == "-" {
            "0".to_string()
        } else {
            formatted
        }
    }

    fn format_result_obase(&self, value: &Decimal) -> Option<String> {
        const DIGITS: &[u8; 36] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        let base = self.runtime.obase() as i32;
        let negative = value.sign() == Sign::Negative;
        let abs_value = value.clone().abs();
        let integer_part = abs_value.trunc();
        let mut integer = ToPrimitive::to_i128(&integer_part)?;

        let mut int_buf = Vec::new();
        if integer == 0 {
            int_buf.push('0');
        } else {
            while integer > 0 {
                let digit = (integer % base as i128) as usize;
                int_buf.push(DIGITS[digit] as char);
                integer /= base as i128;
            }
            int_buf.reverse();
        }

        let mut result = String::new();
        if negative && (!int_buf.is_empty() || !abs_value.fract().is_zero()) {
            result.push('-');
        }
        for ch in int_buf {
            result.push(ch);
        }

        let mut fraction = abs_value - integer_part;
        let scale = self.runtime.scale();
        if !fraction.is_zero() && scale > 0 {
            result.push('.');
            let base_decimal = Decimal::from(base as i64);
            let mut digits_written = 0;
            while digits_written < scale {
                fraction *= base_decimal.clone();
                let digit_dec = fraction.trunc();
                let digit = ToPrimitive::to_u32(&digit_dec)? as usize;
                result.push(DIGITS[digit] as char);
                fraction -= digit_dec;
                digits_written += 1;
                if fraction.is_zero() {
                    break;
                }
            }
            while result.ends_with('0') {
                result.pop();
            }
            if result.ends_with('.') {
                result.pop();
            }
        }

        if result.is_empty() || result == "-" {
            result.push('0');
        }

        Some(result)
    }
}