3
\$\begingroup\$

I have the following code to round a number down to the nearest non zero digit in a string, or if the digit is larger than 1 it would round to the second decimal place. I have yet to find a way to do this without converting the number to a string and then using regex.

The end result of the number can be either a string or a digit, I kept it a string because i already converted it to one earlier in the method.

I am curious to know what you guys would do in this situation. I am working with all kinds of numbers (1 -> 50,000+ (generally)), however I am also working with VERY small numbers (4.42223e-9)

As always,

Thank you for taking the time to reading and answering this. I look forward to everyone's responses and criticism

import re

def roundToNearestZero(number):
    number = abs(number)
    if number < 1:
        #number = re.match("^0\\.0*[1-9][0-9]", f"{abs(number):.99f}").group()
        str_number = f"{number:.99f}"
        index = re.search('[1-9]', str_number).start()
        number = f"{str_number[:index + 3]}"
    
    else:
        number = f"{number:,.2f}"
    
    return number

print( roundToNearestZero(0.0000001232342) )# 0.000000123
print( roundToNearestZero(1.9333) ) # 1.93
print( roundToNearestZero(-0.00333) ) # 0.00333
print( roundToNearestZero(-123.3333) ) # 123.33
\$\endgroup\$
4
  • 2
    \$\begingroup\$ Why would you do this in the first place? This is not only a rounding function, since there is an abs in the mix. \$\endgroup\$
    – Reinderien
    Commented Jul 9, 2021 at 22:52
  • \$\begingroup\$ @Reinderien Hi, thank you for the question. I am creating a price checker and some of the coins/tokens are fractions of pennies. Some could be: 0.000000000012313149 and i am only interested in rounding to the nearest non zero digit: 0.00000000001. the reason i have abs() is because some i am also checking the price change within the last 24 hours and the API I use sends back scientific notation (which is why i have {number.99f}and negative numbers (-0.00000000123 for example) abs(). The abs() is not required in this method. I hope this helps and explains things a little bit more \$\endgroup\$ Commented Jul 9, 2021 at 23:10
  • \$\begingroup\$ I'm afraid that this entire procedure has a bad-idea fragrance. If you're formatting these values for display only, there are better formats. If this is part of an analysis routine you probably shouldn't be rounding at all. \$\endgroup\$
    – Reinderien
    Commented Jul 10, 2021 at 3:28
  • \$\begingroup\$ @Reinderien Yes its for display, could you tell me the better formats you're talking about? Thanks again \$\endgroup\$ Commented Jul 10, 2021 at 7:27

2 Answers 2

4
\$\begingroup\$

A few stylistic edits to consider. Your code is generally fine. In the example below, I made some minor stylistic edits: shorter variable names (because they are just as clear in context as the visually-heavier longer names); direct returns rather than setting number; and taking advantage of the built-in round() function where it applies. I would also add some explanatory comments to the code, as illustrated below. It needs some explanation, for reasons discussed below.

def roundToNearestZero(number):
    n = abs(number)
    if n < 1:
        # Find the first non-zero digit.
        # We want 3 digits, starting at that location.
        s = f'{n:.99f}'
        index = re.search('[1-9]', s).start()
        return s[:index + 3]
    else:
        # We want 2 digits after decimal point.
        return str(round(n, 2))

The function is unusual. Less convincing is the function itself. First, it's not a rounding operation: it takes the absolute value, converts to string, and then it does some rounding-adjacent things. If number is 1 or larger, the function actually rounds (to 2 digits past the decimal). Otherwise, the function imposes a floor operation (to 3 digits, starting at the first non-zero). The whole thing seems like a conceptual mess, but I understand that you might be operating under some odd constraints from your project that you cannot easily change.

Choose a better function name. At a minimum you should rename the function. Naming it according to mathematical operations makes little sense due to its inconsistent and unusual behavior. Instead you might consider a name oriented toward its purpose: for example, display_price() or whatever makes more sense based on its role in the project.

Data-centric orchestration. Finally, I would encourage you to adopt a more data-centric approach to working on algorithmic code – while writing code, debugging it, and when asking people for help. Here I'm talking about the orchestration code rather than the function itself. The first thing I did when experimenting with your code its to set up better orchestration:

# This is print-centric. As such, it's not very useful.

print( roundToNearestZero(0.0000001232342) )# 0.000000123
print( roundToNearestZero(1.9333) ) # 1.93
print( roundToNearestZero(-0.00333) ) # 0.00333
print( roundToNearestZero(-123.3333) ) # 123.33

# This is data-centric and thus amenable to efficient testing.
# Any bad edit I made to the code was quickly revealed, and I
# could easily add more test cases as I explored.

def main():
    TESTS = (
        (0.0000001232342, '0.000000123'),
        (1.9333, '1.93'),
        (-0.00333, '0.00333'),
        (-123.3333, '123.33'),
        (0.00000012032342, '0.000000120'),
        (-0.00000012032342, '0.000000120'),
        (-0.000000120999, '0.000000120'),
        (204.947, '204.95'),
    )
    for inp, exp in TESTS:
        got = roundToNearestZero(inp)
        if got == exp:
            print('ok')
        else:
            print('FAIL', (inp, got, exp))
\$\endgroup\$
3
\$\begingroup\$

First recognize that this is not strictly a rounding problem, but a formatting problem. You're doing this strictly for the purposes of display, and so your method should be named as such.

Have a careful read through https://docs.python.org/3/library/string.html#format-specification-mini-language . I think the g specifier would suit this application fine. I see no universe in which it's a good idea to hide the sign from the user, so don't apply abs.

for x in (
    4.42223e-9,
    0.0000001232342,
    1.9333,
    -0.00333,
    -123.3333,
    50_000,
):
    print(f'{x:+.3g}')
+4.42e-09
+1.23e-07
+1.93
-0.00333
-123
+5e+04

If you want slightly more nuanced formatting behaviour than what g can get you - if, for example, you want all quantities at 0.01 or above to be printed in fixed notation and all others in scientific - then you can have an if statement that selects between two different format specifiers, each with their own precision field; in any case you won't need to call round yourself because that's done by the formatting.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.