num-rational icon indicating copy to clipboard operation
num-rational copied to clipboard

num_rational::Ratio: Add the ability to format as a decimal

Open cuviper opened this issue 7 years ago • 38 comments

From @joshlf on December 21, 2017 20:10

Ratio's Display implementation formats as a fraction (e.g., 17/42). It would be useful if there were a way to format instead as a decimal (e.g., 0.4047619048).

Copied from original issue: rust-num/num#358

cuviper avatar Dec 21 '17 20:12 cuviper

Having issue #4 for float conversions may suffice here, as you could convert and then use any floating point formatting you like. Issue #1 for more formatting traits is also related.

cuviper avatar Dec 21 '17 21:12 cuviper

It'd be nice to have #1 so that you could print in full precision, but having float conversions would certainly be a good start, and probably good enough for most use cases.

joshlf avatar Dec 21 '17 21:12 joshlf

It'd be nice if we could somehow format the repeating part of the ratio, e.g. 1/22 becomes 0.0_45.

Of course, this would not be very trivial to implement and the floating-point conversion is probably best for most cases.

clarfonthey avatar Feb 27 '18 20:02 clarfonthey

A related concern is infinite representations. E.g., how do you print 1/3? I think the best would be to have at least some mechanism of getting a precise string representation if possible (i.e., a method that returns Option<String> and None if it's an infinite representation), and then maybe the default formatter (fmt::Display) could just fall back to the float representation or a truncated representation or something if it couldn't print with full accuracy.

joshlf avatar Feb 27 '18 20:02 joshlf

Every rational number's fractional part is either finite or repeating. You can print 1/3 exactly as 0.(3).

Emerentius avatar Apr 09 '18 03:04 Emerentius

Note however that the repeating part of x/n might be up to n-1 digits long! (see A051626) This can be impractical to display even for Rational32, nevermind Rational64 or BigRational.

cuviper avatar Apr 09 '18 19:04 cuviper

True. We could limit it by printing sth like 0.3431(538…). But in the absence of arbitrary formatting parameters, we would need a proxy type to change printing behaviour. As far as I'm aware cycle detection requires allocation as well.

Emerentius avatar Apr 12 '18 10:04 Emerentius

Any progress on that? I'd like to implement Gauss–Legendre algorithm to calculate Pi numbers, and I need to print the number as a decimal. I'd like to have something like print!("{:500}", my_num) which would print first 500 decimal digits. It would solve the problem with infinite decimal representation.

Pzixel avatar Jul 10 '18 15:07 Pzixel

I'm not aware that anyone has worked on this.

cuviper avatar Jul 10 '18 15:07 cuviper

I have not worked on this.

joshlf avatar Jul 10 '18 18:07 joshlf

I started to work on some small solution for personal needs recently. I'll try to clean it up and send it as a pull request for review and maybe adding it to the crate later.

It's nothing fancy though - just printing decimal representation to a specified precision(eg. 42 places after separator).

Hazardius avatar Jul 18 '18 07:07 Hazardius

I just finished refactorization of my code. But now I'm having some problems while trying to integrate it into the fmt method in the crate.

I have the feeling that adding a lot of traits dependencies is not a good practice. But currently my code is using multiplication, division and modulo of two Ratio<T> and power of Ratio<T> to the usize. I have another, earlier implementation which doesn't use almost any of those traits, but instead have couple of loops which I would prefer to avoid.

I`ll try to make one of those work(or at least compile) in the next few days.

EDIT: I've managed to compile it and make it pass some new tests. Pull request is now on.

Hazardius avatar Jul 21 '18 18:07 Hazardius

Did you end up putting up a PR? I had an idea - maybe this should be a method that returns an option? E.g., fn decimal(&self) -> Option<String>. Maybe also take a base parameter so it doesn't have to be base-10.

joshlf avatar Aug 17 '18 18:08 joshlf

The PR is hanging with its next version - as_decimal as a new method.

Using Option sound best - I'll try to make it so in a next few days and push an update to the same branch as the PR.

Changing it to use other base shouldn't be a big change, but then we should probably also change the name of the method. Any ideas?

EDIT[2018-08-21]: Made change to have Option, but I'm not sure how important it is since we always return a String. That's why I still didn't push it to the repo(and PR).

Hazardius avatar Aug 20 '18 13:08 Hazardius

Hi folks. I implemented a division module (with 2 coroutines and fn divide_to_string) for my project and I could contribute it to this crate if you are to consider that. The main features would be as follows:

  • The module implements division of two generic integers into a decimal numeral
  • The integers should implement following traits: Clone + Integer + From<u8> + ToPrimitive + CheckedMul + DivAssign + SubAssign, so works gracefully with BigUint for example.
  • Lossless division, linear complexity, no heap allocations, no stack overflows
  • Implemented as a coroutine producing a separate u8 for every digit in the final number, so potentially some formatting could be applied on the fly
  • fn divide_to_string<I>(dividend: I, divisor: I, mut precision: usize) -> Result<String, DivisionError>

Here are some examples:

assert_eq!(&divide_to_string(807, 31, 4).unwrap(), "26.0322");
assert!(match divide_to_string(1, 0, 1).err().unwrap() {
  DivisionError::DivisionByZero => true,
   _ => false
});

BigUint example:

    let num = "820123456789012345678901234567890123456789";
    let den = "420420420420240240420240420240420420420";
    let result_1024 = "1950.7222222204153880534352079149419688493160244330759069204925259490806291881834407646199555744655166719234903156227406500953778757619920899563294795746797267246703798051247915744867884912121330007705286911209791422213223230426822190127666529437572025264829201539629594175021000386486667365851813562015885600393107707737453721144405603009059457352854387023006301508907770762997683254372534811586343569328131455924964081595076622200116354403760742833073621862325616259444084808295834752749082040590201012701034118255745516514972270054835928011374231619434684843847682844123336846713100440285930668493675706096464476187809105398269815519780303174023949885603320678109566772031394249633001137109155600514761384776616827086908396960584246277151426685095767130738996420461009015758184659365258827537226581854494195009714400547427050697946126012192691851549545189173091941689299722345297086508711845865506663262915436874050594522308464492248968916100678819937355384703664061966410613989688966690413319849627099468906113991173042101218";

    let asrt1 = divide_to_string(
        BigUint::from_str_radix(num, 10).ok().unwrap(),
        BigUint::from_str_radix(den, 10).ok().unwrap(),
        1024
    ).ok();

    assert_eq!(asrt1.unwrap(), result_1024);

Here is the module itself: https://github.com/dnsl48/fraction/blob/0.4.x/src/division.rs

If you are interested, I could prepare a PR.

dnsl48 avatar Sep 24 '18 18:09 dnsl48

@dnsl48 - there's an open PR in #37, so it would be great if you could collaborate there.

cuviper avatar Oct 02 '18 18:10 cuviper

Fair enough. @Hazardius that's your PR, do you feel like reworking it completely? Your call.

dnsl48 avatar Oct 03 '18 17:10 dnsl48

@dnsl48 I'm unable to work on this before this weekend, when I was planning to fix things pointed out by cuviper . I'm sure I won't be able to rework code in that PR completely on my own, so I wouldn't mind if you start from scratch. Just let me know so I could close it.

Hazardius avatar Oct 04 '18 15:10 Hazardius

@dnsl48, @Hazardius, to be clear, I was not making a judgement of which of you had a better approach. I haven't compared your work in any depth. I'm just hoping that as two interested parties, you can work together to come to the best implementation.

cuviper avatar Oct 04 '18 16:10 cuviper

@cuviper I wouldn't mind judging my code as poor. I think about it that way. ;) I'll be happy to read through any PR solving that issue and maybe do some smaller changes, but right now I'm having a hard time finding a moment to focus on reworking my code completely(except for [maybe] weekends - so I'll try to read dnsl48's code this weekend after I fix things pointed out by you).

Hazardius avatar Oct 05 '18 06:10 Hazardius

I've just found an issue in my code that could make overflows due to some corner cases, e.g. Ratio<u8>(177, 253). I'll have to deal with it first.

dnsl48 avatar Oct 06 '18 19:10 dnsl48

Sorry @Hazardius, my implementation is too different so I made a separate PR. It's also Hacktoberfest going on, so I feel that a separate PR would be a fair thing in this situation. I'd be glad if you could have a review of my PR though, and give any feedback you can think of.

dnsl48 avatar Oct 07 '18 19:10 dnsl48

I have not read the entire thread, but why not using something like

format!("{:.2}",ratio.num() as f64 / ratio.denum() as f64)

where you can change the fraction amount by replacing the format string?

rudolfschmidt avatar Jun 30 '20 02:06 rudolfschmidt

@rudolfschmidt because I may want to have 100 or more significant digits in answer, which is not possible with f64, decimal or any other standard type.

Pzixel avatar Jun 30 '20 11:06 Pzixel

@rudolfschmidt because I may want to have 100 or more significant digits in answer, which is not possible with f64, decimal or any other standard type.

I agree that the result is just an approximation, maybe enough for visual purposes in most cases.

I am open to better solutions that I can apply to my own code as well.

In my Java days, I used to convert into double that should be an f64 in Rust, divided and printed the output.

rudolfschmidt avatar Jul 02 '20 10:07 rudolfschmidt

Let's me show some example.

Assume we are writing SuperPI and use num-rational for that purpose. Now we calculated first 1000 digits of PI. The question now is how do we print it? We cannot use approximation because it goes against the whole purpose of the app.

You can always cast to f64 to get approxmation but there are plenty cases when you need significant digits.

Pzixel avatar Jul 02 '20 10:07 Pzixel

I am open to better solutions that I can apply to my own code as well.

If you need it lossless, that functionality is implemented in the fraction module.

type D = fraction::Decimal;

let result = D::from(0.5) / D::from(0.3);
assert_eq!(format!("{:.4}", result), "1.6666");

It's open sourced. Anyone willing can check out the code and make a PR for this repo, if you'd like. (P.S. sorry, I don't have time to do that myself)

dnsl48 avatar Jul 02 '20 21:07 dnsl48

I'm interested in implementing this feature and creating a PR, however, I have a different idea on the API. I prefer to integrate the formatting into std::fmt with its precision parameter. We can either abuse the Pointer format or change the LowerExp/UpperExp to display decimal digits, and use the alternate flag to display cycle or not.

My proposal to abuse Pointer format:

  • format!("{:p}", Rational32::new(1, 3)) -> 1.3333333333 (we should have a default length for the decimals, let's say 10. We can also choose to display to full length if the rational doesn't have a repeating cycle)
  • format!("{:.4p}", Rational32::new(1, 3)) -> 1.3333
  • format!("{:#p}", Rational32::new(1, 3)) -> 1._3 (use _ to split the cycle, any marker will work)
  • format!("{:#.4p}", Rational32::new(1, 3)) -> 1._3_3_3_3 (display multiple cycle marker)
  • format!("{:p}", Rational32::new(123, 456)) -> 1.2697368421
  • format!("{:.6p}", Rational32::new(123, 456)) -> 1.269736
  • format!("{:#p}", Rational32::new(123, 456)) -> 1.269_7368421... (the repeating cycle is 736842105263157894, which doesn't fit in the default 10 digits, thus a ellipsis is printed. Finding only the start of the cycle could be hard to implement, if so, we can leave it the same as {:p} )
  • format!("{:#.6p}", Rational32::new(123, 456)) -> 1.269_736...

To use LowerExp/UpperExp, we can either use them as intended (display in scientific form), or abuse them as above. I personally prefer abuse Pointer format and use LowerExp/UpperExp to display in scientific form (rather than the current behavior, which displays the numerator and denominator separatly in scientific form) at the same time.

If this idea could get some likes, I'm happy to try to implement it and make a PR.

cmpute avatar Jan 11 '22 04:01 cmpute

A reasonable modification to my proposal above is to print ... whenever the decimal is not finite, and append a second marker to the fraction to indicate the end of cycle:

  • format!("{:p}", Rational32::new(1, 3)) -> 1.3333333333...
  • format!("{:.4p}", Rational32::new(1, 3)) -> 1.3333...
  • format!("{:#p}", Rational32::new(1, 3)) -> 1._3_

cmpute avatar Jan 11 '22 04:01 cmpute

IMHO, since it is acceptable in some places to put parentheses around the repeatand, I would personally prefer that over using underscores. I feel like abusing Unicode or other methods are probably less than ideal, but this at least will help indicate the repeatand just as well.

clarfonthey avatar Jan 11 '22 06:01 clarfonthey