strconv.FormatFloat的精度处理问题

前言

先从一个strconv.FormatFloat的例子说起,你知道以下输出的结果吗?

    fmt.Println(strconv.FormatFloat(0.115, 'f', 2, 64))
	fmt.Println(strconv.FormatFloat(0.125, 'f', 2, 64))
    fmt.Println(strconv.FormatFloat(0.1250, 'f', 2, 64))
    fmt.Println(strconv.FormatFloat(0.1251, 'f', 2, 64))

输出的结果依次是:

0.12
0.12
0.12
0.13

从结果来看,strconv.FormatFloat并不完全符合四舍五入,所以其控制精度的规则是什么呢?

更多内容分享,欢迎关注公众号:Go开发笔记

小数的二进制表示问题

我们知道,当前计算机几乎只能处理二进制的数据,非二进制数据需要转为二进制才能处理。才能进行。一个十进制的整数,可以通过除2取余,逆序排列(用2去除十进制整数,可以得到一个商和余数;再用2去除商,又会得到一个商和余数,如此进行,直到商为零时为止,然后把先得到的余数作为二进制数的低位有效位,后得到的余数作为二进制数的高位有效位,依次排列起来)的方式得到其二进制形式。

那么十进制的小数怎么转成二进制?
与整数的转换方式相反,乘2取整,顺序排列(用2乘十进制小数,得到积,将积的整数部分取出,再用2乘余下的小数部分,得到积,再将积的整数部分取出,如此进行,直到积中的小数部分为零,或者达到所要求的精度为止。然后把取出的整数部分按顺序排列起来,先取的整数作为二进制小数的高位有效位,后取的整数作为低位有效位)即可得到小数部分的二进制形式。

不同的是,除2取余,必然可以结束,即整数的二进制必然是精确的。乘2则不一定,比如0.2,其可以一直进行下去。也就是说,小数的二进制表示有时是不可能精确的。目前表示小数的类型有float32float64,均只能保存一定长度的数据。因此,只能截取有限的部分保存。

根据小数的二进制的计算方式,可以知道小数末尾的0对结果无影响。

根据IEEE 754的规定,一个浮点数值可以由以下三部分表示:

符号 指数部分 尾数部分

go中float的结构如下:

type floatInfo struct {
	mantbits uint
	expbits  uint
	bias     int
}
var float32info = floatInfo{23, 8, -127}
var float64info = floatInfo{52, 11, -1023}

因此,float32只能保存23位,float64只能保存52位。考虑到数据转换回的精度问题,两者支持的最大精度为6-7位和15-16位。

类型指数长度尾数长度有效位字节数
float328236-74
float64115215-168

FormatFloat

decimal

小数的数据结构如下:

type decimal struct {
	d     [800]byte // digits, big-endian representation
	nd    int       // number of digits used
	dp    int       // decimal point
	neg   bool      // negative flag
	trunc bool      // discarded nonzero digits beyond d[:nd]
}
参数名参数说明
d存储小数的每一位数,此处的d存入的是计算后的数据,是包含数据整数及小数的近似值
nd小数的位数,末尾的0不计算在内
dp小数点的位置
neg是否负数
tranc是否丢弃nd之后的数据

数据转换

  1. 二进制数据转为uint64

获取二进制形式值对应的uint64,计算符号位、指数、尾数

func genericFtoa(dst []byte, val float64, fmt byte, prec, bitSize int) []byte {
	var bits uint64
	var flt *floatInfo
	switch bitSize {
	case 32:
		bits = uint64(math.Float32bits(float32(val)))
		flt = &float32info
	case 64:
		bits = math.Float64bits(val)
		flt = &float64info
	default:
		panic("strconv: illegal AppendFloat/FormatFloat bitSize")
	}

	neg := bits>>(flt.expbits+flt.mantbits) != 0
	exp := int(bits>>flt.mantbits) & (1<<flt.expbits - 1)
	mant := bits & (uint64(1)<<flt.mantbits - 1)

	...
	if !ok {
		return bigFtoa(dst, prec, fmt, neg, mant, exp, flt)
	}
	return formatDigits(dst, shortest, neg, digs, prec, fmt)
}
  1. 将指数、尾数部分转为[]byte

将尾数按位顺序存入d.d,然后根据指数右移,计算数据,更新d

func bigFtoa(dst []byte, prec int, fmt byte, neg bool, mant uint64, exp int, flt *floatInfo) []byte {
	d := new(decimal)
	d.Assign(mant)
    d.Shift(exp - int(flt.mantbits))
    ...
}
// Assign v to a.
func (a *decimal) Assign(v uint64) {
	var buf [24]byte

	// Write reversed decimal in buf.
	n := 0
	for v > 0 {
		v1 := v / 10
		v -= 10 * v1
		buf[n] = byte(v + '0')
		n++
		v = v1
	}

	// Reverse again to produce forward decimal in a.d.
	a.nd = 0
	for n--; n >= 0; n-- {
		a.d[a.nd] = buf[n]
		a.nd++
	}
	a.dp = a.nd
	trim(a)
}
// Binary shift left (k > 0) or right (k < 0).
func (a *decimal) Shift(k int) {
	switch {
	case a.nd == 0:
		// nothing to do: a == 0
	case k > 0:
		for k > maxShift {
			leftShift(a, maxShift)
			k -= maxShift
		}
		leftShift(a, uint(k))
	case k < 0:
		for k < -maxShift {
			rightShift(a, maxShift)
			k += maxShift
		}
		rightShift(a, uint(-k))
	}
}

bigFtoa

从注释中可以看出,对于shortestprec<0)的值取得是最小长度的精度,其他取得是round值。

// bigFtoa uses multiprecision computations to format a float.
func bigFtoa(dst []byte, prec int, fmt byte, neg bool, mant uint64, exp int, flt *floatInfo) []byte {
    ...
	var digs decimalSlice
	shortest := prec < 0
	if shortest {
		roundShortest(d, mant, exp, flt)
		digs = decimalSlice{d: d.d[:], nd: d.nd, dp: d.dp}
		// Precision for shortest representation mode.
		switch fmt {
            ...
		case 'f':
            prec = max(digs.nd-digs.dp, 0)
            ...
		}
	} else {
		// Round appropriately.
		switch fmt {
            ...
		case 'f':
            d.Round(d.dp + prec)
            ...
		}
		digs = decimalSlice{d: d.d[:], nd: d.nd, dp: d.dp}
	}
	return formatDigits(dst, shortest, neg, digs, prec, fmt)
}

Round

舍入得规则如下:

先判断是否可以入,不能入则舍。

// Round a to nd digits (or fewer).
// If nd is zero, it means we're rounding
// just to the left of the digits, as in
// 0.09 -> 0.1.
func (a *decimal) Round(nd int) {
	if nd < 0 || nd >= a.nd {
		return
	}
	if shouldRoundUp(a, nd) {
		a.RoundUp(nd)
	} else {
		a.RoundDown(nd)
	}
}

具体的判断过程:

// If we chop a at nd digits, should we round up?
func shouldRoundUp(a *decimal, nd int) bool {
	if nd < 0 || nd >= a.nd {
		return false
	}
	if a.d[nd] == '5' && nd+1 == a.nd { // exactly halfway - round to even
		// if we truncated, a little higher than what's recorded - always round up
		if a.trunc {
			return true
		}
		return nd > 0 && (a.d[nd-1]-'0')%2 != 0
	}
	// not halfway - digit tells all
	return a.d[nd] >= '5'
}
  • 如果需要小数长度>=当前小数的长度,即精度超出当前范围,直接舍。
  • 如果需要的精度后的一位对应的数是5,且小数后只剩一位,则采用偶数舍入,向离的最近的偶数舍入
    • 若丢弃后续的小数位,则一直五入
    • 精度>0,且小数精度对应的位为奇数,则五入
    • 否则五舍
  • 否则,执行四舍五入

结论

strconv.FormatFloat尾数舍入位为5采用的是偶数舍入(向离的最近的偶数舍入),否则采用的是四舍五入
不过这是建立在float的尾数基础上的,单纯从小数形式难以直接看出结果。这个问题的根源仍在于有的小数无法用二进制精确的表示出来,只能用近似值表示,在处理中就需要注意精度问题。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值