Golang四舍五入到最近的0.05。

huangapple go评论70阅读模式
英文:

Golang Round to Nearest 0.05

问题

我正在寻找一个在Golang中将数字四舍五入到最接近的0.05的函数。使用该函数的最终结果必须始终是0.05的倍数。

以下是我正在寻找的函数的一些输出示例:
(函数Round尚不存在,我希望它可以包含在答案中)

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

我已经搜索了很长时间,但没有找到任何答案。

英文:

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.

答案1

得分: 105

**前言:**我在github.com/icza/gox发布了这个实用程序,请参阅mathx.Round()


Go 1.10已经发布,它添加了一个math.Round()函数。该函数将数字四舍五入到最近的整数(基本上是一个“四舍五入到最近的1.0”的操作),利用这个函数,我们可以很容易地构建一个将数字四舍五入到我们选择的单位的函数:

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

测试一下:

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

Go Playground上试一试。

下面是在Go 1.10之前,math.Round()函数不存在时创建的原始答案,它还详细介绍了我们自定义的Round()函数的逻辑。这里是为了教育目的而提供的。


在Go 1.10之前,没有math.Round()函数。但是...

通过float64int64的转换可以很容易地实现四舍五入的任务,但必须注意,浮点数到整数的转换不是四舍五入,而是保留整数部分(详见https://stackoverflow.com/questions/33206059/go-converting-float64-to-int-with-multiplier)。

例如:

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

结果在两种情况下都是12,即整数部分。要获得四舍五入的“功能”,只需添加0.5

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

到目前为止都很好。但我们不想四舍五入到整数。如果我们想要四舍五入到小数点后一位,我们可以在添加0.5和转换之前将数字乘以10:

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

因此,基本上你需要将数字乘以你想要四舍五入的单位的倒数。要四舍五入到0.05单位,乘以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

将其封装成一个函数:

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

使用它:

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

Go Playground上尝试这些示例。

请注意,使用unit=0.053.232四舍五入到0.05单位时,打印出的结果不是精确的3.25,而是0.35000000000000003。这是因为float64数字使用有限精度存储,称为IEEE-754标准。有关详细信息,请参见https://stackoverflow.com/questions/36052922/golang-converting-float64-to-int-error。

还要注意,unit可以是“任何”数字。如果unit1,那么Round()函数基本上就是四舍五入到最近的整数。如果unit10,它会四舍五入到十位数,如果unit0.01,它会四舍五入到小数点后两位。

还要注意,当你用负数调用Round()函数时,可能会得到令人惊讶的结果:

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

这是因为如前所述,转换是保留整数部分,例如-1.6的整数部分是-1(大于-1.6),而1.6的整数部分是1(小于1.6)。

如果你希望-0.363636变为-0.35而不是-0.30,那么在处理负数时,在Round()函数内部的0.5之前添加-0.5。看看我们改进的Round2()函数:

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
}

使用它:

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

编辑:

回应你的评论:因为你不“喜欢”非精确的0.35000000000000003,你提议将其格式化并重新解析,像这样:

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

这“看似”会得到精确的结果,因为打印出来的结果正好是0.35

但这只是一个“幻觉”。由于使用IEEE-754标准,0.35无法用有限的位数表示,无论你对数字做什么操作,如果将其存储在float64类型的值中,它都不会完全等于0.35(而是一个非常接近0.35的IEEE-754数字)。你看到的是fmt.Println()将其打印为0.35,因为fmt.Println()已经进行了一些四舍五入。

但是,如果你尝试以更高的精度打印它:

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))

输出结果并不好看(甚至可能更丑陋):在Go Playground上试一试:

0.349999999999999977795539507497
3.250000000000000000000000000000
0.500000000000000000000000000000

请注意,另一方面,3.250.5是精确的,因为它们可以用有限的位数准确地表示,因为在二进制中表示:

3.25 = 3 + 0.25 = 11.01二进制
0.5 = 0.1二进制

这个教训是,不值得格式化和重新解析结果,因为它也不会是精确的(只是一个不同的float64值,根据默认的fmt.Println()格式化规则,可能在打印时更好看)。如果你想要漂亮的打印格式,只需使用精度格式化,例如:

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
}

它将是精确的(在Go Playground上试一试):

0.350
3.250
0.500

或者将它们乘以100并使用整数进行计算,这样就不会出现表示或四舍五入误差。

英文:

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 https://stackoverflow.com/questions/33206059/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 https://stackoverflow.com/questions/36052922/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.

huangapple
  • 本文由 发表于 2016年9月17日 16:19:54
  • 转载请务必保留本文链接:https://go.coder-hub.com/39544571.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定