4

I was searching how to convert a float to the simplest fraction that would convert back to it and found this answer.

Problem is, the Python implementation given relies on the as_integer_ratio facility in the python standard library, which isn't present in Rust. I asked about this in a comment and found out about f64::frexp but I'm not sure I understand how it works, as its documentation is quite cryptic (to me at least):

Breaks the number into a normalized fraction and a base-2 exponent, satisfying:
self = x * 2^exp
0.5 <= abs(x) < 1.0

And on top of that, it's an unstable feature.

What should I do?

Herohtar
  • 5,347
  • 4
  • 31
  • 41
lenerdv
  • 177
  • 13
  • I'm not sure how it actually works, but I'd start by reading the [Wikipedia article on continued fractions](https://en.wikipedia.org/wiki/Continued_fraction). I believe looking at the convergents of the continued fraction expansion of the exact value and taking the first one that evaluates to the same floating-point number should do the trick. The most difficult part is to define what "the same floating-point number" is supposed to mean. – Sven Marnach May 18 '22 at 14:16
  • What types do you want to use for the fractions? For large floats, the numerator of that float will be too large to fit in an `i64` (or even an `i128`); similarly for the denominator of very small float. Do you plan to limit the range of the input floats? Or use a biginteger type? – Mark Dickinson May 18 '22 at 15:16

2 Answers2

1

The complicated thing about float-to-fraction conversion is that all floats are already rationals with a power-of-two denominator, but that probably isn't so useful. What you are looking for is "best rational approximation", to find the closest rational to a target float value within some max denominator.

The algorithm (described in the link) has some clever continued fractions math behind it, but not too difficult to put into code. Here is a small C library that implements it:

Example use:

#include "number_util.h"

int numerator;
int denominator;
RationalApproximation(M_PI, 1000000, NULL, &numerator, &denominator);
printf("%d/%d\n", numerator, denominator); 
// Prints: 3126535/995207 (= 3.14159265...)

This is hopefully straightforward to port or wrap for use in other languages.

Pascal Getreuer
  • 2,906
  • 1
  • 5
  • 14
0

Algorithm apart, I guess the easiest way would be to use something working already, like the fraction crate:

From the example

use std::str::FromStr;
use fraction::{Fraction, Sign};  // choose the type accordingly with your needs (see prelude module docs)

fn main() {
    // There are several ways to construct a fraction, depending on your use case

    let f = Fraction::new(1u8, 2u8);  // constructs with numerator/denominator and normalizes the fraction (finds least common denominator)
    assert_eq!(f, Fraction::new_generic(Sign::Plus, 1i32, 2u8).unwrap());  // with numerator/denominator of different integer types
    assert_eq!(f, Fraction::from(0.5));  // convert from float (f32, f64)
    assert_eq!(f, Fraction::from_str("0.5").unwrap());  // parse a string

    // Raw construct with no extra calculations.
    // Most performant, but does not look for common denominator and may lead to unexpected results
    // in following calculations. Only use if you are sure numerator/denominator are already normalized.
    assert_eq!(f, Fraction::new_raw(1u64, 2u64));
}
Netwave
  • 40,134
  • 6
  • 50
  • 93
  • It works, but I'm mainly looking for a clue to then implement this myself. I've looked at the source code and it essentially boils down to multiplying by a big power of ten then ratioing that, which doesn't really give the _exact_ value stored in the float, as it would require working with powers of 2 – lenerdv May 18 '22 at 09:52
  • 1
    @lenerdv, did you check the [python implementation](https://github.com/python/cpython/blob/bcf14ae4336fced718c00edc34b9191c2b48525a/Lib/_pydecimal.py#L986) of `as_integer_ratio`? – Netwave May 18 '22 at 10:07
  • 1
    @Netwave: That's for the `Decimal` type; for regular binary floats, `as_integer_ratio` is defined [here](https://github.com/python/cpython/blob/db388df1d9aff02f791fe01c7c2b28d73982dce6/Objects/floatobject.c#L1561-L1630) – Mark Dickinson May 18 '22 at 15:14
  • @MarkDickinson, nice, I couldn't find it. Thanks! – Netwave May 18 '22 at 15:20