Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

surprising NaN in conversion from f64 #112

Open
majewsky opened this issue Aug 30, 2024 · 6 comments
Open

surprising NaN in conversion from f64 #112

majewsky opened this issue Aug 30, 2024 · 6 comments
Assignees
Labels

Comments

@majewsky
Copy link

This test fails:

#[test]
fn just_a_normal_float_nothing_to_see() {
    let foo = fraction::Fraction::from(0.00003233568143917312);
    assert!(!foo.is_nan());
}
@dnsl48
Copy link
Owner

dnsl48 commented Sep 9, 2024

Hi @majewsky, thanks for reporting this!

@dnsl48 dnsl48 self-assigned this Sep 9, 2024
@dnsl48 dnsl48 added the bug label Sep 9, 2024
@dnsl48
Copy link
Owner

dnsl48 commented Sep 13, 2024

Hi @majewsky,
That may be surprising because it is not too well documented.
However, this is expected behaviour for values that don't fit into the underlying types.
If you would like to handle big values gracefully, you may need to use BigFraction, which uses heap allocated memory for storing huge numbers.

Longer explanation:

  • Fraction is GenericFraction<u64>
  • u64::MAX is 18446744073709551615
  • initial denominator is 100000000000000000000 (which does not fit into u64)
  • int overflow is converted to NaN, since from cannot return Result/Option

If you use either GenericFraction<u128> or BigFration, your value should get parsed successfully.

@dnsl48 dnsl48 added question and removed bug labels Sep 13, 2024
@majewsky
Copy link
Author

I suspected something like this. Would it then make sense to have this be an impl TryFrom instead of impl From?

@dnsl48
Copy link
Owner

dnsl48 commented Sep 17, 2024

I agree. We should cater try_from for more reliable conversions.
It could probably make sense to consider deprecating from for unreliable conversions:

  • e.g. GenericFraction<u8>::from::<f64>() is unreliable
  • while BigFraction::from::<f32>() is reliable

@majewsky
Copy link
Author

majewsky commented Sep 17, 2024

I randomly came across how another similar library solves this problem, and will leave this here as potential inspiration.

The bigint/bigfloat implementation in the Go standard library provides a cast from floating-point numbers into Rat (which is short for "rational" and their version of the Fraction type), that does not panic on loss of accuracy. Instead, it reports in a second return value whether accuracy was lost: https://pkg.go.dev/math/big#Float.Rat

I don't know whether it is feasible to actually obtain the required information from the float-to-fraction conversion algorithm that you're using, but from an API design perspective, it would be nice to have, say:

impl TryFrom<f64> for GenericFraction<u64> {
    type Error = LossOfPrecisionError<GenericFraction<u64>>; // placeholder name
}

struct LossOfPrecisionError<F> {
    best_estimate: F,
    // ...other fields to describe the loss of precision...
    // (e.g. the Go library reports whether the estimate is below or above the true value)
}

Library users could then decide to .unwrap() the conversion result to replicate the existing behavior, or do something like .unwrap_or_else(|err| err.best_estimate) to continue with the best-effort value.

@dnsl48
Copy link
Owner

dnsl48 commented Sep 21, 2024

Thanks for the feedback.

I agree, that's one of the sensible ways to do that.
I feel we can do both - provide the standard API implementations such as try_from and from, AND we can also add the best estimate feature to the approx module.

The current from logic was implemented years ago before TryFrom trait was stabilised in the std library. Now it feels to be the right time to make full use of it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants