drakegroup / sigfig

This is the sigfig Python package used for rounding numbers (with expected results)
MIT License
22 stars 6 forks source link

Rounding to D decimals, but keep a minimum of S significant figures #11

Open jgehrcke opened 1 year ago

jgehrcke commented 1 year ago

I am looking into combining the decimals=D and sigfigs=S criteria, in a prioritized fashion. Namely: round to D decimals, but keep a minimum of S significant figures.

Say we want to round to two decimals, but we want the result to retain at least two significant figures, e.g. round 1234.56789 to 1234.57 (two decimals, first priority), but round 0.00018793 to 0.00019 (two significant figures).

I read the documentation and played with the API a tiny bit and I believe this requires custom logic and two calls to sigfig.round(). Is that correct? If yes -- is this use case too exotic or should it maybe be supported via an official method, maybe needing only a single call to sigfig.round()?

jgehrcke commented 1 year ago

@MikeBusuttil, I would appreciate your opinion on this, if you find the time!

MikeBusuttil commented 1 year ago

Hey @jgehrcke absolutely and thanks for pinging me. I'm trying to think of a good interface... Perhaps: round(1234.56789, decimals=2, min_sigfigs=2) => 1234.57 round(0.00018793, decimals=2, min_sigfigs=2) => 0.00019 Would that encapsulate what you're looking for? And could you provide more example inputs and outputs?

If you could give me some details on the use case I could better evaluate whether it'd be worth the time investment to implement.

jgehrcke commented 1 year ago

@MikeBusuttil thank you so much for giving this a quick thought!

I do like the min_sigfigs signature.

If we think it's a viable problem statement and that this is actually useful: I would also love to contribute to this project with a PR once we align on the interface! The following might be a super naive implementation:

import sigfig

def round(numberstr, decimals=2, min_sigfig=2):
    # If decimals=2 then the smallest non-zero output value is 0.01. If the
    # result is smaller than that we've cut too much. Then use the sigfigs
    # criterion instead.
    #
    # What's the pattern here for detecting this cutoff criterion?
    #
    # 2 -> 0.01  -> 1/(10**2)
    # 3 -> 0.001 -> 1/(10**3)

    result_dec = sigfig.round(numberstr, decimals=decimals)

    if float(result_dec) < 1.0 / (10**decimals):
        return sigfig.round(numberstr, sigfigs=min_sigfig)

    return result_dec

assert round("12345.6789", decimals=2, min_sigfig=2) == "12345.68"
assert round("0.000056234", decimals=2, min_sigfig=2) == "0.000056"
assert round("0.000056834", decimals=2, min_sigfig=2) == "0.000057"
assert round("12345.6789", decimals=3, min_sigfig=2) == "12345.679"
assert round("0.0000056234", decimals=2, min_sigfig=1) == "0.000006"

Which problems/limitations would this implementation have?

Ah, interesting: if the input has less significant digits than min_sigfigs: error or silent ignore?

jagerber48 commented 1 year ago

@jgehrcke I'm also interested in your use case. Why do you need to display number of such wildly varying orders of magnitude using the same formatting configuration? Why do you want to see a minimum of two decimal places? Are there unshown uncertainties going along with these numbers?