Search code examples
pythondecimalroundingfractionsfloor

python: round/format float/string to specific decimal/fraction


I've found many posts regarding rounding in python, but from my understanding none so far solves my problem.

Two solutions I'm looking for:

  • Round closest: float/string numbers which need to be rounded differently depending on their size.

  • Round down: float/string numbers which need to be rounded down differently depending on their size.

  • Round up: float/string numbers which need to be rounded up differently depending on their size.

Example:

  • +500: 0 decimals (whole numbers) (e.g., Closest: 500.5 -> 501; Down: 500.99 -> 500; Up: 500.01 -> 501)
  • +100: 1 decimals (fraction: 0.2) (e.g., Closest: 100.29 -> 100.2; Down: 100.39 -> 100.2; Up: 100.21 -> 100.4). So, the value must end with .0, .2, .4, .6 or .8
  • +5: 2 decimals (fraction: 0.01) (e.g., Closest: 5.019 -> 5.02; Down: 5.019 -> 5.01; Up: 5.011 -> 5.02). So, the value must end with .x0, .x1, .x2, .x3, .x4, .x5, .x6, .x7, .x8 or .x9

The resulting variable should be a string.

The neat thing I'm, looking for is a function in which i can pass number of decimals and fraction


Solution

  • You can use the Decimal module for highly customizable rounding:

    import decimal 
    
    def rnd(f,n,r=decimal.ROUND_HALF_DOWN):
        pat='1.'+'0'*n
        return str(decimal.Decimal(str(f)).quantize(decimal.Decimal(pat),r))
    

    Now to test:

    for n in (500.99,100.39,5.019):
        for i in (0,1,2):
            print('n={:10}, i={:2}, Down: {:>10}, Closest: {:>10}'.format(n,i, rnd(n,i,decimal.ROUND_FLOOR),rnd(n,i)))   
    

    Prints:

    n=    500.99, i= 0, Down:        500, Closest:        501
    n=    500.99, i= 1, Down:      500.9, Closest:      501.0
    n=    500.99, i= 2, Down:     500.99, Closest:     500.99
    n=    100.39, i= 0, Down:        100, Closest:        100
    n=    100.39, i= 1, Down:      100.3, Closest:      100.4
    n=    100.39, i= 2, Down:     100.39, Closest:     100.39
    n=     5.019, i= 0, Down:          5, Closest:          5
    n=     5.019, i= 1, Down:        5.0, Closest:        5.0
    n=     5.019, i= 2, Down:       5.01, Closest:       5.02
    

    (Note: This is different output in the case of 1 decimal and the input of 100.39. Please clarify the desired output of 100.2 in that case? Shouldn't the output be 100.3?)


    With the clarification, I think you just need to use fmod:

    import math 
    
    def rnd(n,f,prec,direction='down'):
        if direction=='down':
            if prec==0:
                return str(int(n))
            return str(round(n-math.fmod(n,f),prec))
        else:
            if prec==0:
                return str(int(round(n)))
            n+=f
            return str(round(n-math.fmod(n,f),prec))
    

    Works with all examples given:

    >>> rnd(500.99,0.1,0)
    '500'
    >>> rnd(500.99,0.1,0,'')
    '501'
    >>> rnd(100.39,0.2,1,'')
    '100.4'
    >>> rnd(100.39,0.2,1)
    '100.2'
    >>> rnd(100.59,0.2,1,'')
    '100.6'
    >>> rnd(100.59,0.2,1)
    '100.4'
    >>> rnd(5.019,0.01,2)
    '5.01'
    >>> rnd(5.019,0.01,2,'')
    '5.02'
    

    (Call the function with the direction= keyword argument with any string value other than 'down' to get up. Change to your liking...)


    Edit 2

    This returns the method closest to the input number:

    def rnd(n,f,prec):
        n1=int(n) if prec==0 else round(n-math.fmod(n,f),prec)
        n2=float(int(round(n))) if prec==0 else round((n+f)-math.fmod((n+f),f),prec)
        return str(min((n1,n2), key=lambda e: math.fabs(e-n)))