四舍五入是一个非常常见的功能,在流行语言标准库中往往存在 Round 的功能,它最少支持常用的 Round half up 算法。

而在 Go 语言中这似乎成为了难题,在 stackoverflow 上搜索 [go] Round 会存在大量相关提问,Go 1.10 开始才出现 math.Round 的身影,本以为 Round 的疑问就此结束,但是一看函数注释 Round returns the nearest integer, rounding half away from zero ,这是并不常用的 Round half away from zero 实现呀,说白了就是我们理解的 Round 阉割版,精度为 0 的 Round half up 实现,Round half away from zero 的存在是为了提供一种高效的通过二进制方法得结果,可以作为 Round 精度为 0 时的高效实现分支。

带着对 Round 的‘敬畏’,我在 stackoverflow 翻阅大量关于 Round 问题,开启寻求最佳的答案,本文整理我认为有用的实现,简单分析它们的优缺点,对于不想逐步了解,想直接看结果的小伙伴,可以直接看文末的最佳实现,或者跳转 exmath.Round 直接看源码和使用吧!

Round 第一弹

stackoverflow 问题中的最佳答案首先获得我的关注,它在 mathx.Round 被开源,以下是代码实现:

//source: https://github.com/icza/gox/blob/master/mathx/mathx.go
package mathx import "math" // Round returns x rounded to the given unit.
// Tip: x is "arbitrary", maybe greater than 1.
// For example:
// Round(0.363636, 0.001) // 0.364
// Round(0.363636, 0.01) // 0.36
// Round(0.363636, 0.1) // 0.4
// Round(0.363636, 0.05) // 0.35
// Round(3.2, 1) // 3
// Round(32, 5) // 30
// Round(33, 5) // 35
// Round(32, 10) // 30
//
// For details, see https://stackoverflow.com/a/39544897/1705598
func Round(x, unit float64) float64 {
return math.Round(x/unit) * unit
}

这个实现非常的简洁,借用了 math.Round,由此看来 math.Round 还是很有价值的,大致测试了它的性能一次运算大概 0.4ns,这非常的快。

但是我也很快发现了它的问题,就是精度问题,这个是问题中一个回答的解释让我有了警觉,并开始了实验。他认为使用浮点数确定精度(mathx.Round的第二个参数)是不恰当的,因为浮点数本身并不精确,例如 0.05 在64位IEEE浮点数中,可能会将其存储为0.05000000000000000277555756156289135105907917022705078125

//source: https://play.golang.org/p/0uN1kEG30kI
package main import (
"fmt"
"math"
) func main() {
f := 12.15807659924030304
fmt.Println(Round(f, 0.0001)) // 12.158100000000001 f = 0.15807659924030304
fmt.Println(Round(f, 0.0001)) // 0.15810000000000002
} func Round(x, unit float64) float64 {
return math.Round(x/unit) * unit
}

以上代码可以在 Go Playground 上运行,得到结果并非如期望那般,这个问题主要出现在 math.Round(x/unit)unit 运算时,math.Round 运算后一定会是一个精确的整数,但是 0.0001 的精度存在误差,所以导致最终得到的结果精度出现了偏差。

格式化与反解析

在这个问题中也有人提出了先用 fmt.Sprintf 对结果进行格式化,然后再采用 strconv.ParseFloat 反向解析,Go Playground 代码在这个里。

source: https://play.golang.org/p/jxILFBYBEF
package main import (
"fmt"
"strconv"
) func main() {
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
} func Round(x, unit float64) float64 {
var rounded float64
if x > 0 {
rounded = float64(int64(x/unit+0.5)) * unit
} else {
rounded = float64(int64(x/unit-0.5)) * unit
}
formatted, err := strconv.ParseFloat(fmt.Sprintf("%.2f", rounded), 64)
if err != nil {
return rounded
}
return formatted
}

这段代码中有点问题,第一是结果不对,和我们理解的存在差异,后来一看第二个参数传错了,应该是 0.01,我想试着调整调整精度吧,我改成了 0.0001 之后发现一直都是保持小数点后两位,我细细研究了下这段代码的逻辑,发现 fmt.Sprintf("%.2f", rounded) 中写死了保留的位数,所以它并不通用,我尝试如下简单调整一下使其生效。

package main

import (
"fmt"
"strconv"
) func main() {
f := 12.15807659924030304
fmt.Println(Round(f, 0.0001)) // 12.1581 f = 0.15807659924030304
fmt.Println(Round(f, 0.0001)) // 0.1581 fmt.Println(Round(0.363636, 0.0001)) // 0.3636
fmt.Println(Round(3.232, 0.0001)) // 3.232
fmt.Println(Round(0.4888, 0.0001)) // 0.4888
} func Round(x, unit float64) float64 {
var rounded float64
if x > 0 {
rounded = float64(int64(x/unit+0.5)) * unit
} else {
rounded = float64(int64(x/unit-0.5)) * unit
} var precision int
for unit < 1 {
precision++
unit *= 10
} formatted, err := strconv.ParseFloat(fmt.Sprintf("%."+strconv.Itoa(precision)+"f", rounded), 64)
if err != nil {
return rounded
}
return formatted
}

确实获得了满意的精准度,但是其性能也非常客观,达到了 215ns/op,暂时看来如果追求精度,这个算法目前是比较完美的。

大道至简

很快我发现了另一个极简的算法,它的精度和速度都非常的高,实现还特别精简:

package main

import (
"fmt" "github.com/thinkeridea/go-extend/exmath"
) func main() {
f := 0.15807659924030304
fmt.Println(float64(int64(f*10000+0.5)) / 10000) // 0.1581
}

这并不通用,除非像以下这么包装:

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

unit 参数和之前的概念不同了,保留一位小数 uint =10,只是整数 uint=1, 想对整数部分进行精度控制 uint=0.01 例如: Round(1555.15807659924030304, 0.01) = 1600Round(1555.15807659924030304, 1) = 1555Round(1555.15807659924030304, 10000) = 1555.1581

这似乎就是终极答案了吧,等等……

终极方案

上面的方法够简单,也够高效,但是 api 不太友好,第二个参数不够直观,带了一定的心智负担,其它语言都是传递保留多少位小数,例如 Round(1555.15807659924030304, 0) = 1555Round(1555.15807659924030304, 2) = 1555.16Round(1555.15807659924030304, -2) = 1600,这样的交互才符合人性啊。

别急我在 go-extend 开源了 exmath.Round,其算法符合通用语言 Round 实现,且遵循 Round half up 算法要求,其性能方面在 3.50ns/op, 具体可以参看调优exmath.Round算法, 具体代码如下:

//source: https://github.com/thinkeridea/go-extend/blob/main/exmath/round.go

package exmath

import (
"math"
) // Round 四舍五入,ROUND_HALF_UP 模式实现
// 返回将 val 根据指定精度 precision(十进制小数点后数字的数目)进行四舍五入的结果。precision 也可以是负数或零。
func Round(val float64, precision int) float64 {
p := math.Pow10(precision)
return math.Floor(val*p+0.5) / p
}

总结

Round 功能虽简单,但是受到 float 精度影响,仍然有很多人在四处寻找稳定高效的算法,参阅了大多数资料后精简出 exmath.Round 方法,期望对其他开发者有所帮助,至于其精度使用了大量的测试用例,没有超过 float 精度范围时并没有出现精度问题,未知问题等待社区检验,具体测试用例参见 round_test

转载:

本文作者: 戚银(thinkeridea

本文链接: https://blog.thinkeridea.com/202101/go/round.html

版权声明: 本博客所有文章除特别声明外,均采用 CC BY 4.0 CN协议 许可协议。转载请注明出处!

【Go】四舍五入在go语言中为何如此困难的更多相关文章

  1. C语言中内存分配那些事儿

    C程序的内存结构 C语言的之所以复杂,首先它的内存模型功不可没.不像某些那样的高级语言只需要在使用对象的时候,用new创建.所有之后的事情,你不需要操心.对于C语言,所有与内存相关的东西,都需要熟悉, ...

  2. C语言中最常用的三种输入输出函数scanf()、printf()、getchar()和putchar()

    本文给大家介绍C语言中最常用的三种输入输出函数scanf().printf().getchar()和putchar(). 一.scanf()函数格式化输入函数scanf()的功能是从键盘上输入数据,该 ...

  3. C语言中强制数据类型转换(转)

    原文地址不详 字符型变量的值实质上是一个8位的整数值,因此取值范围一般是-128-127,char型变量也可以加修饰符unsigned,则unsigned char 型变量的取值范围是0-255(有些 ...

  4. C语言中强制类型转换总结

    C语言中强制类型转换总结  ● 字符型变量的值实质上是一个8位的整数值,因此取值范围一般是-128-127,char型变量也可以加修饰符unsigned,则unsigned char 型变量的取值范围 ...

  5. JAVA语言中的修饰符

    JAVA语言中的修饰符 -----------------------------------------------01--------------------------------------- ...

  6. Java语言中的面向对象特性总结

    Java语言中的面向对象特性 (总结得不错) [课前思考]  1. 什么是对象?什么是类?什么是包?什么是接口?什么是内部类?  2. 面向对象编程的特性有哪三个?它们各自又有哪些特性?  3. 你知 ...

  7. python语言中的编码问题

    在编程的过程当中,常常会遇到莫名其妙的乱码问题.很多人选择出了问题直接在网上找答案,把别人的例子照搬过来,这是快速解决问题的一个好办法.然而,作为一个严谨求实的开发者,如果不从源头上彻底理解乱码产生的 ...

  8. 在C语言中利用PCRE实现正则表达式

    1. PCRE简介 2. 正则表达式定义 3. PCRE正则表达式的定义 4. PCRE的函数简介 5. 使用PCRE在C语言中实现正则表达式的解析 6. PCRE函数在C语言中的使用小例子 1. P ...

  9. C语言中函数声明实现的位置

    在学习C语言的时候我遇到了这么个事情,因为之前先学习的C#,在C#编译器中,函数的声明位置不会影响编译的结果,但是在C语言中却发生了错误 先看一段代码: #include <stdio.h> ...

随机推荐

  1. 使用django的用户表进行登录管理

    改写用户基本表 ... AUTH_USER_MODEL = 'appjwt.User' ... setting.py from django.db import models from django. ...

  2. 推荐4个Flutter重磅开源项目

    早上好,骚年,我是小 G,我的公众号「菜鸟翻身」会推荐 GitHub 上有用的项目,一分钟 get 一个优秀的开源项目,挖掘开源的价值,欢迎关注我. 近年来,随着移动智能设备的快速普及,移动多端统一开 ...

  3. caffe源码 全连接层

    图示全连接层 如上图所示,该全链接层输入n * 4,输出为n * 2,n为batch 该层有两个参数W和B,W为系数,B为偏置项 该层的函数为F(x) = W*x + B,则W为4 * 2的矩阵,B ...

  4. nginx转发上传图片接口图片的时候,报错413

    我这边有一个接口是上传图片,使用nginx进行代理,上传大一点的图片,直接调用我的接口不会报错,但是调用nginx上传图片就会报错"413 Request Entity Too Large& ...

  5. git相关操作

    git相关命令 基本操作 git init git add xxx git commit -m "first commit" git tag -a V1.0 -m '我的标签' g ...

  6. 微信小程序日期转换、比较、加减

    直接上干货: 在utils目录下新建一个dateUtil.js,代码如下:(在需要用的地方引入这个js,调用相关方法传入对应参数就可以使用了) 该工具脚本,实用性很高,通用于各类前端项目,熟悉后亦可以 ...

  7. 情话爬虫工具[windows版]

    有没有在气氛暧昧的情况下想说点什么却又无话可说?女朋友有没有抱怨过你,只会写代码,一点都不懂情调?这次,是时候要改变她对你的看法了!一键爬取情话,情话全都躺在txt里面.想怎么玩就怎么玩!张口一句情话 ...

  8. Python 学习笔记 之 01 - 基础总结

    数据类型 整数 十六进制和八进制使用0开头,0x12f, 010 浮点数 可以用科学记数法,如1.23x10^9 可以写成 12.3e8 ,0.000012可以写成 1.2e-5 空值 用None表示 ...

  9. 软件工程与UML的第一次课

    | 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/fzzcxy/2018SE1 | | 这个作业要求在哪里 | https://edu.cnblogs.com ...

  10. JUC(三):JUC包下锁概念

    线程不安全集合类 ArrayList List是线程不安全的集合类,底层是Object数组实现,初始化容量是10(其实是一个空数组,第一次扩容时,将数组扩容为10),其后每次扩容大小为当前容量的一半( ...