oxinum_rational/
convert.rs1use core::str::FromStr;
20
21use dashu_base::ConversionError;
22
23use crate::{IBig, RBig, UBig};
24use oxinum_core::{OxiNumError, OxiNumResult};
25
26pub fn from_f64(x: f64) -> OxiNumResult<RBig> {
54 RBig::try_from(x).map_err(|_| OxiNumError::Parse(format!("non-finite f64: {x}").into()))
55}
56
57pub fn from_f32(x: f32) -> OxiNumResult<RBig> {
75 RBig::try_from(x).map_err(|_| OxiNumError::Parse(format!("non-finite f32: {x}").into()))
76}
77
78pub fn to_f64(x: &RBig) -> f64 {
98 x.to_f64().value()
99}
100
101pub fn to_f64_exact(x: &RBig) -> OxiNumResult<f64> {
122 match f64::try_from(x.clone()) {
123 Ok(v) => Ok(v),
124 Err(ConversionError::OutOfBounds) => Err(OxiNumError::Overflow(
125 format!("rational {x} exceeds f64 range").into(),
126 )),
127 Err(ConversionError::LossOfPrecision) => Err(OxiNumError::Precision(
128 format!("rational {x} not exactly representable as f64").into(),
129 )),
130 }
131}
132
133pub fn parse_mixed(s: &str) -> OxiNumResult<RBig> {
168 let trimmed = s.trim();
169 if trimmed.is_empty() {
170 return Err(OxiNumError::Parse("empty mixed-number string".into()));
171 }
172
173 let split_at = trimmed.find(char::is_whitespace);
176 let Some(idx) = split_at else {
177 return RBig::from_str(trimmed)
178 .map_err(|e| OxiNumError::Parse(format!("invalid rational: {e}").into()));
179 };
180
181 let int_part_str = &trimmed[..idx];
182 let frac_part_str = trimmed[idx..].trim_start();
183 if frac_part_str.is_empty() {
184 return Err(OxiNumError::Parse(
185 "missing fractional part in mixed number".into(),
186 ));
187 }
188 if !frac_part_str.contains('/') {
189 return Err(OxiNumError::Parse(
190 format!("expected fraction after whole part, got {frac_part_str:?}").into(),
191 ));
192 }
193
194 let int_part = IBig::from_str(int_part_str).map_err(|e| {
195 OxiNumError::Parse(format!("invalid integer part {int_part_str:?}: {e}").into())
196 })?;
197 let frac_part = RBig::from_str(frac_part_str).map_err(|e| {
198 OxiNumError::Parse(format!("invalid fractional part {frac_part_str:?}: {e}").into())
199 })?;
200
201 if frac_part.numerator() < &IBig::ZERO {
203 return Err(OxiNumError::Parse(
204 "fractional part of a mixed number must be non-negative".into(),
205 ));
206 }
207
208 let neg = int_part < IBig::ZERO;
212 let abs_int = if neg { -&int_part } else { int_part };
213 let combined = RBig::from(abs_int) + frac_part;
214 let result = if neg { -combined } else { combined };
215 Ok(result)
216}
217
218#[derive(Debug, Clone, PartialEq, Eq)]
235#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
236pub struct MixedNumber(pub RBig);
237
238impl FromStr for MixedNumber {
239 type Err = OxiNumError;
240
241 fn from_str(s: &str) -> Result<Self, Self::Err> {
242 parse_mixed(s).map(MixedNumber)
243 }
244}
245
246impl core::fmt::Display for MixedNumber {
247 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
248 let num = self.0.numerator();
252 let den_u: &UBig = self.0.denominator();
253 let den_i: IBig = den_u.clone().into();
254 if den_i == IBig::ONE {
255 return write!(f, "{}", num);
256 }
257 let abs_num = if num < &IBig::ZERO {
258 -num.clone()
259 } else {
260 num.clone()
261 };
262 if abs_num < den_i {
263 return write!(f, "{}", self.0);
265 }
266 let whole = &abs_num / &den_i;
267 let rem = abs_num - &whole * &den_i;
268 let sign = if num < &IBig::ZERO { "-" } else { "" };
269 if rem == IBig::ZERO {
270 write!(f, "{}{}", sign, whole)
271 } else {
272 write!(f, "{}{} {}/{}", sign, whole, rem, den_i)
273 }
274 }
275}
276
277#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn from_f64_half() {
287 let r = from_f64(0.5).expect("finite");
288 assert_eq!(r, RBig::from_parts(IBig::from(1), UBig::from(2u32)));
289 }
290
291 #[test]
292 fn from_f64_negative() {
293 let r = from_f64(-0.125).expect("finite");
294 assert_eq!(r, RBig::from_parts(IBig::from(-1), UBig::from(8u32)));
295 }
296
297 #[test]
298 fn from_f64_zero() {
299 let r = from_f64(0.0).expect("finite");
300 assert_eq!(r, RBig::ZERO);
301 }
302
303 #[test]
304 fn from_f64_one_tenth_is_dyadic() {
305 let original = 0.1_f64;
310 let r = from_f64(original).expect("finite");
311 let back = to_f64_exact(&r).expect("exact");
313 assert_eq!(back, original);
314 let den = r.denominator().clone();
316 let den_i: IBig = den.into();
317 let two = IBig::from(2);
319 let mut count_bits = 0u32;
320 let mut tmp = den_i;
321 while tmp > IBig::ZERO {
322 if (&tmp % &two) == IBig::ONE {
323 count_bits += 1;
324 }
325 tmp /= &two;
326 }
327 assert_eq!(count_bits, 1, "0.1 denominator must be a power of two");
328 }
329
330 #[test]
331 fn from_f64_nan_errors() {
332 assert!(from_f64(f64::NAN).is_err());
333 }
334
335 #[test]
336 fn from_f64_inf_errors() {
337 assert!(from_f64(f64::INFINITY).is_err());
338 assert!(from_f64(f64::NEG_INFINITY).is_err());
339 }
340
341 #[test]
342 fn from_f32_basic() {
343 let r = from_f32(0.5_f32).expect("finite");
344 assert_eq!(r, RBig::from_parts(IBig::from(1), UBig::from(2u32)));
345 assert!(from_f32(f32::NAN).is_err());
346 assert!(from_f32(f32::INFINITY).is_err());
347 }
348
349 #[test]
350 fn to_f64_third() {
351 let third = RBig::from_parts(IBig::from(1), UBig::from(3u32));
352 let v = to_f64(&third);
353 assert!((v - 1.0_f64 / 3.0).abs() < 1e-15);
354 }
355
356 #[test]
357 fn to_f64_exact_dyadic_ok() {
358 let r = RBig::from_parts(IBig::from(7), UBig::from(8u32));
359 assert_eq!(to_f64_exact(&r).expect("exact"), 0.875);
360 }
361
362 #[test]
363 fn to_f64_exact_third_errors() {
364 let r = RBig::from_parts(IBig::from(1), UBig::from(3u32));
365 assert!(matches!(to_f64_exact(&r), Err(OxiNumError::Precision(_))));
366 }
367
368 #[test]
369 fn f64_roundtrip_dyadic() {
370 for &orig in &[
372 0.5_f64,
373 -0.5,
374 0.25,
375 -0.125,
376 0.875,
377 1.0,
378 -1.0,
379 2.0_f64.powi(20),
380 2.0_f64.powi(-20),
381 std::f64::consts::PI, ] {
383 let r = from_f64(orig).expect("finite");
384 let back = to_f64_exact(&r).expect("exact roundtrip");
385 assert_eq!(back, orig, "roundtrip failed for {orig}");
386 }
387 }
388
389 #[test]
390 fn parse_mixed_basic() {
391 let r = parse_mixed("1 3/4").expect("ok");
392 assert_eq!(r, RBig::from_parts(IBig::from(7), UBig::from(4u32)));
393 }
394
395 #[test]
396 fn parse_mixed_negative() {
397 let r = parse_mixed("-2 1/3").expect("ok");
399 assert_eq!(r, RBig::from_parts(IBig::from(-7), UBig::from(3u32)));
400 }
401
402 #[test]
403 fn parse_mixed_plain_fraction() {
404 let r = parse_mixed("3/4").expect("ok");
405 assert_eq!(r, RBig::from_parts(IBig::from(3), UBig::from(4u32)));
406 }
407
408 #[test]
409 fn parse_mixed_plain_integer() {
410 let r = parse_mixed("5").expect("ok");
411 assert_eq!(r, RBig::from(5u32));
412 }
413
414 #[test]
415 fn parse_mixed_with_whitespace() {
416 let r = parse_mixed(" 1 3/4 ").expect("ok");
417 assert_eq!(r, RBig::from_parts(IBig::from(7), UBig::from(4u32)));
418 }
419
420 #[test]
421 fn parse_mixed_empty_errors() {
422 assert!(parse_mixed("").is_err());
423 assert!(parse_mixed(" ").is_err());
424 }
425
426 #[test]
427 fn parse_mixed_bad_integer_errors() {
428 assert!(parse_mixed("abc 1/2").is_err());
429 }
430
431 #[test]
432 fn parse_mixed_bad_fraction_errors() {
433 assert!(parse_mixed("1 xyz").is_err());
434 assert!(parse_mixed("1 2").is_err()); }
436
437 #[test]
438 fn parse_mixed_negative_fractional_rejected() {
439 assert!(parse_mixed("1 -1/2").is_err());
440 }
441
442 #[test]
443 fn mixed_number_from_str() {
444 let m: MixedNumber = "1 3/4".parse().expect("ok");
445 assert_eq!(m.0, RBig::from_parts(IBig::from(7), UBig::from(4u32)));
446 }
447
448 #[test]
449 fn mixed_number_display_proper() {
450 let m = MixedNumber(RBig::from_parts(IBig::from(3), UBig::from(4u32)));
451 assert_eq!(m.to_string(), "3/4");
452 }
453
454 #[test]
455 fn mixed_number_display_improper() {
456 let m = MixedNumber(RBig::from_parts(IBig::from(7), UBig::from(4u32)));
457 assert_eq!(m.to_string(), "1 3/4");
458 }
459
460 #[test]
461 fn mixed_number_display_negative_improper() {
462 let m = MixedNumber(RBig::from_parts(IBig::from(-7), UBig::from(3u32)));
463 assert_eq!(m.to_string(), "-2 1/3");
464 }
465
466 #[test]
467 fn mixed_number_display_integer() {
468 let m = MixedNumber(RBig::from(5u32));
469 assert_eq!(m.to_string(), "5");
470 }
471
472 #[test]
473 fn mixed_number_roundtrip_display_parse() {
474 let m1: MixedNumber = "1 3/4".parse().expect("ok");
476 let rendered = m1.to_string();
477 assert_eq!(rendered, "1 3/4");
478 let m2: MixedNumber = rendered.parse().expect("ok");
479 assert_eq!(m1, m2);
480 }
481
482 #[cfg(feature = "serde")]
485 #[test]
486 fn rbig_serde_json_roundtrip() {
487 let r = RBig::from_parts(IBig::from(355), UBig::from(113u32));
488 let json = serde_json::to_string(&r).expect("serialize");
489 let back: RBig = serde_json::from_str(&json).expect("deserialize");
490 assert_eq!(r, back);
491 }
492
493 #[cfg(feature = "serde")]
494 #[test]
495 fn rbig_serde_json_roundtrip_negative() {
496 let r = RBig::from_parts(IBig::from(-7), UBig::from(3u32));
497 let json = serde_json::to_string(&r).expect("serialize");
498 let back: RBig = serde_json::from_str(&json).expect("deserialize");
499 assert_eq!(r, back);
500 }
501
502 #[cfg(feature = "serde")]
503 #[test]
504 fn mixed_number_serde_json_roundtrip() {
505 let m = MixedNumber(RBig::from_parts(IBig::from(7), UBig::from(4u32)));
506 let json = serde_json::to_string(&m).expect("serialize");
507 let back: MixedNumber = serde_json::from_str(&json).expect("deserialize");
508 assert_eq!(m, back);
509 }
510}