use std::str::FromStr as _;
use bigdecimal::BigDecimal;
use crate::{
error::{EvalError, EvalResult, InterpreterError},
value::Value,
};
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")
}
#[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)))
}
_ => 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 = match arg {
Value::Int(i) => BigDecimal::from(*i),
Value::Bool(b) => BigDecimal::from(i64::from(*b)),
Value::String(s) => BigDecimal::from_str(s.trim()).map_err(|e| {
EvalError::from(InterpreterError::ValueError(format!(
"invalid Decimal literal: {s:?} ({e})"
)))
})?,
Value::Decimal(d) => (**d).clone(),
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)))
}
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)
}
async fn call(
&self,
state: &mut crate::state::InterpreterState,
func: &str,
args: &[Value],
_kwargs: &indexmap::IndexMap<String, Value>,
_tools: &crate::tools::Tools,
) -> EvalResult {
call(func, args, state)
}
}