VictoriaMetrics/lib/decimal/decimal.go
Roman Khavronenko f5f59896ec
lib/decimal: rename significant decimal digits to significant figures (#698)
The previous notion was inconsistent with what `decimal.Round` does.
According to [wiki](https://en.wikipedia.org/wiki/Significant_figures) rounding
applied to all significant figures, not just decimal ones.
2020-08-16 17:21:35 +03:00

424 lines
8.4 KiB
Go

package decimal
import (
"math"
"sync"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fastnum"
)
// CalibrateScale calibrates a and b with the corresponding exponents ae, be
// and returns the resulting exponent e.
func CalibrateScale(a []int64, ae int16, b []int64, be int16) (e int16) {
if ae == be {
// Fast path - exponents are equal.
return ae
}
if len(a) == 0 {
return be
}
if len(b) == 0 {
return ae
}
if ae < be {
a, b = b, a
ae, be = be, ae
}
upExp := ae - be
downExp := int16(0)
for _, v := range a {
maxUpExp := maxUpExponent(v)
if upExp-maxUpExp > downExp {
downExp = upExp - maxUpExp
}
}
upExp -= downExp
for i, v := range a {
adjExp := upExp
for adjExp > 0 {
v *= 10
adjExp--
}
a[i] = v
}
if downExp > 0 {
for i, v := range b {
adjExp := downExp
for adjExp > 0 {
v /= 10
adjExp--
}
b[i] = v
}
}
return be + downExp
}
// ExtendFloat64sCapacity extends dst capacity to hold additionalItems
// and returns the extended dst.
func ExtendFloat64sCapacity(dst []float64, additionalItems int) []float64 {
dstLen := len(dst)
if n := dstLen + additionalItems - cap(dst); n > 0 {
dst = append(dst[:cap(dst)], make([]float64, n)...)
}
return dst[:dstLen]
}
// ExtendInt64sCapacity extends dst capacity to hold additionalItems
// and returns the extended dst.
func ExtendInt64sCapacity(dst []int64, additionalItems int) []int64 {
dstLen := len(dst)
if n := dstLen + additionalItems - cap(dst); n > 0 {
dst = append(dst[:cap(dst)], make([]int64, n)...)
}
return dst[:dstLen]
}
// AppendDecimalToFloat converts each item in va to f=v*10^e, appends it
// to dst and returns the resulting dst.
func AppendDecimalToFloat(dst []float64, va []int64, e int16) []float64 {
// Extend dst capacity in order to eliminate memory allocations below.
dst = ExtendFloat64sCapacity(dst, len(va))
if fastnum.IsInt64Zeros(va) {
return fastnum.AppendFloat64Zeros(dst, len(va))
}
if e == 0 {
if fastnum.IsInt64Ones(va) {
return fastnum.AppendFloat64Ones(dst, len(va))
}
for _, v := range va {
f := float64(v)
dst = append(dst, f)
}
return dst
}
// increase conversion precision for negative exponents by dividing by e10
if e < 0 {
e10 := math.Pow10(int(-e))
for _, v := range va {
f := float64(v) / e10
dst = append(dst, f)
}
return dst
}
e10 := math.Pow10(int(e))
for _, v := range va {
f := float64(v) * e10
dst = append(dst, f)
}
return dst
}
// AppendFloatToDecimal converts each item in src to v*10^e and appends
// each v to dst returning it as va.
//
// It tries minimizing each item in dst.
func AppendFloatToDecimal(dst []int64, src []float64) (va []int64, e int16) {
if len(src) == 0 {
return dst, 0
}
if fastnum.IsFloat64Zeros(src) {
dst = fastnum.AppendInt64Zeros(dst, len(src))
return dst, 0
}
if fastnum.IsFloat64Ones(src) {
dst = fastnum.AppendInt64Ones(dst, len(src))
return dst, 0
}
// Extend dst capacity in order to eliminate memory allocations below.
dst = ExtendInt64sCapacity(dst, len(src))
vaev := vaeBufPool.Get()
if vaev == nil {
vaev = &vaeBuf{
va: make([]int64, len(src)),
ea: make([]int16, len(src)),
}
}
vae := vaev.(*vaeBuf)
vae.va = vae.va[:0]
vae.ea = vae.ea[:0]
// Determine the minimum exponent across all src items.
v, exp := FromFloat(src[0])
vae.va = append(vae.va, v)
vae.ea = append(vae.ea, exp)
minExp := exp
for _, f := range src[1:] {
v, exp := FromFloat(f)
vae.va = append(vae.va, v)
vae.ea = append(vae.ea, exp)
if exp < minExp {
minExp = exp
}
}
// Determine whether all the src items may be upscaled to minExp.
// If not, adjust minExp accordingly.
downExp := int16(0)
for i, v := range vae.va {
exp := vae.ea[i]
upExp := exp - minExp
maxUpExp := maxUpExponent(v)
if upExp-maxUpExp > downExp {
downExp = upExp - maxUpExp
}
}
minExp += downExp
// Scale each item in src to minExp and append it to dst.
for i, v := range vae.va {
exp := vae.ea[i]
adjExp := exp - minExp
for adjExp > 0 {
v *= 10
adjExp--
}
for adjExp < 0 {
v /= 10
adjExp++
}
dst = append(dst, v)
}
vaeBufPool.Put(vae)
return dst, minExp
}
type vaeBuf struct {
va []int64
ea []int16
}
var vaeBufPool sync.Pool
func maxUpExponent(v int64) int16 {
if v == 0 {
// Any exponent allowed.
return 1024
}
if v < 0 {
v = -v
}
if v < 0 {
// Handle corner case for v=-1<<63
return 0
}
maxMultiplier := ((1 << 63) - 1) / uint64(v)
switch {
case maxMultiplier >= 1e19:
return 19
case maxMultiplier >= 1e18:
return 18
case maxMultiplier >= 1e17:
return 17
case maxMultiplier >= 1e16:
return 16
case maxMultiplier >= 1e15:
return 15
case maxMultiplier >= 1e14:
return 14
case maxMultiplier >= 1e13:
return 13
case maxMultiplier >= 1e12:
return 12
case maxMultiplier >= 1e11:
return 11
case maxMultiplier >= 1e10:
return 10
case maxMultiplier >= 1e9:
return 9
case maxMultiplier >= 1e8:
return 8
case maxMultiplier >= 1e7:
return 7
case maxMultiplier >= 1e6:
return 6
case maxMultiplier >= 1e5:
return 5
case maxMultiplier >= 1e4:
return 4
case maxMultiplier >= 1e3:
return 3
case maxMultiplier >= 1e2:
return 2
case maxMultiplier >= 1e1:
return 1
default:
return 0
}
}
// Round f to value with the given number of significant figures.
func Round(f float64, digits int) float64 {
if digits <= 0 || digits >= 18 {
return f
}
if math.IsNaN(f) || math.IsInf(f, 0) || f == 0 {
return f
}
n := int64(math.Pow10(digits))
isNegative := f < 0
if isNegative {
f = -f
}
v, e := positiveFloatToDecimal(f)
if v > vMax {
v = vMax
}
var rem int64
for v > n {
rem = v % 10
v /= 10
e++
}
if rem >= 5 {
v++
}
if isNegative {
v = -v
}
return ToFloat(v, e)
}
// ToFloat returns f=v*10^e.
func ToFloat(v int64, e int16) float64 {
f := float64(v)
// increase conversion precision for negative exponents by dividing by e10
if e < 0 {
return f / math.Pow10(int(-e))
}
return f * math.Pow10(int(e))
}
const (
vInfPos = 1<<63 - 1
vInfNeg = -1 << 63
vMax = 1<<63 - 3
vMin = -1<<63 + 1
)
// FromFloat converts f to v*10^e.
//
// It tries minimizing v.
// For instance, for f = -1.234 it returns v = -1234, e = -3.
//
// FromFloat doesn't work properly with NaN values, so don't pass them here.
func FromFloat(f float64) (int64, int16) {
if f == 0 {
return 0, 0
}
if math.IsInf(f, 0) {
return fromFloatInf(f)
}
if f > 0 {
v, e := positiveFloatToDecimal(f)
if v > vMax {
v = vMax
}
return v, e
}
v, e := positiveFloatToDecimal(-f)
v = -v
if v < vMin {
v = vMin
}
return v, e
}
func fromFloatInf(f float64) (int64, int16) {
// Limit infs by max and min values for int64
if math.IsInf(f, 1) {
return vInfPos, 0
}
return vInfNeg, 0
}
func positiveFloatToDecimal(f float64) (int64, int16) {
// There is no need in checking for f == 0, since it should be already checked by the caller.
u := uint64(f)
if float64(u) != f {
return positiveFloatToDecimalSlow(f)
}
// Fast path for integers.
if u < 1<<55 && u%10 != 0 {
return int64(u), 0
}
return getDecimalAndScale(u)
}
func getDecimalAndScale(u uint64) (int64, int16) {
var scale int16
for u >= 1<<55 {
// Remove trailing garbage bits left after float64->uint64 conversion,
// since float64 contains only 53 significant bits.
// See https://en.wikipedia.org/wiki/Double-precision_floating-point_format
u /= 10
scale++
}
if u%10 != 0 {
return int64(u), scale
}
// Minimize v by converting trailing zeros to scale.
u /= 10
scale++
for u != 0 && u%10 == 0 {
u /= 10
scale++
}
return int64(u), scale
}
func positiveFloatToDecimalSlow(f float64) (int64, int16) {
// Slow path for floating point numbers.
var scale int16
prec := conversionPrecision
if f > 1e6 || f < 1e-6 {
// Normalize f, so it is in the small range suitable
// for the next loop.
if f > 1e6 {
// Increase conversion precision for big numbers.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/213
prec = 1e15
}
_, exp := math.Frexp(f)
scale = int16(float64(exp) * (math.Ln2 / math.Ln10))
f *= math.Pow10(-int(scale))
}
// Multiply f by 100 until the fractional part becomes
// too small comparing to integer part.
for f < prec {
x, frac := math.Modf(f)
if frac*prec < x {
f = x
break
}
if (1-frac)*prec < x {
f = x + 1
break
}
f *= 100
scale -= 2
}
u := uint64(f)
if u%10 != 0 {
return int64(u), scale
}
// Minimize u by converting trailing zero to scale.
u /= 10
scale++
return int64(u), scale
}
const conversionPrecision = 1e12