Keep only two digits after the first digit other than zero

Question:

I know how to keep only two decimal places after the decimal point. For example (there are other methods):

>>> print(f'{10000.01908223295211791992:.2f}')
10000.02

But I want to keep the first two digits other than zero after the decimal point:

10000.01908223295211791992 will give: 10000.019

0.0000456576578765 will give: 0.000046

Is there an built-in method that I’m missing? Or is the only solution to code a test for (almost) every case?

Asked By: servoz

||

Answers:

This code works as you want, I define a function because there are some exception in doing this rounding operation:

import decimal 

# Set the precision of the decimal module to 20, it should be more than enough
ctx = decimal.Context()
ctx.prec = 20

# utility function to convert float to string
def float_to_str(f):
    d1 = ctx.create_decimal(repr(f))
    return format(d1, 'f')

def format_first_two_non_zero_digits(number):
    number_str = float_to_str(number) # Convert the number to a string
    decimal_point_index = number_str.find('.') # Find the position of the decimal point

    # If there is no decimal point, return the original number
    if decimal_point_index == -1:
        return number_str

    # Iterate through the digits after the decimal point
    for i in range(decimal_point_index + 1, len(number_str)):
        if number_str[i] != '0':
            # Found the first non-zero digit after the decimal point
            return number_str[:i + 2]

    return number_str # If all digits after the decimal point are zero, return the original number

# Examples
number1 = 10000.01908223295211791992
number2 = 0.0000456576578765

formatted1 = format_first_two_non_zero_digits(number1)
formatted2 = format_first_two_non_zero_digits(number2)

print(formatted1)  
print(formatted2) 
Answered By: Marco Parola

Marco’s answer is certainly valid, but I think it reinvents the wheel a bit when this could be solved with regular expressions:

import re

def rounder(number):
    pattern = re.compile(r'(d+.(([1-9]*)?|0+)0[1-9]{2})') #one or more numbers, a decimal point, then either any number of non-0s, or one or more zeros, followed by a zero and 2 other numbers
    if result:=pattern.match(str(number)):
        return(float(result.group(1)))
    else:
        return(None)

If the regex doesn’t match, it will simply return None—you can tweak this if you need error-handling.

Answered By: bc1155

Thanks to all the comments, I think the rounder_2 function is a good candidate.

import math

def rounder_2(x):
    """Round a value to two significant figures.

    If x is an integer or have only zeros after the decimal point, no need to round.
    If x is a float, keep only the first two digits other than zero after the decimal point
    """

    try:
        return round(x, -int(math.floor(math.log10(abs(math.modf(x)[0]))))+1)
    except ValueError:
        return x
Answered By: servoz

The following should be able to match the string and then round the value by applying a magnitude transformation.

import math
import re

TWO_DIGITS_AFTER_ZERO = re.compile(r'(d+.(?:(?:[1-9]*)?|0+)0[1-9]{2})')

def truncate(n: float) -> float:
    if result := TWO_DIGITS_AFTER_ZERO.match(format(n, 'f')):
        value = result.group(0)
        magnitude = 10**len(value.split('.')[1])
        return round(float(value) * magnitude) / magnitude
    else:
        return n
 

if __name__ == '__main__':
    assert math.isclose(truncate(10000.01908223295211791992), 10000.019)
    assert math.isclose(truncate(0.0000456576578765), 0.000046)

Here is an example utilizing the ndigits keyword argument of the round() function:

from math import floor, isclose, log10, modf
from typing import Callable

f: Callable[[float],float] = lambda n: round(n, -int(floor(log10(abs(modf(n)[0]))))+1)

if __name__ == '__main__':
    assert isclose(f(10000.01908223295211791992), 10000.019)
    assert isclose(f(0.0000456576578765), 0.000046)
Answered By: Mr. Polywhirl
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.