use core::str::FromStr;
use dashu_base::ConversionError;
use crate::{IBig, RBig, UBig};
use oxinum_core::{OxiNumError, OxiNumResult};
pub fn from_f64(x: f64) -> OxiNumResult<RBig> {
RBig::try_from(x).map_err(|_| OxiNumError::Parse(format!("non-finite f64: {x}").into()))
}
pub fn from_f32(x: f32) -> OxiNumResult<RBig> {
RBig::try_from(x).map_err(|_| OxiNumError::Parse(format!("non-finite f32: {x}").into()))
}
pub fn to_f64(x: &RBig) -> f64 {
x.to_f64().value()
}
pub fn to_f64_exact(x: &RBig) -> OxiNumResult<f64> {
match f64::try_from(x.clone()) {
Ok(v) => Ok(v),
Err(ConversionError::OutOfBounds) => Err(OxiNumError::Overflow(
format!("rational {x} exceeds f64 range").into(),
)),
Err(ConversionError::LossOfPrecision) => Err(OxiNumError::Precision(
format!("rational {x} not exactly representable as f64").into(),
)),
}
}
pub fn parse_mixed(s: &str) -> OxiNumResult<RBig> {
let trimmed = s.trim();
if trimmed.is_empty() {
return Err(OxiNumError::Parse("empty mixed-number string".into()));
}
let split_at = trimmed.find(char::is_whitespace);
let Some(idx) = split_at else {
return RBig::from_str(trimmed)
.map_err(|e| OxiNumError::Parse(format!("invalid rational: {e}").into()));
};
let int_part_str = &trimmed[..idx];
let frac_part_str = trimmed[idx..].trim_start();
if frac_part_str.is_empty() {
return Err(OxiNumError::Parse(
"missing fractional part in mixed number".into(),
));
}
if !frac_part_str.contains('/') {
return Err(OxiNumError::Parse(
format!("expected fraction after whole part, got {frac_part_str:?}").into(),
));
}
let int_part = IBig::from_str(int_part_str).map_err(|e| {
OxiNumError::Parse(format!("invalid integer part {int_part_str:?}: {e}").into())
})?;
let frac_part = RBig::from_str(frac_part_str).map_err(|e| {
OxiNumError::Parse(format!("invalid fractional part {frac_part_str:?}: {e}").into())
})?;
if frac_part.numerator() < &IBig::ZERO {
return Err(OxiNumError::Parse(
"fractional part of a mixed number must be non-negative".into(),
));
}
let neg = int_part < IBig::ZERO;
let abs_int = if neg { -&int_part } else { int_part };
let combined = RBig::from(abs_int) + frac_part;
let result = if neg { -combined } else { combined };
Ok(result)
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MixedNumber(pub RBig);
impl FromStr for MixedNumber {
type Err = OxiNumError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
parse_mixed(s).map(MixedNumber)
}
}
impl core::fmt::Display for MixedNumber {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let num = self.0.numerator();
let den_u: &UBig = self.0.denominator();
let den_i: IBig = den_u.clone().into();
if den_i == IBig::ONE {
return write!(f, "{}", num);
}
let abs_num = if num < &IBig::ZERO {
-num.clone()
} else {
num.clone()
};
if abs_num < den_i {
return write!(f, "{}", self.0);
}
let whole = &abs_num / &den_i;
let rem = abs_num - &whole * &den_i;
let sign = if num < &IBig::ZERO { "-" } else { "" };
if rem == IBig::ZERO {
write!(f, "{}{}", sign, whole)
} else {
write!(f, "{}{} {}/{}", sign, whole, rem, den_i)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_f64_half() {
let r = from_f64(0.5).expect("finite");
assert_eq!(r, RBig::from_parts(IBig::from(1), UBig::from(2u32)));
}
#[test]
fn from_f64_negative() {
let r = from_f64(-0.125).expect("finite");
assert_eq!(r, RBig::from_parts(IBig::from(-1), UBig::from(8u32)));
}
#[test]
fn from_f64_zero() {
let r = from_f64(0.0).expect("finite");
assert_eq!(r, RBig::ZERO);
}
#[test]
fn from_f64_one_tenth_is_dyadic() {
let original = 0.1_f64;
let r = from_f64(original).expect("finite");
let back = to_f64_exact(&r).expect("exact");
assert_eq!(back, original);
let den = r.denominator().clone();
let den_i: IBig = den.into();
let two = IBig::from(2);
let mut count_bits = 0u32;
let mut tmp = den_i;
while tmp > IBig::ZERO {
if (&tmp % &two) == IBig::ONE {
count_bits += 1;
}
tmp /= &two;
}
assert_eq!(count_bits, 1, "0.1 denominator must be a power of two");
}
#[test]
fn from_f64_nan_errors() {
assert!(from_f64(f64::NAN).is_err());
}
#[test]
fn from_f64_inf_errors() {
assert!(from_f64(f64::INFINITY).is_err());
assert!(from_f64(f64::NEG_INFINITY).is_err());
}
#[test]
fn from_f32_basic() {
let r = from_f32(0.5_f32).expect("finite");
assert_eq!(r, RBig::from_parts(IBig::from(1), UBig::from(2u32)));
assert!(from_f32(f32::NAN).is_err());
assert!(from_f32(f32::INFINITY).is_err());
}
#[test]
fn to_f64_third() {
let third = RBig::from_parts(IBig::from(1), UBig::from(3u32));
let v = to_f64(&third);
assert!((v - 1.0_f64 / 3.0).abs() < 1e-15);
}
#[test]
fn to_f64_exact_dyadic_ok() {
let r = RBig::from_parts(IBig::from(7), UBig::from(8u32));
assert_eq!(to_f64_exact(&r).expect("exact"), 0.875);
}
#[test]
fn to_f64_exact_third_errors() {
let r = RBig::from_parts(IBig::from(1), UBig::from(3u32));
assert!(matches!(to_f64_exact(&r), Err(OxiNumError::Precision(_))));
}
#[test]
fn f64_roundtrip_dyadic() {
for &orig in &[
0.5_f64,
-0.5,
0.25,
-0.125,
0.875,
1.0,
-1.0,
2.0_f64.powi(20),
2.0_f64.powi(-20),
std::f64::consts::PI, ] {
let r = from_f64(orig).expect("finite");
let back = to_f64_exact(&r).expect("exact roundtrip");
assert_eq!(back, orig, "roundtrip failed for {orig}");
}
}
#[test]
fn parse_mixed_basic() {
let r = parse_mixed("1 3/4").expect("ok");
assert_eq!(r, RBig::from_parts(IBig::from(7), UBig::from(4u32)));
}
#[test]
fn parse_mixed_negative() {
let r = parse_mixed("-2 1/3").expect("ok");
assert_eq!(r, RBig::from_parts(IBig::from(-7), UBig::from(3u32)));
}
#[test]
fn parse_mixed_plain_fraction() {
let r = parse_mixed("3/4").expect("ok");
assert_eq!(r, RBig::from_parts(IBig::from(3), UBig::from(4u32)));
}
#[test]
fn parse_mixed_plain_integer() {
let r = parse_mixed("5").expect("ok");
assert_eq!(r, RBig::from(5u32));
}
#[test]
fn parse_mixed_with_whitespace() {
let r = parse_mixed(" 1 3/4 ").expect("ok");
assert_eq!(r, RBig::from_parts(IBig::from(7), UBig::from(4u32)));
}
#[test]
fn parse_mixed_empty_errors() {
assert!(parse_mixed("").is_err());
assert!(parse_mixed(" ").is_err());
}
#[test]
fn parse_mixed_bad_integer_errors() {
assert!(parse_mixed("abc 1/2").is_err());
}
#[test]
fn parse_mixed_bad_fraction_errors() {
assert!(parse_mixed("1 xyz").is_err());
assert!(parse_mixed("1 2").is_err()); }
#[test]
fn parse_mixed_negative_fractional_rejected() {
assert!(parse_mixed("1 -1/2").is_err());
}
#[test]
fn mixed_number_from_str() {
let m: MixedNumber = "1 3/4".parse().expect("ok");
assert_eq!(m.0, RBig::from_parts(IBig::from(7), UBig::from(4u32)));
}
#[test]
fn mixed_number_display_proper() {
let m = MixedNumber(RBig::from_parts(IBig::from(3), UBig::from(4u32)));
assert_eq!(m.to_string(), "3/4");
}
#[test]
fn mixed_number_display_improper() {
let m = MixedNumber(RBig::from_parts(IBig::from(7), UBig::from(4u32)));
assert_eq!(m.to_string(), "1 3/4");
}
#[test]
fn mixed_number_display_negative_improper() {
let m = MixedNumber(RBig::from_parts(IBig::from(-7), UBig::from(3u32)));
assert_eq!(m.to_string(), "-2 1/3");
}
#[test]
fn mixed_number_display_integer() {
let m = MixedNumber(RBig::from(5u32));
assert_eq!(m.to_string(), "5");
}
#[test]
fn mixed_number_roundtrip_display_parse() {
let m1: MixedNumber = "1 3/4".parse().expect("ok");
let rendered = m1.to_string();
assert_eq!(rendered, "1 3/4");
let m2: MixedNumber = rendered.parse().expect("ok");
assert_eq!(m1, m2);
}
#[cfg(feature = "serde")]
#[test]
fn rbig_serde_json_roundtrip() {
let r = RBig::from_parts(IBig::from(355), UBig::from(113u32));
let json = serde_json::to_string(&r).expect("serialize");
let back: RBig = serde_json::from_str(&json).expect("deserialize");
assert_eq!(r, back);
}
#[cfg(feature = "serde")]
#[test]
fn rbig_serde_json_roundtrip_negative() {
let r = RBig::from_parts(IBig::from(-7), UBig::from(3u32));
let json = serde_json::to_string(&r).expect("serialize");
let back: RBig = serde_json::from_str(&json).expect("deserialize");
assert_eq!(r, back);
}
#[cfg(feature = "serde")]
#[test]
fn mixed_number_serde_json_roundtrip() {
let m = MixedNumber(RBig::from_parts(IBig::from(7), UBig::from(4u32)));
let json = serde_json::to_string(&m).expect("serialize");
let back: MixedNumber = serde_json::from_str(&json).expect("deserialize");
assert_eq!(m, back);
}
}