use std::str::FromStr as _;
use num_bigint::BigInt;
use num_rational::BigRational;
use crate::{
error::{EvalError, EvalResult, InterpreterError},
value::Value,
};
pub fn has_function(name: &str) -> bool {
matches!(name, "Fraction")
}
#[must_use]
pub fn type_classmethod(type_name: &str, method: &str) -> Option<&'static str> {
match (type_name, method) {
("Fraction", "from_float") => Some("from_float"),
("Fraction", "from_decimal") => Some("from_decimal"),
_ => None,
}
}
pub fn call(func: &str, args: &[Value]) -> EvalResult {
match func {
"Fraction" => construct_fraction(args),
"from_float" => match args.first() {
Some(Value::Float(_)) => from_single(&args[0]),
Some(other) => Err(InterpreterError::TypeError(format!(
"Fraction.from_float() only takes floats, not {} ({})",
other,
other.type_name()
))
.into()),
None => {
Err(InterpreterError::TypeError("from_float() missing required argument".into())
.into())
}
},
"from_decimal" => match args.first() {
Some(Value::Decimal(..)) => from_single(&args[0]),
Some(other) => Err(InterpreterError::TypeError(format!(
"Fraction.from_decimal() only takes Decimals, not {} ({})",
other,
other.type_name()
))
.into()),
None => {
Err(InterpreterError::TypeError("from_decimal() missing required argument".into())
.into())
}
},
_ => Err(InterpreterError::AttributeError(format!(
"module 'fractions' has no attribute '{func}'"
))
.into()),
}
}
pub(crate) fn construct_fraction(args: &[Value]) -> EvalResult {
match args {
[] => Ok(Value::Fraction(Box::new(BigRational::from_integer(BigInt::from(0))))),
[arg] => from_single(arg),
[numer, denom] => from_pair(numer, denom),
_ => Err(InterpreterError::TypeError(format!(
"Fraction() takes at most 2 arguments ({} given)",
args.len()
))
.into()),
}
}
fn from_single(arg: &Value) -> EvalResult {
let rational = match arg {
Value::Int(i) => BigRational::from_integer(BigInt::from(*i)),
Value::BigInt(b) => BigRational::from_integer((**b).clone()),
Value::Bool(b) => BigRational::from_integer(BigInt::from(i64::from(*b))),
Value::Fraction(f) => (**f).clone(),
Value::String(s) => parse_fraction_str(s)?,
Value::Float(f) => {
BigRational::from_float(*f).ok_or_else(|| {
EvalError::from(InterpreterError::ValueError(format!(
"cannot convert float {f} to Fraction"
)))
})?
}
Value::Decimal(d, _) => {
let (mantissa, scale) = d.as_bigint_and_exponent();
let ten = BigInt::from(10);
if scale >= 0 {
BigRational::new(mantissa, ten.pow(u32::try_from(scale).unwrap_or(0)))
} else {
BigRational::from_integer(mantissa * ten.pow(u32::try_from(-scale).unwrap_or(0)))
}
}
other => {
return Err(InterpreterError::TypeError(format!(
"Fraction() expects int / str / Fraction, got '{}'",
other.type_name()
))
.into());
}
};
Ok(Value::Fraction(Box::new(rational)))
}
fn from_pair(numer: &Value, denom: &Value) -> EvalResult {
let n = value_to_bigint(numer, "numerator")?;
let d = value_to_bigint(denom, "denominator")?;
if d.sign() == num_bigint::Sign::NoSign {
return Err(crate::value::ExceptionValue::zero_division_error(
"Fraction(_, 0): denominator is zero",
)
.into());
}
Ok(Value::Fraction(Box::new(BigRational::new(n, d))))
}
fn value_to_bigint(value: &Value, field: &str) -> Result<BigInt, EvalError> {
crate::value::value_as_bigint(value).ok_or_else(|| {
InterpreterError::TypeError(format!(
"Fraction() {field} expects int, got '{}'",
value.type_name()
))
.into()
})
}
fn parse_fraction_str(s: &str) -> Result<BigRational, EvalError> {
let trimmed = s.trim();
let invalid = || {
EvalError::from(InterpreterError::ValueError(format!(
"Invalid literal for Fraction: {s:?}"
)))
};
if let Some((n_str, d_str)) = trimmed.split_once('/') {
let n = parse_int_literal(n_str.trim(), true).ok_or_else(invalid)?;
let d = parse_int_literal(d_str.trim(), false).ok_or_else(invalid)?;
if d.sign() == num_bigint::Sign::NoSign {
return Err(crate::value::ExceptionValue::zero_division_error(
"Fraction(_, 0): denominator is zero",
)
.into());
}
Ok(BigRational::new(n, d))
} else {
parse_decimal_to_rational(trimmed).ok_or_else(invalid)
}
}
fn parse_int_literal(s: &str, allow_sign: bool) -> Option<BigInt> {
let cleaned = s.replace('_', "");
let (neg, digits) = if let Some(rest) = cleaned.strip_prefix('-') {
allow_sign.then_some((true, rest))?
} else if let Some(rest) = cleaned.strip_prefix('+') {
allow_sign.then_some((false, rest))?
} else {
(false, cleaned.as_str())
};
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let n = BigInt::from_str(digits).ok()?;
Some(if neg { -n } else { n })
}
fn parse_decimal_to_rational(s: &str) -> Option<BigRational> {
let s = s.trim().replace('_', "");
if s.is_empty() {
return None;
}
let (mantissa, exp): (&str, i64) = match s.split_once(['e', 'E']) {
Some((m, e)) => (m, e.parse().ok()?),
None => (s.as_str(), 0),
};
let (negative, mantissa) = match mantissa.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, mantissa.strip_prefix('+').unwrap_or(mantissa)),
};
let (int_str, frac_str) = mantissa.split_once('.').unwrap_or((mantissa, ""));
if int_str.is_empty() && frac_str.is_empty() {
return None;
}
if !int_str.bytes().all(|b| b.is_ascii_digit()) || !frac_str.bytes().all(|b| b.is_ascii_digit())
{
return None;
}
let mut numer = BigInt::from_str(&format!("0{int_str}{frac_str}")).ok()?;
if negative {
numer = -numer;
}
let net_exp = exp - i64::try_from(frac_str.len()).ok()?;
let pow10 = |k: u32| BigInt::from(10).pow(k);
Some(if net_exp >= 0 {
BigRational::from_integer(numer * pow10(u32::try_from(net_exp).ok()?))
} else {
BigRational::new(numer, pow10(u32::try_from(-net_exp).ok()?))
})
}
pub struct FractionsModule;
#[async_trait::async_trait]
impl crate::eval::modules::Module for FractionsModule {
fn name(&self) -> &'static str {
"fractions"
}
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)
}
}