use std::str::FromStr as _;
use bigdecimal::BigDecimal;
use crate::{
error::{EvalError, EvalResult, InterpreterError},
value::{DecimalKind, Value},
};
#[must_use]
pub fn abs_decimal(d: &BigDecimal, kind: DecimalKind) -> Value {
match kind {
DecimalKind::Nan => Value::Decimal(Box::new(BigDecimal::from(0)), DecimalKind::Nan),
DecimalKind::PosInf | DecimalKind::NegInf => {
Value::Decimal(Box::new(BigDecimal::from(0)), DecimalKind::PosInf)
}
_ => Value::Decimal(Box::new(d.abs()), DecimalKind::Normal),
}
}
#[must_use]
pub fn neg_decimal(d: &BigDecimal, kind: DecimalKind) -> Value {
match kind {
DecimalKind::Nan => Value::Decimal(Box::new(BigDecimal::from(0)), DecimalKind::Nan),
DecimalKind::PosInf => Value::Decimal(Box::new(BigDecimal::from(0)), DecimalKind::NegInf),
DecimalKind::NegInf => Value::Decimal(Box::new(BigDecimal::from(0)), DecimalKind::PosInf),
_ => Value::Decimal(Box::new(-(d.clone())), DecimalKind::Normal),
}
}
const fn abs_kind(kind: DecimalKind) -> DecimalKind {
match kind {
DecimalKind::PosInf | DecimalKind::NegInf => DecimalKind::PosInf,
other => other,
}
}
const fn neg_kind(kind: DecimalKind) -> DecimalKind {
match kind {
DecimalKind::PosInf => DecimalKind::NegInf,
DecimalKind::NegInf => DecimalKind::PosInf,
other => other,
}
}
fn special_as_tuple(kind: DecimalKind) -> EvalResult {
let (sign, digits, exp): (i64, Vec<Value>, &str) = match kind {
DecimalKind::PosInf => (0, vec![Value::Int(0)], "F"),
DecimalKind::NegInf => (1, vec![Value::Int(0)], "F"),
_ => (0, Vec::new(), "n"),
};
let mut fields = std::collections::BTreeMap::new();
fields.insert("sign".to_string(), Value::Int(sign));
fields.insert("digits".to_string(), Value::Tuple(digits));
fields.insert("exponent".to_string(), Value::String(exp.into()));
Ok(Value::Instance(crate::value::InstanceValue {
class_name: "DecimalTuple".to_string(),
fields: crate::value::shared_fields(fields),
}))
}
pub(crate) fn dispatch_decimal_method(
d: &BigDecimal,
kind: DecimalKind,
method: &str,
args: &[Value],
kwargs: &indexmap::IndexMap<String, Value>,
) -> EvalResult {
use num_traits::{Signed as _, Zero as _};
match method {
"is_nan" => return Ok(Value::Bool(kind.is_nan())),
"is_qnan" => return Ok(Value::Bool(kind == DecimalKind::Nan)),
"is_snan" => return Ok(Value::Bool(false)),
"is_infinite" => return Ok(Value::Bool(kind.is_infinite())),
"is_finite" => return Ok(Value::Bool(!kind.is_special())),
"is_signed" => {
return Ok(Value::Bool(
matches!(kind, DecimalKind::NegInf | DecimalKind::NegZero) || d.is_negative(),
));
}
_ => {}
}
if kind.is_special() {
return Ok(match method {
"copy_abs" => Value::Decimal(Box::new(BigDecimal::from(0)), abs_kind(kind)),
"copy_negate" => Value::Decimal(Box::new(BigDecimal::from(0)), neg_kind(kind)),
"as_tuple" => return special_as_tuple(kind),
_ if kind.is_nan() => Value::Decimal(Box::new(BigDecimal::from(0)), DecimalKind::Nan),
_ => {
return Err(EvalError::Exception(crate::value::ExceptionValue::new(
"InvalidOperation",
format!("{method}() of a non-finite Decimal"),
)));
}
});
}
match method {
"quantize" => {
let Some(Value::Decimal(exp, _)) = args.first() else {
return Err(InterpreterError::TypeError(
"quantize() requires a Decimal argument".into(),
)
.into());
};
let rounding = match args.get(1).or_else(|| kwargs.get("rounding")) {
Some(Value::String(s)) => rounding_mode(s)?,
Some(Value::None) | None => bigdecimal::RoundingMode::HalfEven,
Some(other) => {
return Err(InterpreterError::TypeError(format!(
"quantize() rounding must be a decimal.ROUND_* constant, not '{}'",
other.type_name()
))
.into());
}
};
let scale = exp.fractional_digit_count();
Ok(Value::Decimal(Box::new(d.with_scale_round(scale, rounding)), DecimalKind::Normal))
}
"copy_abs" => Ok(Value::Decimal(Box::new(d.abs()), DecimalKind::Normal)),
"copy_negate" => Ok(Value::Decimal(Box::new(-d.clone()), DecimalKind::Normal)),
"copy_sign" => {
let Some(Value::Decimal(other, other_kind)) = args.first() else {
return Err(InterpreterError::TypeError(
"copy_sign() requires a Decimal argument".into(),
)
.into());
};
let other_neg = other.is_negative()
|| matches!(other_kind, DecimalKind::NegZero | DecimalKind::NegInf);
let magnitude = d.abs();
Ok(Value::Decimal(
Box::new(if other_neg { -magnitude } else { magnitude }),
DecimalKind::Normal,
))
}
"is_zero" => Ok(Value::Bool(d.is_zero())),
"normalize" => Ok(Value::Decimal(Box::new(d.normalized()), DecimalKind::Normal)),
"sqrt" => {
let r = d.sqrt().ok_or_else(|| {
EvalError::Exception(crate::value::ExceptionValue::new(
"InvalidOperation",
"sqrt of negative Decimal",
))
})?;
let exact = ((&r * &r) - d).is_zero();
let result = if exact {
r.with_scale((d.fractional_digit_count() + 1) / 2)
} else {
r.with_prec(28)
};
Ok(Value::Decimal(Box::new(result), DecimalKind::Normal))
}
"exp" if d.is_zero() => {
Ok(Value::Decimal(Box::new(BigDecimal::from(1)), DecimalKind::Normal))
}
"exp" => Ok(Value::Decimal(Box::new(d.exp().with_prec(28)), DecimalKind::Normal)),
"ln" => {
if power_of_ten_exponent(d) == Some(0) {
Ok(Value::Decimal(Box::new(BigDecimal::from(0)), DecimalKind::Normal))
} else {
decimal_ln_prec(d, 45)
.map(|r| Value::Decimal(Box::new(r.with_prec(28)), DecimalKind::Normal))
.ok_or_else(|| non_positive_log_error("ln"))
}
}
"log10" => {
if let Some(k) = power_of_ten_exponent(d) {
Ok(Value::Decimal(Box::new(BigDecimal::from(k)), DecimalKind::Normal))
} else {
let ten = BigDecimal::from(10);
match (decimal_ln_prec(d, 48), decimal_ln_prec(&ten, 48)) {
(Some(ln_d), Some(ln_ten)) => Ok(Value::Decimal(
Box::new((ln_d / ln_ten).with_prec(28)),
DecimalKind::Normal,
)),
_ => Err(non_positive_log_error("log10")),
}
}
}
"compare" => {
let Some(Value::Decimal(other, _)) = args.first() else {
return Err(InterpreterError::TypeError(
"compare() requires a Decimal argument".into(),
)
.into());
};
let sign = match d.cmp(other) {
std::cmp::Ordering::Less => -1,
std::cmp::Ordering::Equal => 0,
std::cmp::Ordering::Greater => 1,
};
Ok(Value::Decimal(Box::new(BigDecimal::from(sign)), DecimalKind::Normal))
}
"compare_total" => {
let Some(Value::Decimal(other, other_kind)) = args.first() else {
return Err(InterpreterError::TypeError(
"compare_total() requires a Decimal argument".into(),
)
.into());
};
let sign = compare_total_sign(d, kind, other, *other_kind);
Ok(Value::Decimal(Box::new(BigDecimal::from(sign)), DecimalKind::Normal))
}
"fma" => {
let (Some(Value::Decimal(other, _)), Some(Value::Decimal(third, _))) =
(args.first(), args.get(1))
else {
return Err(InterpreterError::TypeError(
"fma() requires two Decimal arguments".into(),
)
.into());
};
let result = d.clone() * (**other).clone() + (**third).clone();
Ok(Value::Decimal(Box::new(result), DecimalKind::Normal))
}
"remainder_near" => {
let Some(Value::Decimal(other, _)) = args.first() else {
return Err(InterpreterError::TypeError(
"remainder_near() requires a Decimal argument".into(),
)
.into());
};
if other.is_zero() {
return Err(InterpreterError::ValueError(
"remainder_near() division by zero".into(),
)
.into());
}
let n = (d.clone() / (**other).clone())
.with_scale_round(0, bigdecimal::RoundingMode::HalfEven);
let result = d.clone() - (**other).clone() * n;
Ok(Value::Decimal(Box::new(result), DecimalKind::Normal))
}
"adjusted" => {
let (mantissa, scale) = d.as_bigint_and_exponent();
if mantissa.is_zero() {
return Ok(Value::Int(0));
}
let digits = i64::try_from(mantissa.abs().to_string().len()).unwrap_or(i64::MAX);
Ok(Value::Int(digits - 1 - scale))
}
"scaleb" => {
let n = match args.first() {
Some(Value::Int(i)) => *i,
Some(Value::Decimal(dec, _)) => {
use num_traits::ToPrimitive as _;
dec.to_i64().unwrap_or(0)
}
_ => {
return Err(InterpreterError::TypeError(
"scaleb() requires an integer or Decimal argument".into(),
)
.into());
}
};
let (mantissa, scale) = d.as_bigint_and_exponent();
Ok(Value::Decimal(Box::new(BigDecimal::new(mantissa, scale - n)), DecimalKind::Normal))
}
"to_integral_value" | "to_integral" => {
let rounding = match args.first().or_else(|| kwargs.get("rounding")) {
Some(Value::String(s)) => rounding_mode(s)?,
_ => bigdecimal::RoundingMode::HalfEven,
};
Ok(Value::Decimal(Box::new(d.with_scale_round(0, rounding)), DecimalKind::Normal))
}
"as_integer_ratio" => {
let (mantissa, scale) = d.as_bigint_and_exponent();
let (num, den) = if scale >= 0 {
(mantissa, num_bigint::BigInt::from(10).pow(u32::try_from(scale).unwrap_or(0)))
} else {
(
mantissa * num_bigint::BigInt::from(10).pow(u32::try_from(-scale).unwrap_or(0)),
num_bigint::BigInt::from(1),
)
};
let ratio = num_rational::BigRational::new(num, den);
Ok(Value::Tuple(vec![
crate::value::int_from_bigint(ratio.numer().clone()),
crate::value::int_from_bigint(ratio.denom().clone()),
]))
}
"as_tuple" => {
let (mantissa, scale) = d.as_bigint_and_exponent();
let sign = i64::from(mantissa.is_negative());
let digits: Vec<Value> = mantissa
.abs()
.to_string()
.bytes()
.map(|b| Value::Int(i64::from(b - b'0')))
.collect();
let mut fields = std::collections::BTreeMap::new();
fields.insert("sign".to_string(), Value::Int(sign));
fields.insert("digits".to_string(), Value::Tuple(digits));
fields.insert("exponent".to_string(), Value::Int(-scale));
Ok(Value::Instance(crate::value::InstanceValue {
class_name: "DecimalTuple".to_string(),
fields: crate::value::shared_fields(fields),
}))
}
_ => Err(InterpreterError::AttributeError(format!(
"'Decimal' object has no attribute '{method}'"
))
.into()),
}
}
fn compare_total_sign(
a: &BigDecimal,
a_kind: DecimalKind,
b: &BigDecimal,
b_kind: DecimalKind,
) -> i64 {
use num_traits::Signed as _;
match b_kind {
DecimalKind::Nan | DecimalKind::PosInf => -1,
DecimalKind::NegInf => 1,
DecimalKind::Normal | DecimalKind::NegZero => {
let a_neg = a.is_negative() || matches!(a_kind, DecimalKind::NegZero);
let b_neg = b.is_negative() || matches!(b_kind, DecimalKind::NegZero);
if a_neg != b_neg {
return if a_neg { -1 } else { 1 };
}
match a.cmp(b) {
std::cmp::Ordering::Less => -1,
std::cmp::Ordering::Greater => 1,
std::cmp::Ordering::Equal => {
let a_exp = -a.as_bigint_and_exponent().1;
let b_exp = -b.as_bigint_and_exponent().1;
let base = match a_exp.cmp(&b_exp) {
std::cmp::Ordering::Less => -1,
std::cmp::Ordering::Greater => 1,
std::cmp::Ordering::Equal => 0,
};
if a_neg { -base } else { base }
}
}
}
}
}
fn non_positive_log_error(op: &str) -> EvalError {
EvalError::Exception(crate::value::ExceptionValue::new(
"InvalidOperation",
format!("{op} of a non-positive value"),
))
}
fn power_of_ten_exponent(d: &BigDecimal) -> Option<i64> {
let (mantissa, scale) = d.normalized().into_bigint_and_exponent();
(mantissa == num_bigint::BigInt::from(1)).then_some(-scale)
}
fn decimal_ln_prec(d: &BigDecimal, guard: u64) -> Option<BigDecimal> {
use num_traits::{FromPrimitive as _, Signed as _, ToPrimitive as _};
if !d.is_positive() {
return None;
}
let one = BigDecimal::from(1);
let mut y = BigDecimal::from_f64(d.to_f64()?.ln())?;
for _ in 0..40 {
let ey = y.exp().with_prec(guard);
let ratio = (d / &ey).with_prec(guard);
let correction = (ratio - &one).with_prec(guard);
let y_next = (&y + &correction).with_prec(guard);
if y_next == y {
break;
}
y = y_next;
}
Some(y)
}
pub const CONTEXT_CLASS: &str = "decimal.Context";
pub const LOCAL_CONTEXT_CLASS: &str = "decimal.LocalContext";
pub fn has_function(name: &str) -> bool {
matches!(name, "Decimal" | "getcontext" | "setcontext" | "localcontext" | "Context")
}
#[must_use]
pub fn type_classmethod(type_name: &str, method: &str) -> Option<&'static str> {
match (type_name, method) {
("Decimal", "from_float") => Some("from_float"),
_ => None,
}
}
pub fn call(func: &str, args: &[Value], state: &mut crate::state::InterpreterState) -> EvalResult {
match func {
"Decimal" => construct_decimal(args.first()),
"getcontext" => Ok(make_context_instance(state)),
"localcontext" => {
ensure_context_class(state);
let mut fields = std::collections::BTreeMap::new();
fields.insert("saved_prec".into(), Value::Int(state.decimal_prec));
if let Some(Value::Instance(inst)) = args.first() {
if inst.class_name == CONTEXT_CLASS {
if let Some(Value::Int(n)) = inst.fields.lock().get("prec") {
fields.insert("enter_prec".into(), Value::Int(*n));
}
}
}
fields.insert("ctx".into(), make_context_instance(state));
Ok(Value::Instance(crate::value::InstanceValue {
class_name: LOCAL_CONTEXT_CLASS.into(),
fields: crate::value::shared_fields(fields),
}))
}
"setcontext" => {
let Some(Value::Instance(inst)) = args.first() else {
return Err(InterpreterError::TypeError(
"setcontext() argument must be a Context".into(),
)
.into());
};
if inst.class_name != CONTEXT_CLASS {
return Err(InterpreterError::TypeError(
"setcontext() argument must be a Context".into(),
)
.into());
}
if let Some(Value::Int(n)) = inst.fields.lock().get("prec") {
if *n < 1 {
return Err(InterpreterError::ValueError(
"valid range for prec is [1, MAX_PREC]".into(),
)
.into());
}
state.decimal_prec = *n;
}
Ok(Value::None)
}
"from_float" => {
let Some(Value::Float(f)) = args.first() else {
return Err(InterpreterError::TypeError(
"from_float() argument must be a float".into(),
)
.into());
};
let big = BigDecimal::try_from(*f).map_err(|_| {
InterpreterError::ValueError(format!("cannot convert float {f} to Decimal"))
})?;
Ok(Value::Decimal(Box::new(big), DecimalKind::Normal))
}
_ => Err(InterpreterError::AttributeError(format!(
"module 'decimal' has no attribute '{func}'"
))
.into()),
}
}
pub(crate) fn construct_decimal(arg: Option<&Value>) -> EvalResult {
let Some(arg) = arg else {
return Err(InterpreterError::TypeError(
"Decimal() requires a value (int, str, or Decimal)".into(),
)
.into());
};
let (big, kind) = match arg {
Value::Int(i) => (BigDecimal::from(*i), DecimalKind::Normal),
Value::Bool(b) => (BigDecimal::from(i64::from(*b)), DecimalKind::Normal),
Value::String(s) => {
use num_traits::Zero as _;
let trimmed = s.trim();
let lower = trimmed.to_ascii_lowercase();
let (neg, body) = lower.strip_prefix('-').map_or_else(
|| (false, lower.strip_prefix('+').unwrap_or(lower.as_str())),
|rest| (true, rest),
);
if body == "inf" || body == "infinity" {
let kind = if neg { DecimalKind::NegInf } else { DecimalKind::PosInf };
(BigDecimal::from(0), kind)
} else if body == "nan" || body == "snan" {
(BigDecimal::from(0), DecimalKind::Nan)
} else {
let bd = BigDecimal::from_str(trimmed).map_err(|e| {
EvalError::from(InterpreterError::ValueError(format!(
"invalid Decimal literal: {s:?} ({e})"
)))
})?;
let kind = if bd.is_zero() && trimmed.starts_with('-') {
DecimalKind::NegZero
} else {
DecimalKind::Normal
};
(bd, kind)
}
}
Value::Decimal(d, k) => ((**d).clone(), *k),
Value::Float(_) => {
return Err(InterpreterError::TypeError(
"Decimal() does not accept float — use a string instead (see \
CONFORMANCE.md#decimal-float-rejection)"
.into(),
)
.into());
}
other => {
return Err(InterpreterError::TypeError(format!(
"Decimal() expects int / str / Decimal, got '{}'",
other.type_name()
))
.into());
}
};
Ok(Value::Decimal(Box::new(big), kind))
}
fn make_context_instance(state: &mut crate::state::InterpreterState) -> Value {
ensure_context_class(state);
let mut fields = std::collections::BTreeMap::new();
fields.insert("prec".into(), Value::Int(state.decimal_prec));
fields.insert("rounding".into(), Value::String("ROUND_HALF_EVEN".into()));
Value::Instance(crate::value::InstanceValue {
class_name: CONTEXT_CLASS.into(),
fields: crate::value::shared_fields(fields),
})
}
pub(crate) fn try_localcontext_method(
state: &mut crate::state::InterpreterState,
receiver: &Value,
method: &str,
_args: &[Value],
) -> Option<EvalResult> {
let Value::Instance(inst) = receiver else {
return None;
};
if inst.class_name != LOCAL_CONTEXT_CLASS {
return None;
}
match method {
"__enter__" => {
let fields = inst.fields.lock();
if let Some(Value::Int(n)) = fields.get("enter_prec") {
if *n >= 1 {
state.decimal_prec = *n;
}
}
let ctx = fields.get("ctx").cloned().unwrap_or(Value::None);
drop(fields);
if let Value::Instance(ctx_inst) = &ctx {
ctx_inst.fields.lock().insert("prec".into(), Value::Int(state.decimal_prec));
}
Some(Ok(ctx))
}
"__exit__" => {
let fields = inst.fields.lock();
if let Some(Value::Int(n)) = fields.get("saved_prec") {
state.decimal_prec = *n;
}
Some(Ok(Value::Bool(false)))
}
_ => Some(Err(InterpreterError::AttributeError(format!(
"'LocalContext' object has no attribute '{method}'"
))
.into())),
}
}
fn ensure_context_class(state: &mut crate::state::InterpreterState) {
use crate::value::ClassValue;
if !state.classes.contains_key(CONTEXT_CLASS) {
state.classes.insert(CONTEXT_CLASS.to_string(), ClassValue::new(CONTEXT_CLASS));
}
if !state.classes.contains_key(LOCAL_CONTEXT_CLASS) {
state.classes.insert(LOCAL_CONTEXT_CLASS.to_string(), ClassValue::new(LOCAL_CONTEXT_CLASS));
}
}
pub struct DecimalModule;
#[async_trait::async_trait]
impl crate::eval::modules::Module for DecimalModule {
fn name(&self) -> &'static str {
"decimal"
}
fn has_function(&self, name: &str) -> bool {
has_function(name)
}
fn constant(&self, name: &str) -> Option<Value> {
if matches!(
name,
"InvalidOperation"
| "DivisionByZero"
| "Overflow"
| "Underflow"
| "Inexact"
| "Rounded"
| "Subnormal"
| "Clamped"
| "FloatOperation"
| "DecimalException"
) {
return Some(Value::ExceptionType(name.to_string()));
}
rounding_constant(name)
}
async fn call(
&self,
state: &mut crate::state::InterpreterState,
func: &str,
args: &[Value],
kwargs: &indexmap::IndexMap<String, Value>,
_tools: &crate::tools::Tools,
) -> EvalResult {
if func == "Context" {
ensure_context_class(state);
let ctx = make_context_instance(state);
if let (Value::Instance(inst), Some(Value::Int(prec))) = (&ctx, kwargs.get("prec")) {
inst.fields.lock().insert("prec".into(), Value::Int(*prec));
}
return Ok(ctx);
}
call(func, args, state)
}
}
fn rounding_mode(name: &str) -> Result<bigdecimal::RoundingMode, EvalError> {
use bigdecimal::RoundingMode;
Ok(match name {
"ROUND_CEILING" => RoundingMode::Ceiling,
"ROUND_DOWN" => RoundingMode::Down,
"ROUND_FLOOR" => RoundingMode::Floor,
"ROUND_HALF_DOWN" => RoundingMode::HalfDown,
"ROUND_HALF_EVEN" => RoundingMode::HalfEven,
"ROUND_HALF_UP" => RoundingMode::HalfUp,
"ROUND_UP" | "ROUND_05UP" => RoundingMode::Up,
_ => {
return Err(
InterpreterError::ValueError(format!("invalid rounding mode: {name}")).into()
);
}
})
}
pub(crate) fn rounding_constant(name: &str) -> Option<Value> {
matches!(
name,
"ROUND_CEILING"
| "ROUND_DOWN"
| "ROUND_FLOOR"
| "ROUND_HALF_DOWN"
| "ROUND_HALF_EVEN"
| "ROUND_HALF_UP"
| "ROUND_UP"
| "ROUND_05UP"
)
.then(|| Value::String(name.into()))
}