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

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

Open
cuviper opened this issue Dec 21, 2017 · 38 comments
Open

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

cuviper opened this issue Dec 21, 2017 · 38 comments

Comments

@cuviper
Copy link
Member

cuviper commented Dec 21, 2017

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
Copy link
Member Author

cuviper commented Dec 21, 2017

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.

@joshlf
Copy link

joshlf commented Dec 21, 2017

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.

@clarfonthey
Copy link
Contributor

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.

@joshlf
Copy link

joshlf commented Feb 27, 2018

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.

@Emerentius
Copy link
Contributor

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

@cuviper
Copy link
Member Author

cuviper commented Apr 9, 2018

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.

@Emerentius
Copy link
Contributor

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.

@Pzixel
Copy link

Pzixel commented Jul 10, 2018

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.

@cuviper
Copy link
Member Author

cuviper commented Jul 10, 2018

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

@joshlf
Copy link

joshlf commented Jul 10, 2018

I have not worked on this.

@Hazardius
Copy link

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
Copy link

Hazardius commented Jul 21, 2018

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 added a commit to Hazardius/num-rational that referenced this issue Jul 21, 2018
…approximation to the fmt if it's given a Formatter with precision.
Hazardius added a commit to Hazardius/num-rational that referenced this issue Aug 11, 2018
…he separate method 'as_decimal' returning String.
Hazardius added a commit to Hazardius/num-rational that referenced this issue Aug 11, 2018
Hazardius added a commit to Hazardius/num-rational that referenced this issue Aug 11, 2018
@joshlf
Copy link

joshlf commented Aug 17, 2018

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.

@Hazardius
Copy link

Hazardius commented Aug 20, 2018

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).

@dnsl48
Copy link
Contributor

dnsl48 commented Sep 24, 2018

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.

Hazardius added a commit to Hazardius/num-rational that referenced this issue Sep 26, 2018
@cuviper
Copy link
Member Author

cuviper commented Oct 2, 2018

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

@dnsl48
Copy link
Contributor

dnsl48 commented Oct 3, 2018

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

@Hazardius
Copy link

@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.

@cuviper
Copy link
Member Author

cuviper commented Oct 4, 2018

@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.

@Hazardius
Copy link

@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 added a commit to Hazardius/num-rational that referenced this issue Oct 6, 2018
@dnsl48
Copy link
Contributor

dnsl48 commented Oct 6, 2018

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
Copy link
Contributor

dnsl48 commented Oct 7, 2018

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.

@rudolfschmidt
Copy link

rudolfschmidt commented Jun 30, 2020

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?

@Pzixel
Copy link

Pzixel commented Jun 30, 2020

@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.

@rudolfschmidt
Copy link

@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.

@Pzixel
Copy link

Pzixel commented Jul 2, 2020

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.

@dnsl48
Copy link
Contributor

dnsl48 commented Jul 2, 2020

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)

@cmpute
Copy link

cmpute commented Jan 11, 2022

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
Copy link

cmpute commented Jan 11, 2022

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_

@clarfonthey
Copy link
Contributor

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.

@cmpute
Copy link

cmpute commented Jan 11, 2022

I didn't know that using parentheses is a common way, in that case we can definitely use parentheses to mark the cycle

  • format!("{:#p}", Rational32::new(1, 3)) -> 1.(3)
  • format!("{:#.4p}", Rational32::new(1, 3)) -> 1.(3) or 1.(3)(3)(3)(3)?
  • format!("{:#p}", Rational32::new(123, 456)) -> 1.269(7368421... or 1.269(7368421...)

@clarfonthey
Copy link
Contributor

The repeating part should be inside the parentheses, so 1.(xxx) to me indicates that xxx repeats, but 1.(xxx...) indicates that xxx isn't the full repeatand. I'm not sure how useful multiple repeats of the repeatand actually is, but IMHO since expanding the repeatand into multiple repetitions doesn't make it inaccurate, I feel like the second one, if we did it at all, would be 1.(3333) for brevity.

@cmpute
Copy link

cmpute commented Jan 12, 2022

Multiple repeats are not necessary, I put four 3's here just to comply with the specified number of digits. It might be better to just have 1.(3) as long as the whole repeating part is fully displayed

@clarfonthey
Copy link
Contributor

clarfonthey commented Jan 12, 2022

Honestly, handling requests for excessive precision, it may be best to only parenthesise the first group. So, for example, if someone requests 8 digits of precision for 1/7, then we could display 0.(142857)14…, indicating both the repeated part and the number of digits requested. I'm using "digits" here to mean actual digits, but it's probably worth asking whether we should treat the precision as a number of digits, or a number of characters, since the latter would require at least 3 extra characters for the above format (assuming we use a Unicode ellipsis, otherwise 5) for repeating decimals.

Another potential thing is whether we should still show the repeating part for non-repeating decimals in certain scenarios, e.g. should 3/10 ever equal 0.3(0)…?

@cmpute
Copy link

cmpute commented Jan 12, 2022

Yeah I think 1.(428571)428.. is a good idea. For finite decimal, I think it's better to leave it as simple as possible. Even if someone want to detect whether the decimal is repeating or not, they can easily do this by checking whether there's parenthesis or not

@8573
Copy link

8573 commented Jan 12, 2022

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. [...] 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 [...]

Rather than abusing the format traits, would it not be better to use methods that return wrappers that control how a Ratio is Displayed, like uuid does? Then choices such as whether to use non-ASCII characters, or whether to limit just digits or total glyphs, can be made with, for example, a builder pattern on the wrapper.

@cuviper
Copy link
Member Author

cuviper commented Jan 12, 2022

I agree that we shouldn't abuse the format traits. Pointer in particular is also weird because it always prints &T as its memory address, whereas all the other format traits will forward to the T's implementation. So &Ratio<T> would not use your code.

I like the idea of a customizable wrapper. Some folks will want plain decimal precision with rounding, and others might want precisely-represented repetition -- the latter could even be gotten by further methods to extract parts. (e.g. integer, non_repeating, and repeating parts.)

@cmpute
Copy link

cmpute commented Jan 13, 2022

I prefer to use fmt rather than any explicit methods since IMO a Ratio can be a replacement for float numbers (just as the crate fraction does). I'm not negative about the wrapper idea, however I also want that the rational number can be easily displayed just as other numeric types do (uuid is not a numeric type). Besides, using a wrapper will bring a whole lot more code .

How about directly use default format with precision to display the digits, as the precision parameter is unused in default format

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

To retrieve the non-repeating and repeating part, we can provide methods directly for Ratio<T> itself, rather than on a sub type.

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

No branches or pull requests

10 participants