Golang Round to Nearest 0.05

34,105

Foreword: I released this utility in github.com/icza/gox, see mathx.Round().


Go 1.10 has been released, and it adds a math.Round() function. This function rounds to the nearest integer (which is basically a "round to nearest 1.0" operation), and using that we can very easily construct a function that rounds to the unit of our choice:

func Round(x, unit float64) float64 {
    return math.Round(x/unit) * unit
}

Testing it:

fmt.Println(Round(0.363636, 0.05)) // 0.35
fmt.Println(Round(3.232, 0.05))    // 3.25
fmt.Println(Round(0.4888, 0.05))   // 0.5

fmt.Println(Round(-0.363636, 0.05)) // -0.35
fmt.Println(Round(-3.232, 0.05))    // -3.25
fmt.Println(Round(-0.4888, 0.05))   // -0.5

Try it on the Go Playground.

The original answer follows which was created before Go 1.10 when no math.Round() existed, and which also details the logic behind our custom Round() function. It's here for educational purposes.


In the pre-Go1.10 era there was no math.Round(). But...

Rounding tasks can easily be implemented by a float64 => int64 converison, but care must be taken as float to int conversion is not rounding but keeping the integer part (see details in Go: Converting float64 to int with multiplier).

For example:

var f float64
f = 12.3
fmt.Println(int64(f)) // 12
f = 12.6
fmt.Println(int64(f)) // 12

Result is 12 in both cases, the integer part. To get the rounding "functionality", simply add 0.5:

f = 12.3
fmt.Println(int64(f + 0.5)) // 12
f = 12.6
fmt.Println(int64(f + 0.5)) // 13

So far so good. But we don't want to round to integers. If we'd wanted to round to 1 fraction digit, we would multiply by 10 before adding 0.5 and converting:

f = 12.31
fmt.Println(float64(int64(f*10+0.5)) / 10) // 12.3
f = 12.66
fmt.Println(float64(int64(f*10+0.5)) / 10) // 12.7

So basically you multiply by the reciprocal of the unit you want to round to. To round to 0.05 units, multiply by 1/0.05 = 20:

f = 12.31
fmt.Println(float64(int64(f*20+0.5)) / 20) // 12.3
f = 12.66
fmt.Println(float64(int64(f*20+0.5)) / 20) // 12.65

Wrapping this into a function:

func Round(x, unit float64) float64 {
    return float64(int64(x/unit+0.5)) * unit
}

Using it:

fmt.Println(Round(0.363636, 0.05)) // 0.35
fmt.Println(Round(3.232, 0.05))    // 3.25
fmt.Println(Round(0.4888, 0.05))   // 0.5

Try the examples on the Go Playground.

Note that rounding 3.232 with unit=0.05 will not print exactly 3.25 but 0.35000000000000003. This is because float64 numbers are stored using finite precision, called the IEEE-754 standard. For details see Golang converting float64 to int error.

Also note that unit may be "any" number. If it's 1, then Round() basically rounds to nearest integer number. If it's 10, it rounds to tens, if it's 0.01, it rounds to 2 fraction digits.

Also note that when you call Round() with a negative number, you might get surprising result:

fmt.Println(Round(-0.363636, 0.05)) // -0.3
fmt.Println(Round(-3.232, 0.05))    // -3.2
fmt.Println(Round(-0.4888, 0.05))   // -0.45

This is because –as said earlier– conversion is keeping the integer part, and for example integer part of -1.6 is -1 (which is greater than -1.6; while integer part of 1.6 is 1 which is less than 1.6).

If you want -0.363636 to become -0.35 instead of -0.30, then in case of negative numbers add -0.5 instead of 0.5 inside the Round() function. See our improved Round2() function:

func Round2(x, unit float64) float64 {
    if x > 0 {
        return float64(int64(x/unit+0.5)) * unit
    }
    return float64(int64(x/unit-0.5)) * unit
}

And using it:

fmt.Println(Round2(-0.363636, 0.05)) // -0.35
fmt.Println(Round2(-3.232, 0.05))    // -3.25
fmt.Println(Round2(-0.4888, 0.05))   // -0.5

EDIT:

To address your comment: because you don't "like" the non-exact 0.35000000000000003, you proposed to format it and re-parse it like:

formatted, err := strconv.ParseFloat(fmt.Sprintf("%.2f", rounded), 64)

And this "seemingly" results in the exact result as printing it gives 0.35 exactly.

But this is just an "illusion". Since 0.35 cannot be represented with finite bits using IEEE-754 standard, doesn't matter what you do with the number, if you store it in a value of type float64, it won't be exactly 0.35 (but an IEEE-754 number being very close to it). What you see is fmt.Println() printing it as 0.35 because fmt.Println() already does some rounding.

But if you attempt to print it with higher precision:

fmt.Printf("%.30f\n", Round(0.363636, 0.05))
fmt.Printf("%.30f\n", Round(3.232, 0.05))
fmt.Printf("%.30f\n", Round(0.4888, 0.05))

Output: it's not nicer (might be even uglier): try it on the Go Playground:

0.349999999999999977795539507497
3.250000000000000000000000000000
0.500000000000000000000000000000

Note that on the other hand 3.25 and 0.5 are exact because they can be represented with finite bits exactly, because representing in binary:

3.25 = 3 + 0.25 = 11.01binary
0.5 = 0.1binary

What's the lesson? It's not worth formatting and re-parsing the result, as it won't be exact either (just a different float64 value which –according to default fmt.Println() formatting rules– might be nicer in printing). If you want nice printed format, just format with precision, like:

func main() {
    fmt.Printf("%.3f\n", Round(0.363636, 0.05))
    fmt.Printf("%.3f\n", Round(3.232, 0.05))
    fmt.Printf("%.3f\n", Round(0.4888, 0.05))
}

func Round(x, unit float64) float64 {
    return float64(int64(x/unit+0.5)) * unit
}

And it will be exact (try it on the Go Playground):

0.350
3.250
0.500

Or just multiply them by 100 and work with integer numbers, so that no representation or rounding error may occur.

Share:
34,105
Ari Seyhun
Author by

Ari Seyhun

Updated on September 16, 2021

Comments

  • Ari Seyhun
    Ari Seyhun over 2 years

    I am looking for a function to round to the nearest 0.05 in Golang. The end result of using the function must always be a factor of 0.05.


    Here are some examples of outputs for the function I am looking for: (The function Round doesn't exist yet, I am hoping it can be included in the answer)

    Round(0.363636) // 0.35
    Round(3.232)    // 3.25
    Round(0.4888)   // 0.5
    

    I have searched around for ages now and haven't found any answers.