use num_bigint::BigInt;
use crate::{
args::{ArgValues, FromArgs, is_long_int},
bytecode::VM,
defer_drop,
exception_private::{ExcType, RunResult, SimpleException},
types::LongInt,
value::Value,
};
pub fn normalize_bool_to_int(value: Value) -> Value {
match value {
Value::Bool(b) => Value::Int(i64::from(b)),
other => other,
}
}
#[derive(FromArgs)]
#[from_args(name = "round", style = c_named, at_most_total)]
struct RoundArgs {
number: Value,
#[from_args(default = Value::None)]
ndigits: Value,
}
pub fn builtin_round(vm: &mut VM<'_>, args: ArgValues) -> RunResult<Value> {
let RoundArgs { number, ndigits } = RoundArgs::from_args(args, vm)?;
let number = normalize_bool_to_int(number);
defer_drop!(number, vm);
defer_drop!(ndigits, vm);
let digits: Option<i64> = match ndigits {
Value::None => None,
Value::Int(n) => Some(*n),
Value::Bool(b) => Some(i64::from(*b)),
v if is_long_int(v, vm) => Some(if v.long_int_is_negative(vm) { i64::MIN } else { i64::MAX }),
v => {
let type_name = v.py_type_name(vm);
return Err(SimpleException::new_msg(
ExcType::TypeError,
format!("'{type_name}' object cannot be interpreted as an integer"),
)
.into());
}
};
match number {
Value::Int(n) => {
if let Some(d) = digits {
if d >= 0 {
Ok(Value::Int(*n))
} else {
let result: i128 = match u32::try_from(d.unsigned_abs()) {
Ok(exp @ ..=19) => {
let factor = 10_i128.pow(exp);
let n = i128::from(*n);
let mut q = n / factor;
let r2 = (n % factor).abs() * 2;
if r2 > factor || (r2 == factor && q % 2 != 0) {
q += if n < 0 { -1 } else { 1 };
}
q * factor
}
_ => 0,
};
Ok(match i64::try_from(result) {
Ok(i) => Value::Int(i),
Err(_) => LongInt::new(BigInt::from(result)).into_value(vm.heap),
})
}
} else {
Ok(Value::Int(*n))
}
}
Value::Float(f) => {
if let Some(d) = digits {
Ok(Value::Float(round_float_to_digits(*f, d)))
} else {
if f.is_nan() {
Err(SimpleException::new_msg(ExcType::ValueError, "cannot convert float NaN to integer").into())
} else if f.is_infinite() {
Err(
SimpleException::new_msg(ExcType::OverflowError, "cannot convert float infinity to integer")
.into(),
)
} else {
Ok(Value::Int(f64_to_i64(bankers_round(*f))))
}
}
}
_ => {
let type_name = number.py_type_name(vm);
Err(SimpleException::new_msg(
ExcType::TypeError,
format!("type {type_name} doesn't define __round__ method"),
)
.into())
}
}
}
fn bankers_round(value: f64) -> f64 {
let floor = value.floor();
let frac = value - floor;
if frac < 0.5 {
floor
} else if frac > 0.5 {
floor + 1.0
} else {
if f64_to_i64(floor) % 2 == 0 { floor } else { floor + 1.0 }
}
}
fn round_float_to_digits(value: f64, digits: i64) -> f64 {
if !value.is_finite() {
return value;
}
let rounded = if digits >= 0 {
let Ok(exp) = i32::try_from(digits) else {
return value;
};
let multiplier = 10_f64.powi(exp);
if !multiplier.is_finite() {
return value;
}
let scaled = value * multiplier;
if !scaled.is_finite() {
return value;
}
bankers_round(scaled) / multiplier
} else {
let Ok(exp) = i32::try_from(digits) else {
return 0.0_f64.copysign(value);
};
let multiplier = 10_f64.powi(exp);
if multiplier == 0.0 {
return 0.0_f64.copysign(value);
}
let scaled = value * multiplier;
bankers_round(scaled) / multiplier
};
if rounded == 0.0 {
0.0_f64.copysign(value)
} else {
rounded
}
}
fn f64_to_i64(value: f64) -> i64 {
#[expect(
clippy::cast_possible_truncation,
reason = "intentional truncation; float-to-int casts saturate and map NaN to 0"
)]
let result = value as i64;
result
}