quantity: Allow quantity to be converted to float64

Allow a fast approximate conversion to float64 from quantity (which
is an arbitrary precision integral value). Will return +Inf/-Inf if
a float64 is not sufficient to represent the current value.
This commit is contained in:
Clayton Coleman 2020-09-22 17:16:20 -04:00
parent f4a156eb29
commit c06c1121d1
No known key found for this signature in database
GPG Key ID: 3D16906B4F1C5CB3
2 changed files with 116 additions and 0 deletions

View File

@ -20,6 +20,7 @@ import (
"bytes"
"errors"
"fmt"
"math"
"math/big"
"strconv"
"strings"
@ -442,6 +443,36 @@ func (q *Quantity) CanonicalizeBytes(out []byte) (result, suffix []byte) {
}
}
// AsApproximateFloat64 returns a float64 representation of the quantity which may
// lose precision. If the value of the quantity is outside the range of a float64
// +Inf/-Inf will be returned.
func (q *Quantity) AsApproximateFloat64() float64 {
var base float64
var exponent int
if q.d.Dec != nil {
base, _ = big.NewFloat(0).SetInt(q.d.Dec.UnscaledBig()).Float64()
exponent = int(-q.d.Dec.Scale())
} else {
base = float64(q.i.value)
exponent = int(q.i.scale)
}
if exponent == 0 {
return base
}
// multiply by the appropriate exponential scale
switch q.Format {
case DecimalExponent, DecimalSI:
return base * math.Pow10(exponent)
default:
// fast path for exponents that can fit in 64 bits
if exponent > 0 && exponent < 7 {
return base * float64(int64(1)<<(exponent*10))
}
return base * math.Pow(2, float64(exponent*10))
}
}
// AsInt64 returns a representation of the current value as an int64 if a fast conversion
// is possible. If false is returned, callers must use the inf.Dec form of this quantity.
func (q *Quantity) AsInt64() (int64, bool) {

View File

@ -18,6 +18,8 @@ package resource
import (
"encoding/json"
"fmt"
"math"
"math/rand"
"strings"
"testing"
@ -1177,6 +1179,77 @@ func TestNegateRoundTrip(t *testing.T) {
}
}
}
func TestQuantityAsApproximateFloat64(t *testing.T) {
table := []struct {
in Quantity
out float64
}{
{decQuantity(0, 0, DecimalSI), 0.0},
{decQuantity(0, 0, DecimalExponent), 0.0},
{decQuantity(0, 0, BinarySI), 0.0},
{decQuantity(1, 0, DecimalSI), 1},
{decQuantity(1, 0, DecimalExponent), 1},
{decQuantity(1, 0, BinarySI), 1},
// Binary suffixes
{decQuantity(1024, 0, BinarySI), 1024},
{decQuantity(8*1024, 0, BinarySI), 8 * 1024},
{decQuantity(7*1024*1024, 0, BinarySI), 7 * 1024 * 1024},
{decQuantity(7*1024*1024, 1, BinarySI), (7 * 1024 * 1024) * 1024},
{decQuantity(7*1024*1024, 4, BinarySI), (7 * 1024 * 1024) * (1024 * 1024 * 1024 * 1024)},
{decQuantity(7*1024*1024, 8, BinarySI), (7 * 1024 * 1024) * (1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024)},
{decQuantity(7*1024*1024, -1, BinarySI), (7 * 1024 * 1024) / float64(1024)},
{decQuantity(7*1024*1024, -8, BinarySI), (7 * 1024 * 1024) / float64(1024*1024*1024*1024*1024*1024*1024*1024)},
{decQuantity(1024, 0, DecimalSI), 1024},
{decQuantity(8*1024, 0, DecimalSI), 8 * 1024},
{decQuantity(7*1024*1024, 0, DecimalSI), 7 * 1024 * 1024},
{decQuantity(7*1024*1024, 1, DecimalSI), (7 * 1024 * 1024) * 10},
{decQuantity(7*1024*1024, 4, DecimalSI), (7 * 1024 * 1024) * 10000},
{decQuantity(7*1024*1024, 8, DecimalSI), (7 * 1024 * 1024) * 100000000},
{decQuantity(7*1024*1024, -1, DecimalSI), (7 * 1024 * 1024) * math.Pow10(-1)}, // '* Pow10' and '/ float(10)' do not round the same way
{decQuantity(7*1024*1024, -8, DecimalSI), (7 * 1024 * 1024) / float64(100000000)},
{decQuantity(1024, 0, DecimalExponent), 1024},
{decQuantity(8*1024, 0, DecimalExponent), 8 * 1024},
{decQuantity(7*1024*1024, 0, DecimalExponent), 7 * 1024 * 1024},
{decQuantity(7*1024*1024, 1, DecimalExponent), (7 * 1024 * 1024) * 10},
{decQuantity(7*1024*1024, 4, DecimalExponent), (7 * 1024 * 1024) * 10000},
{decQuantity(7*1024*1024, 8, DecimalExponent), (7 * 1024 * 1024) * 100000000},
{decQuantity(7*1024*1024, -1, DecimalExponent), (7 * 1024 * 1024) * math.Pow10(-1)}, // '* Pow10' and '/ float(10)' do not round the same way
{decQuantity(7*1024*1024, -8, DecimalExponent), (7 * 1024 * 1024) / float64(100000000)},
// very large numbers
{Quantity{d: maxAllowed, Format: DecimalSI}, math.MaxInt64},
{Quantity{d: maxAllowed, Format: BinarySI}, math.MaxInt64},
{decQuantity(12, 18, DecimalSI), 1.2e19},
// infinities caused due to float64 overflow
{decQuantity(12, 500, DecimalSI), math.Inf(0)},
{decQuantity(-12, 500, DecimalSI), math.Inf(-1)},
}
for _, item := range table {
t.Run(fmt.Sprintf("%s %s", item.in.Format, item.in.String()), func(t *testing.T) {
out := item.in.AsApproximateFloat64()
if out != item.out {
t.Fatalf("expected %v, got %v", item.out, out)
}
if item.in.d.Dec != nil {
if i, ok := item.in.AsInt64(); ok {
q := intQuantity(i, 0, item.in.Format)
out := q.AsApproximateFloat64()
if out != item.out {
t.Fatalf("as int quantity: expected %v, got %v", item.out, out)
}
}
}
})
}
}
func benchmarkQuantities() []Quantity {
return []Quantity{
intQuantity(1024*1024*1024, 0, BinarySI),
@ -1346,3 +1419,15 @@ func BenchmarkQuantityCmp(b *testing.B) {
}
b.StopTimer()
}
func BenchmarkQuantityAsApproximateFloat64(b *testing.B) {
values := benchmarkQuantities()
b.ResetTimer()
for i := 0; i < b.N; i++ {
q := values[i%len(values)]
if q.AsApproximateFloat64() == -1 {
b.Fatal(q)
}
}
b.StopTimer()
}