use crate::{
args::ArgValues,
bytecode::VM,
defer_drop,
exception_private::{ExcType, RunResult, SimpleException},
resource::ResourceTracker,
value::Value,
};
pub fn normalize_bool_to_int(value: Value) -> Value {
match value {
Value::Bool(b) => Value::Int(i64::from(b)),
other => other,
}
}
pub fn builtin_round(vm: &mut VM<'_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {
let (number, ndigits) = args.get_one_two_args("round", vm.heap)?;
let number = normalize_bool_to_int(number);
defer_drop!(number, vm);
defer_drop!(ndigits, vm);
let digits: Option<i64> = match ndigits {
Some(Value::None) => None,
Some(Value::Int(n)) => Some(*n),
Some(Value::Bool(b)) => Some(i64::from(*b)),
Some(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());
}
None => None,
};
match number {
Value::Int(n) => {
if let Some(d) = digits {
if d >= 0 {
Ok(Value::Int(*n))
} else {
let exp = u32::try_from(-d).unwrap_or(u32::MAX);
let factor = 10_i64.saturating_pow(exp);
let rounded_f = bankers_round(*n as f64 / factor as f64);
let rounded = f64_to_i64(rounded_f) * factor;
Ok(Value::Int(rounded))
}
} 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
}