Add an int64 quantity implementation

Provides fast transformations of int64 scaled values with overflow to
inf.Dec as needed. Attempts to reduce allocations and expensive
operations in the critical path.

Alters the public signature of Quantity to encourage more efficient use
This commit is contained in:
Clayton Coleman 2016-05-17 00:35:40 -04:00
parent c63ac4e664
commit b2a01d4d94
No known key found for this signature in database
GPG Key ID: 3D16906B4F1C5CB3
11 changed files with 2364 additions and 952 deletions

298
pkg/api/resource/amount.go Normal file
View File

@ -0,0 +1,298 @@
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resource
import (
"math/big"
"strconv"
inf "gopkg.in/inf.v0"
)
// Scale is used for getting and setting the base-10 scaled value.
// Base-2 scales are omitted for mathematical simplicity.
// See Quantity.ScaledValue for more details.
type Scale int32
// infScale adapts a Scale value to an inf.Scale value.
func (s Scale) infScale() inf.Scale {
return inf.Scale(-s) // inf.Scale is upside-down
}
const (
Nano Scale = -9
Micro Scale = -6
Milli Scale = -3
Kilo Scale = 3
Mega Scale = 6
Giga Scale = 9
Tera Scale = 12
Peta Scale = 15
Exa Scale = 18
)
var (
Zero = int64Amount{}
// Used by quantity strings - treat as read only
zeroBytes = []byte("0")
)
// int64Amount represents a fixed precision numerator and arbitary scale exponent. It is faster
// than operations on inf.Dec for values that can be represented as int64.
type int64Amount struct {
value int64
scale Scale
}
// Sign returns 0 if the value is zero, -1 if it is less than 0, or 1 if it is greater than 0.
func (a int64Amount) Sign() int {
switch {
case a.value == 0:
return 0
case a.value > 0:
return 1
default:
return -1
}
}
// AsInt64 returns the current amount as an int64 at scale 0, or false if the value cannot be
// represented in an int64 OR would result in a loss of precision. This method is intended as
// an optimization to avoid calling AsDec.
func (a int64Amount) AsInt64() (int64, bool) {
if a.scale == 0 {
return a.value, true
}
if a.scale < 0 {
// TODO: attempt to reduce factors, although it is assumed that factors are reduced prior
// to the int64Amount being created.
return 0, false
}
return positiveScaleInt64(a.value, a.scale)
}
// AsScaledInt64 returns an int64 representing the value of this amount at the specified scale,
// rounding up, or false if that would result in overflow. (1e20).AsScaledInt64(1) would result
// in overflow because 1e19 is not representable as an int64. Note that setting a scale larger
// than the current value may result in loss of precision - i.e. (1e-6).AsScaledInt64(0) would
// return 1, because 0.000001 is rounded up to 1.
func (a int64Amount) AsScaledInt64(scale Scale) (result int64, ok bool) {
if a.scale < scale {
result, _ = negativeScaleInt64(a.value, scale-a.scale)
return result, true
}
return positiveScaleInt64(a.value, a.scale-scale)
}
// AsDec returns an inf.Dec representation of this value.
func (a int64Amount) AsDec() *inf.Dec {
var base inf.Dec
base.SetUnscaled(a.value)
base.SetScale(inf.Scale(-a.scale))
return &base
}
// Cmp returns 0 if a and b are equal, 1 if a is greater than b, or -1 if a is less than b.
func (a int64Amount) Cmp(b int64Amount) int {
switch {
case a.scale == b.scale:
// compare only the unscaled portion
case a.scale > b.scale:
result, remainder, exact := divideByScaleInt64(b.value, a.scale-b.scale)
if !exact {
return a.AsDec().Cmp(b.AsDec())
}
if result == a.value {
switch {
case remainder == 0:
return 0
case remainder > 0:
return -1
default:
return 1
}
}
b.value = result
default:
result, remainder, exact := divideByScaleInt64(a.value, b.scale-a.scale)
if !exact {
return a.AsDec().Cmp(b.AsDec())
}
if result == b.value {
switch {
case remainder == 0:
return 0
case remainder > 0:
return 1
default:
return -1
}
}
a.value = result
}
switch {
case a.value == b.value:
return 0
case a.value < b.value:
return -1
default:
return 1
}
}
// Add adds two int64Amounts together, matching scales. It will return false and not mutate
// a if overflow or underflow would result.
func (a *int64Amount) Add(b int64Amount) bool {
switch {
case b.value == 0:
return true
case a.value == 0:
a.value = b.value
a.scale = b.scale
return true
case a.scale == b.scale:
c, ok := int64Add(a.value, b.value)
if !ok {
return false
}
a.value = c
case a.scale > b.scale:
c, ok := positiveScaleInt64(a.value, a.scale-b.scale)
if !ok {
return false
}
c, ok = int64Add(c, b.value)
if !ok {
return false
}
a.scale = b.scale
a.value = c
default:
c, ok := positiveScaleInt64(b.value, b.scale-a.scale)
if !ok {
return false
}
c, ok = int64Add(a.value, c)
if !ok {
return false
}
a.value = c
}
return true
}
// Sub removes the value of b from the current amount, or returns false if underflow would result.
func (a *int64Amount) Sub(b int64Amount) bool {
return a.Add(int64Amount{value: -b.value, scale: b.scale})
}
// AsScale adjusts this amount to set a minimum scale, rounding up, and returns true iff no precision
// was lost. (1.1e5).AsScale(5) would return 1.1e5, but (1.1e5).AsScale(6) would return 1e6.
func (a int64Amount) AsScale(scale Scale) (int64Amount, bool) {
if a.scale >= scale {
return a, true
}
result, exact := negativeScaleInt64(a.value, scale-a.scale)
return int64Amount{value: result, scale: scale}, exact
}
// AsCanonicalBytes accepts a buffer to write the base-10 string value of this field to, and returns
// either that buffer or a larger buffer and the current exponent of the value. The value is adjusted
// until the exponent is a multiple of 3 - i.e. 1.1e5 would return "110", 3.
func (a int64Amount) AsCanonicalBytes(out []byte) (result []byte, exponent int32) {
mantissa := a.value
exponent = int32(a.scale)
amount, times := removeInt64Factors(mantissa, 10)
exponent += int32(times)
// make sure exponent is a multiple of 3
var ok bool
switch exponent % 3 {
case 1, -2:
amount, ok = int64MultiplyScale10(amount)
if !ok {
return infDecAmount{a.AsDec()}.AsCanonicalBytes(out)
}
exponent = exponent - 1
case 2, -1:
amount, ok = int64MultiplyScale100(amount)
if !ok {
return infDecAmount{a.AsDec()}.AsCanonicalBytes(out)
}
exponent = exponent - 2
}
return strconv.AppendInt(out, amount, 10), exponent
}
// AsCanonicalBase1024Bytes accepts a buffer to write the base-1024 string value of this field to, and returns
// either that buffer or a larger buffer and the current exponent of the value. 2048 is 2 * 1024 ^ 1 and would
// return []byte("2048"), 1.
func (a int64Amount) AsCanonicalBase1024Bytes(out []byte) (result []byte, exponent int32) {
value, ok := a.AsScaledInt64(0)
if !ok {
return infDecAmount{a.AsDec()}.AsCanonicalBase1024Bytes(out)
}
amount, exponent := removeInt64Factors(value, 1024)
return strconv.AppendInt(out, amount, 10), exponent
}
// infDecAmount implements common operations over an inf.Dec that are specific to the quantity
// representation.
type infDecAmount struct {
*inf.Dec
}
// AsScale adjusts this amount to set a minimum scale, rounding up, and returns true iff no precision
// was lost. (1.1e5).AsScale(5) would return 1.1e5, but (1.1e5).AsScale(6) would return 1e6.
func (a infDecAmount) AsScale(scale Scale) (infDecAmount, bool) {
tmp := &inf.Dec{}
tmp.Round(a.Dec, scale.infScale(), inf.RoundUp)
return infDecAmount{tmp}, tmp.Cmp(a.Dec) == 0
}
// AsCanonicalBytes accepts a buffer to write the base-10 string value of this field to, and returns
// either that buffer or a larger buffer and the current exponent of the value. The value is adjusted
// until the exponent is a multiple of 3 - i.e. 1.1e5 would return "110", 3.
func (a infDecAmount) AsCanonicalBytes(out []byte) (result []byte, exponent int32) {
mantissa := a.Dec.UnscaledBig()
exponent = int32(-a.Dec.Scale())
amount := big.NewInt(0).Set(mantissa)
// move all factors of 10 into the exponent for easy reasoning
amount, times := removeBigIntFactors(amount, bigTen)
exponent += times
// make sure exponent is a multiple of 3
for exponent%3 != 0 {
amount.Mul(amount, bigTen)
exponent--
}
return append(out, amount.String()...), exponent
}
// AsCanonicalBase1024Bytes accepts a buffer to write the base-1024 string value of this field to, and returns
// either that buffer or a larger buffer and the current exponent of the value. 2048 is 2 * 1024 ^ 1 and would
// return []byte("2048"), 1.
func (a infDecAmount) AsCanonicalBase1024Bytes(out []byte) (result []byte, exponent int32) {
tmp := &inf.Dec{}
tmp.Round(a.Dec, 0, inf.RoundUp)
amount, exponent := removeBigIntFactors(tmp.UnscaledBig(), big1024)
return append(out, amount.String()...), exponent
}

View File

@ -0,0 +1,111 @@
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resource
import (
"testing"
)
func TestInt64AmountAsInt64(t *testing.T) {
for _, test := range []struct {
value int64
scale Scale
result int64
ok bool
}{
{100, 0, 100, true},
{100, 1, 1000, true},
{100, -5, 0, false},
{100, 100, 0, false},
} {
r, ok := int64Amount{value: test.value, scale: test.scale}.AsInt64()
if r != test.result {
t.Errorf("%v: unexpected result: %d", test, r)
}
if ok != test.ok {
t.Errorf("%v: unexpected ok: %t", test, ok)
}
}
}
func TestInt64AmountAdd(t *testing.T) {
for _, test := range []struct {
a, b, c int64Amount
ok bool
}{
{int64Amount{value: 100, scale: 1}, int64Amount{value: 10, scale: 2}, int64Amount{value: 200, scale: 1}, true},
{int64Amount{value: 100, scale: 1}, int64Amount{value: 1, scale: 2}, int64Amount{value: 110, scale: 1}, true},
{int64Amount{value: 100, scale: 1}, int64Amount{value: 1, scale: 100}, int64Amount{value: 1, scale: 100}, false},
{int64Amount{value: -5, scale: 2}, int64Amount{value: 50, scale: 1}, int64Amount{value: 0, scale: 1}, true},
{int64Amount{value: -5, scale: 2}, int64Amount{value: 5, scale: 2}, int64Amount{value: 0, scale: 2}, true},
{int64Amount{value: mostPositive, scale: -1}, int64Amount{value: 1, scale: -1}, int64Amount{value: 0, scale: -1}, false},
{int64Amount{value: mostPositive, scale: -1}, int64Amount{value: 0, scale: -1}, int64Amount{value: mostPositive, scale: -1}, true},
{int64Amount{value: mostPositive / 10, scale: 1}, int64Amount{value: 10, scale: 0}, int64Amount{value: mostPositive, scale: -1}, false},
} {
c := test.a
ok := c.Add(test.b)
if ok != test.ok {
t.Errorf("%v: unexpected ok: %t", test, ok)
}
if ok {
if c != test.c {
t.Errorf("%v: unexpected result: %d", test, c)
}
} else {
if c != test.a {
t.Errorf("%v: overflow addition mutated source: %d", test, c)
}
}
// addition is commutative
c = test.b
if ok := c.Add(test.a); ok != test.ok {
t.Errorf("%v: unexpected ok: %t", test, ok)
}
if ok {
if c != test.c {
t.Errorf("%v: unexpected result: %d", test, c)
}
} else {
if c != test.b {
t.Errorf("%v: overflow addition mutated source: %d", test, c)
}
}
}
}
func TestInt64AsCanonicalString(t *testing.T) {
for _, test := range []struct {
value int64
scale Scale
result string
exponent int32
}{
{100, 0, "100", 0},
{100, 1, "1", 3},
{100, -1, "10", 0},
{10800, -10, "1080", -9},
} {
r, exp := int64Amount{value: test.value, scale: test.scale}.AsCanonicalBytes(nil)
if string(r) != test.result {
t.Errorf("%v: unexpected result: %s", test, r)
}
if exp != test.exponent {
t.Errorf("%v: unexpected exponent: %d", test, exp)
}
}
}

View File

@ -21,35 +21,27 @@ limitations under the License.
package resource package resource
import ( import (
inf_v0 "gopkg.in/inf.v0"
conversion "k8s.io/kubernetes/pkg/conversion" conversion "k8s.io/kubernetes/pkg/conversion"
) )
func DeepCopy_resource_Quantity(in Quantity, out *Quantity, c *conversion.Cloner) error { func DeepCopy_resource_Quantity(in Quantity, out *Quantity, c *conversion.Cloner) error {
if in.Amount != nil { if newVal, err := c.DeepCopy(in.i); err != nil {
in, out := in.Amount, &out.Amount
*out = new(inf_v0.Dec)
if newVal, err := c.DeepCopy(*in); err != nil {
return err return err
} else { } else {
**out = newVal.(inf_v0.Dec) out.i = newVal.(int64Amount)
} }
if newVal, err := c.DeepCopy(in.d); err != nil {
return err
} else { } else {
out.Amount = nil out.d = newVal.(infDecAmount)
} }
out.Format = in.Format if in.s != nil {
return nil in, out := in.s, &out.s
}
func DeepCopy_resource_QuantityProto(in QuantityProto, out *QuantityProto, c *conversion.Cloner) error {
out.Format = in.Format
out.Scale = in.Scale
if in.Bigint != nil {
in, out := in.Bigint, &out.Bigint
*out = make([]byte, len(in)) *out = make([]byte, len(in))
copy(*out, in) copy(*out, in)
} else { } else {
out.Bigint = nil out.s = nil
} }
out.Format = in.Format
return nil return nil
} }

View File

@ -26,7 +26,6 @@ limitations under the License.
It has these top-level messages: It has these top-level messages:
Quantity Quantity
QuantityProto
*/ */
package resource package resource
@ -34,8 +33,6 @@ import proto "github.com/gogo/protobuf/proto"
import fmt "fmt" import fmt "fmt"
import math "math" import math "math"
import io "io"
// Reference imports to suppress errors if they are not otherwise used. // Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal var _ = proto.Marshal
var _ = fmt.Errorf var _ = fmt.Errorf
@ -44,328 +41,6 @@ var _ = math.Inf
func (m *Quantity) Reset() { *m = Quantity{} } func (m *Quantity) Reset() { *m = Quantity{} }
func (*Quantity) ProtoMessage() {} func (*Quantity) ProtoMessage() {}
func (m *QuantityProto) Reset() { *m = QuantityProto{} }
func (m *QuantityProto) String() string { return proto.CompactTextString(m) }
func (*QuantityProto) ProtoMessage() {}
func init() { func init() {
proto.RegisterType((*Quantity)(nil), "k8s.io.kubernetes.pkg.api.resource.Quantity") proto.RegisterType((*Quantity)(nil), "k8s.io.kubernetes.pkg.api.resource.Quantity")
proto.RegisterType((*QuantityProto)(nil), "k8s.io.kubernetes.pkg.api.resource.QuantityProto")
} }
func (m *QuantityProto) Marshal() (data []byte, err error) {
size := m.Size()
data = make([]byte, size)
n, err := m.MarshalTo(data)
if err != nil {
return nil, err
}
return data[:n], nil
}
func (m *QuantityProto) MarshalTo(data []byte) (int, error) {
var i int
_ = i
var l int
_ = l
data[i] = 0xa
i++
i = encodeVarintGenerated(data, i, uint64(len(m.Format)))
i += copy(data[i:], m.Format)
data[i] = 0x10
i++
i = encodeVarintGenerated(data, i, uint64(m.Scale))
if m.Bigint != nil {
data[i] = 0x1a
i++
i = encodeVarintGenerated(data, i, uint64(len(m.Bigint)))
i += copy(data[i:], m.Bigint)
}
return i, nil
}
func encodeFixed64Generated(data []byte, offset int, v uint64) int {
data[offset] = uint8(v)
data[offset+1] = uint8(v >> 8)
data[offset+2] = uint8(v >> 16)
data[offset+3] = uint8(v >> 24)
data[offset+4] = uint8(v >> 32)
data[offset+5] = uint8(v >> 40)
data[offset+6] = uint8(v >> 48)
data[offset+7] = uint8(v >> 56)
return offset + 8
}
func encodeFixed32Generated(data []byte, offset int, v uint32) int {
data[offset] = uint8(v)
data[offset+1] = uint8(v >> 8)
data[offset+2] = uint8(v >> 16)
data[offset+3] = uint8(v >> 24)
return offset + 4
}
func encodeVarintGenerated(data []byte, offset int, v uint64) int {
for v >= 1<<7 {
data[offset] = uint8(v&0x7f | 0x80)
v >>= 7
offset++
}
data[offset] = uint8(v)
return offset + 1
}
func (m *QuantityProto) Size() (n int) {
var l int
_ = l
l = len(m.Format)
n += 1 + l + sovGenerated(uint64(l))
n += 1 + sovGenerated(uint64(m.Scale))
if m.Bigint != nil {
l = len(m.Bigint)
n += 1 + l + sovGenerated(uint64(l))
}
return n
}
func sovGenerated(x uint64) (n int) {
for {
n++
x >>= 7
if x == 0 {
break
}
}
return n
}
func sozGenerated(x uint64) (n int) {
return sovGenerated(uint64((x << 1) ^ uint64((int64(x) >> 63))))
}
func (m *QuantityProto) Unmarshal(data []byte) error {
l := len(data)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := data[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: QuantityProto: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: QuantityProto: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Format", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := data[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthGenerated
}
postIndex := iNdEx + intStringLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Format = Format(data[iNdEx:postIndex])
iNdEx = postIndex
case 2:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Scale", wireType)
}
m.Scale = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := data[iNdEx]
iNdEx++
m.Scale |= (int32(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
case 3:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Bigint", wireType)
}
var byteLen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := data[iNdEx]
iNdEx++
byteLen |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
if byteLen < 0 {
return ErrInvalidLengthGenerated
}
postIndex := iNdEx + byteLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Bigint = append(m.Bigint[:0], data[iNdEx:postIndex]...)
if m.Bigint == nil {
m.Bigint = []byte{}
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipGenerated(data[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthGenerated
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func skipGenerated(data []byte) (n int, err error) {
l := len(data)
iNdEx := 0
for iNdEx < l {
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowGenerated
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := data[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
wireType := int(wire & 0x7)
switch wireType {
case 0:
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowGenerated
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
iNdEx++
if data[iNdEx-1] < 0x80 {
break
}
}
return iNdEx, nil
case 1:
iNdEx += 8
return iNdEx, nil
case 2:
var length int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowGenerated
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := data[iNdEx]
iNdEx++
length |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
iNdEx += length
if length < 0 {
return 0, ErrInvalidLengthGenerated
}
return iNdEx, nil
case 3:
for {
var innerWire uint64
var start int = iNdEx
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowGenerated
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := data[iNdEx]
iNdEx++
innerWire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
innerWireType := int(innerWire & 0x7)
if innerWireType == 4 {
break
}
next, err := skipGenerated(data[start:])
if err != nil {
return 0, err
}
iNdEx = start + next
}
return iNdEx, nil
case 4:
return iNdEx, nil
case 5:
iNdEx += 4
return iNdEx, nil
default:
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
}
}
panic("unreachable")
}
var (
ErrInvalidLengthGenerated = fmt.Errorf("proto: negative length found during unmarshaling")
ErrIntOverflowGenerated = fmt.Errorf("proto: integer overflow")
)

View File

@ -84,26 +84,10 @@ option go_package = "resource";
// cause implementors to also use a fixed point implementation. // cause implementors to also use a fixed point implementation.
// //
// +protobuf=true // +protobuf=true
// +protobuf.embed=QuantityProto // +protobuf.embed=string
// +protobuf.options.marshal=false // +protobuf.options.marshal=false
// +protobuf.options.(gogoproto.goproto_stringer)=false // +protobuf.options.(gogoproto.goproto_stringer)=false
message Quantity { message Quantity {
optional QuantityProto QuantityProto = 1; optional string string = 1;
}
// QuantityProto is a struct that is equivalent to Quantity, but intended for
// protobuf marshalling/unmarshalling. It is generated into a serialization
// that matches Quantity. Do not use in Go structs.
//
// +protobuf=true
message QuantityProto {
// The format of the quantity
optional string format = 1;
// The scale dimension of the value
optional int32 scale = 2;
// Bigint is serialized as a raw bytes array
optional bytes bigint = 3;
} }

327
pkg/api/resource/math.go Normal file
View File

@ -0,0 +1,327 @@
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resource
import (
"math/big"
inf "gopkg.in/inf.v0"
)
const (
// maxInt64Factors is the highest value that will be checked when removing factors of 10 from an int64.
// It is also the maximum decimal digits that can be represented with an int64.
maxInt64Factors = 18
)
var (
// Commonly needed big.Int values-- treat as read only!
bigTen = big.NewInt(10)
bigZero = big.NewInt(0)
bigOne = big.NewInt(1)
bigThousand = big.NewInt(1000)
big1024 = big.NewInt(1024)
// Commonly needed inf.Dec values-- treat as read only!
decZero = inf.NewDec(0, 0)
decOne = inf.NewDec(1, 0)
decMinusOne = inf.NewDec(-1, 0)
decThousand = inf.NewDec(1000, 0)
dec1024 = inf.NewDec(1024, 0)
decMinus1024 = inf.NewDec(-1024, 0)
// Largest (in magnitude) number allowed.
maxAllowed = infDecAmount{inf.NewDec((1<<63)-1, 0)} // == max int64
// The maximum value we can represent milli-units for.
// Compare with the return value of Quantity.Value() to
// see if it's safe to use Quantity.MilliValue().
MaxMilliValue = int64(((1 << 63) - 1) / 1000)
)
const mostNegative = -(mostPositive + 1)
const mostPositive = 1<<63 - 1
// int64Add returns a+b, or false if that would overflow int64.
func int64Add(a, b int64) (int64, bool) {
c := a + b
switch {
case a > 0 && b > 0:
if c < 0 {
return 0, false
}
case a < 0 && b < 0:
if c > 0 {
return 0, false
}
if a == mostNegative && b == mostNegative {
return 0, false
}
}
return c, true
}
// int64Multiply returns a*b, or false if that would overflow or underflow int64.
func int64Multiply(a, b int64) (int64, bool) {
if a == 0 || b == 0 || a == 1 || b == 1 {
return a * b, true
}
if a == mostNegative || b == mostNegative {
return 0, false
}
c := a * b
return c, c/b == a
}
// int64MultiplyScale returns a*b, assuming b is greater than one, or false if that would overflow or underflow int64.
// Use when b is known to be greater than one.
func int64MultiplyScale(a int64, b int64) (int64, bool) {
if a == 0 || a == 1 {
return a * b, true
}
if a == mostNegative && b != 1 {
return 0, false
}
c := a * b
return c, c/b == a
}
// int64MultiplyScale10 multiplies a by 10, or returns false if that would overflow. This method is faster than
// int64Multiply(a, 10) because the compiler can optimize constant factor multiplication.
func int64MultiplyScale10(a int64) (int64, bool) {
if a == 0 || a == 1 {
return a * 10, true
}
if a == mostNegative {
return 0, false
}
c := a * 10
return c, c/10 == a
}
// int64MultiplyScale100 multiplies a by 100, or returns false if that would overflow. This method is faster than
// int64Multiply(a, 100) because the compiler can optimize constant factor multiplication.
func int64MultiplyScale100(a int64) (int64, bool) {
if a == 0 || a == 1 {
return a * 100, true
}
if a == mostNegative {
return 0, false
}
c := a * 100
return c, c/100 == a
}
// int64MultiplyScale1000 multiplies a by 1000, or returns false if that would overflow. This method is faster than
// int64Multiply(a, 1000) because the compiler can optimize constant factor multiplication.
func int64MultiplyScale1000(a int64) (int64, bool) {
if a == 0 || a == 1 {
return a * 1000, true
}
if a == mostNegative {
return 0, false
}
c := a * 1000
return c, c/1000 == a
}
// positiveScaleInt64 multiplies base by 10^scale, returning false if the
// value overflows. Passing a negative scale is undefined.
func positiveScaleInt64(base int64, scale Scale) (int64, bool) {
switch scale {
case 0:
return base, true
case 1:
return int64MultiplyScale10(base)
case 2:
return int64MultiplyScale100(base)
case 3:
return int64MultiplyScale1000(base)
case 6:
return int64MultiplyScale(base, 1000000)
case 9:
return int64MultiplyScale(base, 1000000000)
default:
value := base
var ok bool
for i := Scale(0); i < scale; i++ {
if value, ok = int64MultiplyScale(value, 10); !ok {
return 0, false
}
}
return value, true
}
}
// negativeScaleInt64 reduces base by the provided scale, rounding up, until the
// value is zero or the scale is reached. Passing a negative scale is undefined.
// The value returned, if not exact, is rounded away from zero.
func negativeScaleInt64(base int64, scale Scale) (result int64, exact bool) {
if scale == 0 {
return base, true
}
value := base
var fraction bool
for i := Scale(0); i < scale; i++ {
if !fraction && value%10 != 0 {
fraction = true
}
value = value / 10
if value == 0 {
if fraction {
if base > 0 {
return 1, false
}
return -1, false
}
return 0, true
}
}
if fraction {
if base > 0 {
value += 1
} else {
value += -1
}
}
return value, !fraction
}
func pow10Int64(b int64) int64 {
switch b {
case 0:
return 1
case 1:
return 10
case 2:
return 100
case 3:
return 1000
case 4:
return 10000
case 5:
return 100000
case 6:
return 1000000
case 7:
return 10000000
case 8:
return 100000000
case 9:
return 1000000000
case 10:
return 10000000000
case 11:
return 100000000000
case 12:
return 1000000000000
case 13:
return 10000000000000
case 14:
return 100000000000000
case 15:
return 1000000000000000
case 16:
return 10000000000000000
case 17:
return 100000000000000000
case 18:
return 1000000000000000000
default:
return 0
}
}
// powInt64 raises a to the bth power. Is not overflow aware.
func powInt64(a, b int64) int64 {
p := int64(1)
for b > 0 {
if b&1 != 0 {
p *= a
}
b >>= 1
a *= a
}
return p
}
// negativeScaleInt64 returns the result of dividing base by scale * 10 and the remainder, or
// false if no such division is possible. Dividing by negative scales is undefined.
func divideByScaleInt64(base int64, scale Scale) (result, remainder int64, exact bool) {
if scale == 0 {
return base, 0, true
}
// the max scale representable in base 10 in an int64 is 18 decimal places
if scale >= 18 {
return 0, base, false
}
divisor := pow10Int64(int64(scale))
return base / divisor, base % divisor, true
}
// removeInt64Factors divides in a loop; the return values have the property that
// value == result * base ^ scale
func removeInt64Factors(value int64, base int64) (result int64, times int32) {
times = 0
result = value
negative := result < 0
if negative {
result = -result
}
switch base {
// allow the compiler to optimize the common cases
case 10:
for result >= 10 && result%10 == 0 {
times++
result = result / 10
}
// allow the compiler to optimize the common cases
case 1024:
for result >= 1024 && result%1024 == 0 {
times++
result = result / 1024
}
default:
for result >= base && result%base == 0 {
times++
result = result / base
}
}
if negative {
result = -result
}
return result, times
}
// removeBigIntFactors divides in a loop; the return values have the property that
// d == result * factor ^ times
// d may be modified in place.
// If d == 0, then the return values will be (0, 0)
func removeBigIntFactors(d, factor *big.Int) (result *big.Int, times int32) {
q := big.NewInt(0)
m := big.NewInt(0)
for d.Cmp(bigZero) != 0 {
q.DivMod(d, factor, m)
if m.Cmp(bigZero) != 0 {
break
}
times++
d, q = q, d
}
return d, times
}

View File

@ -0,0 +1,211 @@
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resource
import (
"testing"
)
func TestDetectOverflowAdd(t *testing.T) {
for _, test := range []struct {
a, b int64
c int64
ok bool
}{
{0, 0, 0, true},
{-1, 1, 0, true},
{0, 1, 1, true},
{2, 2, 4, true},
{2, -2, 0, true},
{-2, -2, -4, true},
{mostNegative, -1, 0, false},
{mostNegative, 1, mostNegative + 1, true},
{mostPositive, -1, mostPositive - 1, true},
{mostPositive, 1, 0, false},
{mostNegative, mostPositive, -1, true},
{mostPositive, mostNegative, -1, true},
{mostPositive, mostPositive, 0, false},
{mostNegative, mostNegative, 0, false},
{-mostPositive, mostNegative, 0, false},
{mostNegative, -mostPositive, 0, false},
{-mostPositive, -mostPositive, 0, false},
} {
c, ok := int64Add(test.a, test.b)
if c != test.c {
t.Errorf("%v: unexpected result: %d", test, c)
}
if ok != test.ok {
t.Errorf("%v: unexpected overflow: %t", test, ok)
}
// addition is commutative
d, ok2 := int64Add(test.b, test.a)
if c != d || ok != ok2 {
t.Errorf("%v: not commutative: %d %t", test, d, ok2)
}
}
}
func TestDetectOverflowMultiply(t *testing.T) {
for _, test := range []struct {
a, b int64
c int64
ok bool
}{
{0, 0, 0, true},
{-1, 1, -1, true},
{-1, -1, 1, true},
{1, 1, 1, true},
{0, 1, 0, true},
{1, 0, 0, true},
{2, 2, 4, true},
{2, -2, -4, true},
{-2, -2, 4, true},
{mostNegative, -1, 0, false},
{mostNegative, 1, mostNegative, true},
{mostPositive, -1, -mostPositive, true},
{mostPositive, 1, mostPositive, true},
{mostNegative, mostPositive, 0, false},
{mostPositive, mostNegative, 0, false},
{mostPositive, mostPositive, 1, false},
{mostNegative, mostNegative, 0, false},
{-mostPositive, mostNegative, 0, false},
{mostNegative, -mostPositive, 0, false},
{-mostPositive, -mostPositive, 1, false},
} {
c, ok := int64Multiply(test.a, test.b)
if c != test.c {
t.Errorf("%v: unexpected result: %d", test, c)
}
if ok != test.ok {
t.Errorf("%v: unexpected overflow: %t", test, ok)
}
// multiplication is commutative
d, ok2 := int64Multiply(test.b, test.a)
if c != d || ok != ok2 {
t.Errorf("%v: not commutative: %d %t", test, d, ok2)
}
}
}
func TestDetectOverflowScale(t *testing.T) {
for _, a := range []int64{0, -1, 1, 10, -10, mostPositive, mostNegative, -mostPositive} {
for _, b := range []int64{1, 2, 10, 100, 1000, mostPositive} {
expect, expectOk := int64Multiply(a, b)
c, ok := int64MultiplyScale(a, b)
if c != expect {
t.Errorf("%d*%d: unexpected result: %d", a, b, c)
}
if ok != expectOk {
t.Errorf("%d*%d: unexpected overflow: %t", a, b, ok)
}
}
for _, test := range []struct {
base int64
fn func(a int64) (int64, bool)
}{
{10, int64MultiplyScale10},
{100, int64MultiplyScale100},
{1000, int64MultiplyScale1000},
} {
expect, expectOk := int64Multiply(a, test.base)
c, ok := test.fn(a)
if c != expect {
t.Errorf("%d*%d: unexpected result: %d", a, test.base, c)
}
if ok != expectOk {
t.Errorf("%d*%d: unexpected overflow: %t", a, test.base, ok)
}
}
}
}
func TestRemoveInt64Factors(t *testing.T) {
for _, test := range []struct {
value int64
max int64
result int64
scale int32
}{
{100, 10, 1, 2},
{100, 10, 1, 2},
{100, 100, 1, 1},
{1, 10, 1, 0},
} {
r, s := removeInt64Factors(test.value, test.max)
if r != test.result {
t.Errorf("%v: unexpected result: %d", test, r)
}
if s != test.scale {
t.Errorf("%v: unexpected scale: %d", test, s)
}
}
}
func TestNegativeScaleInt64(t *testing.T) {
for _, test := range []struct {
base int64
scale Scale
result int64
exact bool
}{
{1234567, 0, 1234567, true},
{1234567, 1, 123457, false},
{1234567, 2, 12346, false},
{1234567, 3, 1235, false},
{1234567, 4, 124, false},
{-1234567, 0, -1234567, true},
{-1234567, 1, -123457, false},
{-1234567, 2, -12346, false},
{-1234567, 3, -1235, false},
{-1234567, 4, -124, false},
{1000, 0, 1000, true},
{1000, 1, 100, true},
{1000, 2, 10, true},
{1000, 3, 1, true},
{1000, 4, 1, false},
{-1000, 0, -1000, true},
{-1000, 1, -100, true},
{-1000, 2, -10, true},
{-1000, 3, -1, true},
{-1000, 4, -1, false},
{0, 0, 0, true},
{0, 1, 0, true},
{0, 2, 0, true},
// negative scale is undefined behavior
{1000, -1, 1000, true},
} {
result, exact := negativeScaleInt64(test.base, test.scale)
if result != test.result {
t.Errorf("%v: unexpected result: %d", test, result)
}
if exact != test.exact {
t.Errorf("%v: unexpected exact: %t", test, exact)
}
}
}

View File

@ -17,10 +17,12 @@ limitations under the License.
package resource package resource
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"math/big" "math/big"
"regexp" "regexp"
"strconv"
"strings" "strings"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
@ -86,19 +88,34 @@ import (
// cause implementors to also use a fixed point implementation. // cause implementors to also use a fixed point implementation.
// //
// +protobuf=true // +protobuf=true
// +protobuf.embed=QuantityProto // +protobuf.embed=string
// +protobuf.options.marshal=false // +protobuf.options.marshal=false
// +protobuf.options.(gogoproto.goproto_stringer)=false // +protobuf.options.(gogoproto.goproto_stringer)=false
type Quantity struct { type Quantity struct {
// Amount is public, so you can manipulate it if the accessor // i is the quantity in int64 scaled form, if d.Dec == nil
// functions are not sufficient. i int64Amount
Amount *inf.Dec // d is the quantity in inf.Dec form if d.Dec != nil
d infDecAmount
// s is the generated value of this quantity to avoid recalculation
s []byte
// Change Format at will. See the comment for Canonicalize for // Change Format at will. See the comment for Canonicalize for
// more details. // more details.
Format Format
} }
// CanonicalValue allows a quantity amount to be converted to a string.
type CanonicalValue interface {
// AsCanonicalBytes returns a byte array representing the string representation
// of the value mantissa and an int32 representing its exponent in base-10. Callers may
// pass a byte slice to the method to avoid allocations.
AsCanonicalBytes(out []byte) ([]byte, int32)
// AsCanonicalBase1024Bytes returns a byte array representing the string representation
// of the value mantissa and an int32 representing its exponent in base-1024. Callers
// may pass a byte slice to the method to avoid allocations.
AsCanonicalBase1024Bytes(out []byte) ([]byte, int32)
}
// Format lists the three possible formattings of a quantity. // Format lists the three possible formattings of a quantity.
type Format string type Format string
@ -115,26 +132,9 @@ func MustParse(str string) Quantity {
if err != nil { if err != nil {
panic(fmt.Errorf("cannot parse '%v': %v", str, err)) panic(fmt.Errorf("cannot parse '%v': %v", str, err))
} }
return *q return q
} }
// Scale is used for getting and setting the base-10 scaled value.
// Base-2 scales are omitted for mathematical simplicity.
// See Quantity.ScaledValue for more details.
type Scale int
const (
Nano Scale = -9
Micro Scale = -6
Milli Scale = -3
Kilo Scale = 3
Mega Scale = 6
Giga Scale = 9
Tera Scale = 12
Peta Scale = 15
Exa Scale = 18
)
const ( const (
// splitREString is used to separate a number from its suffix; as such, // splitREString is used to separate a number from its suffix; as such,
// this is overly permissive, but that's OK-- it will be checked later. // this is overly permissive, but that's OK-- it will be checked later.
@ -149,47 +149,189 @@ var (
ErrFormatWrong = errors.New("quantities must match the regular expression '" + splitREString + "'") ErrFormatWrong = errors.New("quantities must match the regular expression '" + splitREString + "'")
ErrNumeric = errors.New("unable to parse numeric part of quantity") ErrNumeric = errors.New("unable to parse numeric part of quantity")
ErrSuffix = errors.New("unable to parse quantity's suffix") ErrSuffix = errors.New("unable to parse quantity's suffix")
// Commonly needed big.Int values-- treat as read only!
bigTen = big.NewInt(10)
bigZero = big.NewInt(0)
bigOne = big.NewInt(1)
bigThousand = big.NewInt(1000)
big1024 = big.NewInt(1024)
// Commonly needed inf.Dec values-- treat as read only!
decZero = inf.NewDec(0, 0)
decOne = inf.NewDec(1, 0)
decMinusOne = inf.NewDec(-1, 0)
decThousand = inf.NewDec(1000, 0)
dec1024 = inf.NewDec(1024, 0)
decMinus1024 = inf.NewDec(-1024, 0)
// Largest (in magnitude) number allowed.
maxAllowed = inf.NewDec((1<<63)-1, 0) // == max int64
// The maximum value we can represent milli-units for.
// Compare with the return value of Quantity.Value() to
// see if it's safe to use Quantity.MilliValue().
MaxMilliValue = int64(((1 << 63) - 1) / 1000)
) )
// parseQuantityString is a fast scanner for quantity values.
func parseQuantityString(str string) (positive bool, value, num, denom, suffix string, err error) {
positive = true
pos := 0
end := len(str)
// handle leading sign
if pos < end {
switch str[0] {
case '-':
positive = false
pos++
case '+':
pos++
}
}
// strip leading zeros
Zeroes:
for i := pos; ; i++ {
if i >= end {
num = "0"
value = num
return
}
switch str[i] {
case '0':
pos++
default:
break Zeroes
}
}
// extract the numerator
Num:
for i := pos; ; i++ {
if i >= end {
num = str[pos:end]
value = str[0:end]
return
}
switch str[i] {
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
default:
num = str[pos:i]
pos = i
break Num
}
}
// if we stripped all numerator positions, always return 0
if len(num) == 0 {
num = "0"
}
// handle a denominator
if pos < end && str[pos] == '.' {
pos++
Denom:
for i := pos; ; i++ {
if i >= end {
denom = str[pos:end]
value = str[0:end]
return
}
switch str[i] {
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
default:
denom = str[pos:i]
pos = i
break Denom
}
}
// TODO: we currently allow 1.G, but we may not want to in the future.
// if len(denom) == 0 {
// err = ErrFormatWrong
// return
// }
}
value = str[0:pos]
// grab the elements of the suffix
suffixStart := pos
for i := pos; ; i++ {
if i >= end {
suffix = str[suffixStart:end]
return
}
if !strings.ContainsAny(str[i:i+1], "eEinumkKMGTP") {
pos = i
break
}
}
if pos < end {
switch str[pos] {
case '-', '+':
pos++
}
}
Suffix:
for i := pos; ; i++ {
if i >= end {
suffix = str[suffixStart:end]
return
}
switch str[i] {
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
default:
pos = i
break Suffix
}
}
// we encountered a non decimal in the Suffix loop, but the last character
// was not a valid exponent
err = ErrFormatWrong
return
}
// ParseQuantity turns str into a Quantity, or returns an error. // ParseQuantity turns str into a Quantity, or returns an error.
func ParseQuantity(str string) (*Quantity, error) { func ParseQuantity(str string) (Quantity, error) {
parts := splitRE.FindStringSubmatch(strings.TrimSpace(str)) if len(str) == 0 {
// regexp returns are entire match, followed by an entry for each () section. return Quantity{}, ErrFormatWrong
if len(parts) != 3 { }
return nil, ErrFormatWrong if str == "0" {
return Quantity{Format: DecimalSI}, nil
}
positive, value, num, denom, suf, err := parseQuantityString(str)
if err != nil {
return Quantity{}, err
}
base, exponent, format, ok := quantitySuffixer.interpret(suffix(suf))
if !ok {
return Quantity{}, ErrSuffix
}
precision := int32(0)
scale := int32(0)
mantissa := int64(1)
switch format {
case DecimalExponent, DecimalSI:
scale = exponent
precision = maxInt64Factors - int32(len(num)+len(denom))
case BinarySI:
scale = 0
switch {
case exponent >= 0 && len(denom) == 0:
// only handle positive binary numbers with the fast path
mantissa = int64(int64(mantissa) << uint64(exponent))
// 1Mi (2^20) has ~6 digits of decimal precision, so exponent*3/10 -1 is roughly the precision
precision = 15 - int32(len(num)) - int32(float32(exponent)*3/10) - 1
default:
precision = -1
}
}
if precision >= 0 {
// if we have a denominator, shift the entire value to the left by the number of places in the
// denominator
scale -= int32(len(denom))
if scale >= int32(Nano) {
shifted := num + denom
var value int64
value, err := strconv.ParseInt(shifted, 10, 64)
if err != nil {
return Quantity{}, ErrNumeric
}
if result, ok := int64Multiply(value, int64(mantissa)); ok {
if !positive {
result = -result
}
return Quantity{i: int64Amount{value: result, scale: Scale(scale)}, Format: format}, nil
}
}
} }
amount := new(inf.Dec) amount := new(inf.Dec)
if _, ok := amount.SetString(parts[1]); !ok { if _, ok := amount.SetString(value); !ok {
return nil, ErrNumeric return Quantity{}, ErrNumeric
}
base, exponent, format, ok := quantitySuffixer.interpret(suffix(parts[2]))
if !ok {
return nil, ErrSuffix
} }
// So that no one but us has to think about suffixes, remove it. // So that no one but us has to think about suffixes, remove it.
@ -217,9 +359,11 @@ func ParseQuantity(str string) (*Quantity, error) {
} }
// The max is just a simple cap. // The max is just a simple cap.
if amount.Cmp(maxAllowed) > 0 { // TODO: this prevents accumulating quantities greater than int64, for instance quota across a cluster
amount.Set(maxAllowed) if format == BinarySI && amount.Cmp(maxAllowed.Dec) > 0 {
amount.Set(maxAllowed.Dec)
} }
if format == BinarySI && amount.Cmp(decOne) < 0 && amount.Cmp(decZero) > 0 { if format == BinarySI && amount.Cmp(decOne) < 0 && amount.Cmp(decZero) > 0 {
// This avoids rounding and hopefully confusion, too. // This avoids rounding and hopefully confusion, too.
format = DecimalSI format = DecimalSI
@ -228,25 +372,7 @@ func ParseQuantity(str string) (*Quantity, error) {
amount.Neg(amount) amount.Neg(amount)
} }
return &Quantity{amount, format}, nil return Quantity{d: infDecAmount{amount}, Format: format}, nil
}
// removeFactors divides in a loop; the return values have the property that
// d == result * factor ^ times
// d may be modified in place.
// If d == 0, then the return values will be (0, 0)
func removeFactors(d, factor *big.Int) (result *big.Int, times int) {
q := big.NewInt(0)
m := big.NewInt(0)
for d.Cmp(bigZero) != 0 {
q.DivMod(d, factor, m)
if m.Cmp(bigZero) != 0 {
break
}
times++
d, q = q, d
}
return d, times
} }
// Canonicalize returns the canonical form of q and its suffix (see comment on Quantity). // Canonicalize returns the canonical form of q and its suffix (see comment on Quantity).
@ -256,27 +382,22 @@ func removeFactors(d, factor *big.Int) (result *big.Int, times int) {
// -1 and +1, it will be emitted as if q.Format were DecimalSI. // -1 and +1, it will be emitted as if q.Format were DecimalSI.
// * Otherwise, if q.Format is set to BinarySI, frational parts of q.Amount will be // * Otherwise, if q.Format is set to BinarySI, frational parts of q.Amount will be
// rounded up. (1.1i becomes 2i.) // rounded up. (1.1i becomes 2i.)
func (q *Quantity) Canonicalize() (string, suffix) { func (q *Quantity) CanonicalizeBytes(out []byte) (result, suffix []byte) {
if q.Amount == nil { if q.IsZero() {
return "0", "" return zeroBytes, nil
}
// zero is zero always
if q.Amount.Cmp(&inf.Dec{}) == 0 {
return "0", ""
} }
var rounded CanonicalValue
format := q.Format format := q.Format
switch format { switch format {
case DecimalExponent, DecimalSI: case DecimalExponent, DecimalSI:
case BinarySI: case BinarySI:
if q.Amount.Cmp(decMinus1024) > 0 && q.Amount.Cmp(dec1024) < 0 { if q.CmpInt64(-1024) > 0 && q.CmpInt64(1024) < 0 {
// This avoids rounding and hopefully confusion, too. // This avoids rounding and hopefully confusion, too.
format = DecimalSI format = DecimalSI
} else { } else {
tmp := &inf.Dec{} var exact bool
tmp.Round(q.Amount, 0, inf.RoundUp) if rounded, exact = q.AsScale(0); !exact {
if tmp.Cmp(q.Amount) != 0 {
// Don't lose precision-- show as DecimalSI // Don't lose precision-- show as DecimalSI
format = DecimalSI format = DecimalSI
} }
@ -289,125 +410,223 @@ func (q *Quantity) Canonicalize() (string, suffix) {
// one of the other formats. // one of the other formats.
switch format { switch format {
case DecimalExponent, DecimalSI: case DecimalExponent, DecimalSI:
mantissa := q.Amount.UnscaledBig() number, exponent := q.AsCanonicalBytes(out)
exponent := int(-q.Amount.Scale()) suffix, _ := quantitySuffixer.constructBytes(10, exponent, format)
amount := big.NewInt(0).Set(mantissa)
// move all factors of 10 into the exponent for easy reasoning
amount, times := removeFactors(amount, bigTen)
exponent += times
// make sure exponent is a multiple of 3
for exponent%3 != 0 {
amount.Mul(amount, bigTen)
exponent--
}
suffix, _ := quantitySuffixer.construct(10, exponent, format)
number := amount.String()
return number, suffix return number, suffix
case BinarySI: default:
tmp := &inf.Dec{} // format must be BinarySI
tmp.Round(q.Amount, 0, inf.RoundUp) number, exponent := rounded.AsCanonicalBase1024Bytes(out)
suffix, _ := quantitySuffixer.constructBytes(2, exponent*10, format)
amount, exponent := removeFactors(tmp.UnscaledBig(), big1024)
suffix, _ := quantitySuffixer.construct(2, exponent*10, format)
number := amount.String()
return number, suffix return number, suffix
} }
return "0", ""
} }
// 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) {
if q.d.Dec != nil {
return 0, false
}
return q.i.AsInt64()
}
// ToDec promotes the quantity in place to use an inf.Dec representation and returns itself.
func (q *Quantity) ToDec() *Quantity {
if q.d.Dec == nil {
q.d.Dec = q.i.AsDec()
q.i = int64Amount{}
}
return q
}
// AsDec returns the quantity as represented by a scaled inf.Dec.
func (q *Quantity) AsDec() *inf.Dec {
if q.d.Dec != nil {
return q.d.Dec
}
q.d.Dec = q.i.AsDec()
q.i = int64Amount{}
return q.d.Dec
}
// AsCanonicalBytes returns the canonical byte representation of this quantity as a mantissa
// and base 10 exponent. The out byte slice may be passed to the method to avoid an extra
// allocation.
func (q *Quantity) AsCanonicalBytes(out []byte) (result []byte, exponent int32) {
if q.d.Dec != nil {
return q.d.AsCanonicalBytes(out)
}
return q.i.AsCanonicalBytes(out)
}
// IsZero returns true if the quantity is equal to zero.
func (q *Quantity) IsZero() bool {
if q.d.Dec != nil {
return q.d.Dec.Sign() == 0
}
return q.i.value == 0
}
// Sign returns 0 if the quantity is zero, -1 if the quantity is less than zero, or 1 if the
// quantity is greater than zero.
func (q *Quantity) Sign() int {
if q.d.Dec != nil {
return q.d.Dec.Sign()
}
return q.i.Sign()
}
// AsScaled returns the current value, rounded up to the provided scale, and returns
// false if the scale resulted in a loss of precision.
func (q *Quantity) AsScale(scale Scale) (CanonicalValue, bool) {
if q.d.Dec != nil {
return q.d.AsScale(scale)
}
return q.i.AsScale(scale)
}
// RoundUp updates the quantity to the provided scale, ensuring that the value is at
// least 1. False is returned if the rounding operation resulted in a loss of precision.
// Negative numbers are rounded away from zero (-9 scale 1 rounds to -10).
func (q *Quantity) RoundUp(scale Scale) bool {
if q.d.Dec != nil {
d, exact := q.d.AsScale(scale)
q.d = d
return exact
}
i, exact := q.i.AsScale(scale)
q.i = i
return exact
}
// Add adds the provide y quantity to the current value. If the current value is zero,
// the format of the quantity will be updated to the format of y.
func (q *Quantity) Add(y Quantity) {
q.s = nil
if q.d.Dec == nil && y.d.Dec == nil {
if q.i.value == 0 {
q.Format = y.Format
}
if q.i.Add(y.i) {
return
}
} else if q.IsZero() {
q.Format = y.Format
}
q.ToDec().d.Dec.Add(q.d.Dec, y.AsDec())
}
// Sub subtracts the provided quantity from the current value in place. If the current
// value is zero, the format of the quantity will be updated to the format of y.
func (q *Quantity) Sub(y Quantity) {
q.s = nil
if q.IsZero() {
q.Format = y.Format
}
if q.d.Dec == nil && y.d.Dec == nil && q.i.Sub(y.i) {
return
}
q.ToDec().d.Dec.Sub(q.d.Dec, y.AsDec())
}
// Cmp returns 0 if the quantity is equal to y, -1 if the quantity is less than y, or 1 if the
// quantity is greater than y.
func (q *Quantity) Cmp(y Quantity) int {
if q.d.Dec == nil && y.d.Dec == nil {
return q.i.Cmp(y.i)
}
return q.AsDec().Cmp(y.AsDec())
}
// CmpInt64 returns 0 if the quantity is equal to y, -1 if the quantity is less than y, or 1 if the
// quantity is greater than y.
func (q *Quantity) CmpInt64(y int64) int {
if q.d.Dec != nil {
return q.d.Dec.Cmp(inf.NewDec(y, inf.Scale(0)))
}
return q.i.Cmp(int64Amount{value: y})
}
// Neg sets quantity to be the negative value of itself.
func (q *Quantity) Neg() {
q.s = nil
if q.d.Dec == nil {
q.i.value = -q.i.value
return
}
q.d.Dec.Neg(q.d.Dec)
}
// toBytes ensures q.s is set to a byte slice representing the canonical string form of this
// quantity and then returns the value. CanonicalizeBytes is an expensive operation, and caching
// this result significantly reduces the cost of normal parse / marshal operations on Quantity.
func (q *Quantity) toBytes() []byte {
if q.s == nil {
result := make([]byte, 0, int64QuantityExpectedBytes)
number, suffix := q.CanonicalizeBytes(result)
number = append(number, suffix...)
q.s = number
}
return q.s
}
// int64QuantityExpectedBytes is the expected width in bytes of the canonical string representation
// of most Quantity values.
const int64QuantityExpectedBytes = 18
// String formats the Quantity as a string. // String formats the Quantity as a string.
func (q *Quantity) String() string { func (q *Quantity) String() string {
number, suffix := q.Canonicalize() return string(q.toBytes())
return number + string(suffix)
}
// Cmp compares q and y and returns:
//
// -1 if q < y
// 0 if q == y
// +1 if q > y
//
func (q *Quantity) Cmp(y Quantity) int {
if q.Amount == nil {
if y.Amount == nil {
return 0
}
return -y.Amount.Sign()
}
if y.Amount == nil {
return q.Amount.Sign()
}
return q.Amount.Cmp(y.Amount)
}
func (q *Quantity) Add(y Quantity) error {
switch {
case y.Amount == nil:
// Adding 0: do nothing.
case q.Amount == nil:
q.Amount = &inf.Dec{}
return q.Add(y)
default:
// we want to preserve the format of the non-zero value
zero := &inf.Dec{}
if q.Amount.Cmp(zero) == 0 && y.Amount.Cmp(zero) != 0 {
q.Format = y.Format
}
q.Amount.Add(q.Amount, y.Amount)
}
return nil
}
func (q *Quantity) Sub(y Quantity) error {
switch {
case y.Amount == nil:
// Subtracting 0: do nothing.
case q.Amount == nil:
q.Amount = &inf.Dec{}
return q.Sub(y)
default:
// we want to preserve the format of the non-zero value
zero := &inf.Dec{}
if q.Amount.Cmp(zero) == 0 && y.Amount.Cmp(zero) != 0 {
q.Format = y.Format
}
q.Amount.Sub(q.Amount, y.Amount)
}
return nil
}
// Neg sets q to the negative value of y.
// It updates the format of q to match y.
func (q *Quantity) Neg(y Quantity) error {
switch {
case y.Amount == nil:
*q = y
case q.Amount == nil:
q.Amount = &inf.Dec{}
fallthrough
default:
q.Amount.Neg(y.Amount)
q.Format = y.Format
}
return nil
} }
// MarshalJSON implements the json.Marshaller interface. // MarshalJSON implements the json.Marshaller interface.
func (q Quantity) MarshalJSON() ([]byte, error) { func (q Quantity) MarshalJSON() ([]byte, error) {
return []byte(`"` + q.String() + `"`), nil if q.s != nil {
out := make([]byte, len(q.s)+2)
out[0], out[len(out)-1] = '"', '"'
copy(out[1:], q.s)
return out, nil
}
result := make([]byte, int64QuantityExpectedBytes, int64QuantityExpectedBytes)
result[0] = '"'
number, suffix := q.CanonicalizeBytes(result[1:1])
// if the same slice was returned to us that we passed in, avoid another allocation by copying number into
// the source slice and returning that
if len(number) > 0 && &number[0] == &result[1] && (len(number)+len(suffix)+2) <= int64QuantityExpectedBytes {
number = append(number, suffix...)
number = append(number, '"')
return result[:1+len(number)], nil
}
// if CanonicalizeBytes needed more space than our slice provided, we may need to allocate again so use
// append
result = result[:1]
result = append(result, number...)
result = append(result, suffix...)
result = append(result, '"')
return result, nil
} }
// UnmarshalJSON implements the json.Unmarshaller interface. // UnmarshalJSON implements the json.Unmarshaller interface.
func (q *Quantity) UnmarshalJSON(value []byte) error { func (q *Quantity) UnmarshalJSON(value []byte) error {
str := string(value) l := len(value)
parsed, err := ParseQuantity(strings.Trim(str, `"`)) if l == 4 && bytes.Equal(value, []byte("null")) {
q.d.Dec = nil
q.i = int64Amount{}
return nil
}
if l < 2 {
return ErrFormatWrong
}
if value[0] == '"' && value[l-1] == '"' {
value = value[1 : l-1]
}
parsed, err := ParseQuantity(string(value))
if err != nil { if err != nil {
return err return err
} }
parsed.s = value
// This copy is safe because parsed will not be referred to again. // This copy is safe because parsed will not be referred to again.
*q = *parsed *q = parsed
return nil return nil
} }
@ -415,7 +634,7 @@ func (q *Quantity) UnmarshalJSON(value []byte) error {
// value in the given format. // value in the given format.
func NewQuantity(value int64, format Format) *Quantity { func NewQuantity(value int64, format Format) *Quantity {
return &Quantity{ return &Quantity{
Amount: inf.NewDec(value, 0), i: int64Amount{value: value},
Format: format, Format: format,
} }
} }
@ -426,7 +645,7 @@ func NewQuantity(value int64, format Format) *Quantity {
// values x where (-1 < x < 1) && (x != 0). // values x where (-1 < x < 1) && (x != 0).
func NewMilliQuantity(value int64, format Format) *Quantity { func NewMilliQuantity(value int64, format Format) *Quantity {
return &Quantity{ return &Quantity{
Amount: inf.NewDec(value, 3), i: int64Amount{value: value, scale: -3},
Format: format, Format: format,
} }
} }
@ -435,7 +654,7 @@ func NewMilliQuantity(value int64, format Format) *Quantity {
// value * 10^scale in DecimalSI format. // value * 10^scale in DecimalSI format.
func NewScaledQuantity(value int64, scale Scale) *Quantity { func NewScaledQuantity(value int64, scale Scale) *Quantity {
return &Quantity{ return &Quantity{
Amount: inf.NewDec(value, scale.infScale()), i: int64Amount{value: value, scale: scale},
Format: DecimalSI, Format: DecimalSI,
} }
} }
@ -454,10 +673,12 @@ func (q *Quantity) MilliValue() int64 {
// ScaledValue returns the value of ceil(q * 10^scale); this could overflow an int64. // ScaledValue returns the value of ceil(q * 10^scale); this could overflow an int64.
// To detect overflow, call Value() first and verify the expected magnitude. // To detect overflow, call Value() first and verify the expected magnitude.
func (q *Quantity) ScaledValue(scale Scale) int64 { func (q *Quantity) ScaledValue(scale Scale) int64 {
if q.Amount == nil { if q.d.Dec == nil {
return 0 i, _ := q.i.AsScaledInt64(scale)
return i
} }
return scaledValue(q.Amount.UnscaledBig(), int(q.Amount.Scale()), int(scale.infScale())) dec := q.d.Dec
return scaledValue(dec.UnscaledBig(), int(dec.Scale()), int(scale.infScale()))
} }
// Set sets q's value to be value. // Set sets q's value to be value.
@ -472,22 +693,25 @@ func (q *Quantity) SetMilli(value int64) {
// SetScaled sets q's value to be value * 10^scale // SetScaled sets q's value to be value * 10^scale
func (q *Quantity) SetScaled(value int64, scale Scale) { func (q *Quantity) SetScaled(value int64, scale Scale) {
if q.Amount == nil { q.s = nil
q.Amount = &inf.Dec{} q.d.Dec = nil
} q.i = int64Amount{value: value, scale: scale}
q.Amount.SetUnscaled(value)
q.Amount.SetScale(scale.infScale())
} }
// Copy is a convenience function that makes a deep copy for you. Non-deep // Copy is a convenience function that makes a deep copy for you. Non-deep
// copies of quantities share pointers and you will regret that. // copies of quantities share pointers and you will regret that.
func (q *Quantity) Copy() *Quantity { func (q *Quantity) Copy() *Quantity {
if q.Amount == nil { if q.d.Dec == nil {
return NewQuantity(0, q.Format) return &Quantity{
s: q.s,
i: q.i,
Format: q.Format,
}
} }
tmp := &inf.Dec{} tmp := &inf.Dec{}
return &Quantity{ return &Quantity{
Amount: tmp.Set(q.Amount), s: q.s,
d: infDecAmount{tmp.Set(q.d.Dec)},
Format: q.Format, Format: q.Format,
} }
} }
@ -504,7 +728,7 @@ func (qf qFlag) Set(val string) error {
return err return err
} }
// This copy is OK because q will not be referenced again. // This copy is OK because q will not be referenced again.
*qf.dest = *q *qf.dest = q
return nil return nil
} }
@ -531,8 +755,3 @@ func QuantityFlag(flagName, defaultValue, description string) *Quantity {
func NewQuantityFlagValue(q *Quantity) flag.Value { func NewQuantityFlagValue(q *Quantity) flag.Value {
return qFlag{q} return qFlag{q}
} }
// infScale adapts a Scale value to an inf.Scale value.
func (s Scale) infScale() inf.Scale {
return inf.Scale(-s) // inf.Scale is upside-down
}

View File

@ -17,62 +17,268 @@ limitations under the License.
package resource package resource
import ( import (
"math/big" "fmt"
"io"
inf "gopkg.in/inf.v0" "github.com/gogo/protobuf/proto"
) )
// QuantityProto is a struct that is equivalent to Quantity, but intended for var _ proto.Sizer = &Quantity{}
// protobuf marshalling/unmarshalling. It is generated into a serialization
// that matches Quantity. Do not use in Go structs. func (m *Quantity) Marshal() (data []byte, err error) {
// size := m.Size()
// +protobuf=true data = make([]byte, size)
type QuantityProto struct { n, err := m.MarshalTo(data)
// The format of the quantity if err != nil {
Format Format `protobuf:"bytes,1,opt,name=format,casttype=Format"` return nil, err
// The scale dimension of the value }
Scale int32 `protobuf:"varint,2,opt,name=scale"` return data[:n], nil
// Bigint is serialized as a raw bytes array
Bigint []byte `protobuf:"bytes,3,opt,name=bigint"`
} }
// ProtoTime returns the Time as a new ProtoTime value. // MarshalTo is a customized version of the generated Protobuf unmarshaler for a struct
func (q *Quantity) QuantityProto() *QuantityProto { // with a single string field.
if q == nil { func (m *Quantity) MarshalTo(data []byte) (int, error) {
return &QuantityProto{} var i int
} _ = i
p := &QuantityProto{ var l int
Format: q.Format, _ = l
}
if q.Amount != nil { data[i] = 0xa
p.Scale = int32(q.Amount.Scale()) i++
p.Bigint = q.Amount.UnscaledBig().Bytes() // BEGIN CUSTOM MARSHAL
} out := m.toBytes()
return p i = encodeVarintGenerated(data, i, uint64(len(out)))
i += copy(data[i:], out)
// END CUSTOM MARSHAL
return i, nil
} }
// Size implements the protobuf marshalling interface. func encodeVarintGenerated(data []byte, offset int, v uint64) int {
func (q *Quantity) Size() (n int) { return q.QuantityProto().Size() } for v >= 1<<7 {
data[offset] = uint8(v&0x7f | 0x80)
v >>= 7
offset++
}
data[offset] = uint8(v)
return offset + 1
}
// Reset implements the protobuf marshalling interface. func (m *Quantity) Size() (n int) {
func (q *Quantity) Unmarshal(data []byte) error { var l int
p := QuantityProto{} _ = l
if err := p.Unmarshal(data); err != nil {
// BEGIN CUSTOM SIZE
l = len(m.toBytes())
// END CUSTOM SIZE
n += 1 + l + sovGenerated(uint64(l))
return n
}
func sovGenerated(x uint64) (n int) {
for {
n++
x >>= 7
if x == 0 {
break
}
}
return n
}
// Unmarshal is a customized version of the generated Protobuf unmarshaler for a struct
// with a single string field.
func (m *Quantity) Unmarshal(data []byte) error {
l := len(data)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := data[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: Quantity: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: Quantity: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field String_", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := data[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthGenerated
}
postIndex := iNdEx + intStringLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
s := string(data[iNdEx:postIndex])
// BEGIN CUSTOM DECODE
p, err := ParseQuantity(s)
if err != nil {
return err return err
} }
q.Format = p.Format *m = p
b := big.NewInt(0) // END CUSTOM DECODE
b.SetBytes(p.Bigint)
q.Amount = inf.NewDecBig(b, inf.Scale(p.Scale)) iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipGenerated(data[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthGenerated
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil return nil
} }
// Marshal implements the protobuf marshalling interface. func skipGenerated(data []byte) (n int, err error) {
func (q *Quantity) Marshal() (data []byte, err error) { l := len(data)
return q.QuantityProto().Marshal() iNdEx := 0
for iNdEx < l {
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowGenerated
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := data[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
wireType := int(wire & 0x7)
switch wireType {
case 0:
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowGenerated
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
iNdEx++
if data[iNdEx-1] < 0x80 {
break
}
}
return iNdEx, nil
case 1:
iNdEx += 8
return iNdEx, nil
case 2:
var length int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowGenerated
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := data[iNdEx]
iNdEx++
length |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
iNdEx += length
if length < 0 {
return 0, ErrInvalidLengthGenerated
}
return iNdEx, nil
case 3:
for {
var innerWire uint64
var start int = iNdEx
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowGenerated
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := data[iNdEx]
iNdEx++
innerWire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
innerWireType := int(innerWire & 0x7)
if innerWireType == 4 {
break
}
next, err := skipGenerated(data[start:])
if err != nil {
return 0, err
}
iNdEx = start + next
}
return iNdEx, nil
case 4:
return iNdEx, nil
case 5:
iNdEx += 4
return iNdEx, nil
default:
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
}
}
panic("unreachable")
} }
// MarshalTo implements the protobuf marshalling interface. var (
func (q *Quantity) MarshalTo(data []byte) (int, error) { ErrInvalidLengthGenerated = fmt.Errorf("proto: negative length found during unmarshaling")
return q.QuantityProto().MarshalTo(data) ErrIntOverflowGenerated = fmt.Errorf("proto: integer overflow")
} )

File diff suppressed because it is too large Load Diff

View File

@ -24,8 +24,9 @@ type suffix string
// suffixer can interpret and construct suffixes. // suffixer can interpret and construct suffixes.
type suffixer interface { type suffixer interface {
interpret(suffix) (base, exponent int, fmt Format, ok bool) interpret(suffix) (base, exponent int32, fmt Format, ok bool)
construct(base, exponent int, fmt Format) (s suffix, ok bool) construct(base, exponent int32, fmt Format) (s suffix, ok bool)
constructBytes(base, exponent int32, fmt Format) (s []byte, ok bool)
} }
// quantitySuffixer handles suffixes for all three formats that quantity // quantitySuffixer handles suffixes for all three formats that quantity
@ -33,12 +34,13 @@ type suffixer interface {
var quantitySuffixer = newSuffixer() var quantitySuffixer = newSuffixer()
type bePair struct { type bePair struct {
base, exponent int base, exponent int32
} }
type listSuffixer struct { type listSuffixer struct {
suffixToBE map[suffix]bePair suffixToBE map[suffix]bePair
beToSuffix map[bePair]suffix beToSuffix map[bePair]suffix
beToSuffixBytes map[bePair][]byte
} }
func (ls *listSuffixer) addSuffix(s suffix, pair bePair) { func (ls *listSuffixer) addSuffix(s suffix, pair bePair) {
@ -48,11 +50,15 @@ func (ls *listSuffixer) addSuffix(s suffix, pair bePair) {
if ls.beToSuffix == nil { if ls.beToSuffix == nil {
ls.beToSuffix = map[bePair]suffix{} ls.beToSuffix = map[bePair]suffix{}
} }
if ls.beToSuffixBytes == nil {
ls.beToSuffixBytes = map[bePair][]byte{}
}
ls.suffixToBE[s] = pair ls.suffixToBE[s] = pair
ls.beToSuffix[pair] = s ls.beToSuffix[pair] = s
ls.beToSuffixBytes[pair] = []byte(s)
} }
func (ls *listSuffixer) lookup(s suffix) (base, exponent int, ok bool) { func (ls *listSuffixer) lookup(s suffix) (base, exponent int32, ok bool) {
pair, ok := ls.suffixToBE[s] pair, ok := ls.suffixToBE[s]
if !ok { if !ok {
return 0, 0, false return 0, 0, false
@ -60,19 +66,50 @@ func (ls *listSuffixer) lookup(s suffix) (base, exponent int, ok bool) {
return pair.base, pair.exponent, true return pair.base, pair.exponent, true
} }
func (ls *listSuffixer) construct(base, exponent int) (s suffix, ok bool) { func (ls *listSuffixer) construct(base, exponent int32) (s suffix, ok bool) {
s, ok = ls.beToSuffix[bePair{base, exponent}] s, ok = ls.beToSuffix[bePair{base, exponent}]
return return
} }
func (ls *listSuffixer) constructBytes(base, exponent int32) (s []byte, ok bool) {
s, ok = ls.beToSuffixBytes[bePair{base, exponent}]
return
}
type suffixHandler struct { type suffixHandler struct {
decSuffixes listSuffixer decSuffixes listSuffixer
binSuffixes listSuffixer binSuffixes listSuffixer
} }
type fastLookup struct {
*suffixHandler
}
func (l fastLookup) interpret(s suffix) (base, exponent int32, format Format, ok bool) {
switch s {
case "":
return 10, 0, DecimalSI, true
case "n":
return 10, -9, DecimalSI, true
case "u":
return 10, -6, DecimalSI, true
case "m":
return 10, -3, DecimalSI, true
case "k":
return 10, 3, DecimalSI, true
case "M":
return 10, 6, DecimalSI, true
case "G":
return 10, 9, DecimalSI, true
}
return l.suffixHandler.interpret(s)
}
func newSuffixer() suffixer { func newSuffixer() suffixer {
sh := &suffixHandler{} sh := &suffixHandler{}
// IMPORTANT: if you change this section you must change fastLookup
sh.binSuffixes.addSuffix("Ki", bePair{2, 10}) sh.binSuffixes.addSuffix("Ki", bePair{2, 10})
sh.binSuffixes.addSuffix("Mi", bePair{2, 20}) sh.binSuffixes.addSuffix("Mi", bePair{2, 20})
sh.binSuffixes.addSuffix("Gi", bePair{2, 30}) sh.binSuffixes.addSuffix("Gi", bePair{2, 30})
@ -94,10 +131,10 @@ func newSuffixer() suffixer {
sh.decSuffixes.addSuffix("P", bePair{10, 15}) sh.decSuffixes.addSuffix("P", bePair{10, 15})
sh.decSuffixes.addSuffix("E", bePair{10, 18}) sh.decSuffixes.addSuffix("E", bePair{10, 18})
return sh return fastLookup{sh}
} }
func (sh *suffixHandler) construct(base, exponent int, fmt Format) (s suffix, ok bool) { func (sh *suffixHandler) construct(base, exponent int32, fmt Format) (s suffix, ok bool) {
switch fmt { switch fmt {
case DecimalSI: case DecimalSI:
return sh.decSuffixes.construct(base, exponent) return sh.decSuffixes.construct(base, exponent)
@ -115,7 +152,32 @@ func (sh *suffixHandler) construct(base, exponent int, fmt Format) (s suffix, ok
return "", false return "", false
} }
func (sh *suffixHandler) interpret(suffix suffix) (base, exponent int, fmt Format, ok bool) { func (sh *suffixHandler) constructBytes(base, exponent int32, format Format) (s []byte, ok bool) {
switch format {
case DecimalSI:
return sh.decSuffixes.constructBytes(base, exponent)
case BinarySI:
return sh.binSuffixes.constructBytes(base, exponent)
case DecimalExponent:
if base != 10 {
return nil, false
}
if exponent == 0 {
return nil, true
}
result := make([]byte, 8, 8)
result[0] = 'e'
number := strconv.AppendInt(result[1:1], int64(exponent), 10)
if &result[1] == &number[0] {
return result[:1+len(number)], true
}
result = append(result[:1], number...)
return result, true
}
return nil, false
}
func (sh *suffixHandler) interpret(suffix suffix) (base, exponent int32, fmt Format, ok bool) {
// Try lookup tables first // Try lookup tables first
if b, e, ok := sh.decSuffixes.lookup(suffix); ok { if b, e, ok := sh.decSuffixes.lookup(suffix); ok {
return b, e, DecimalSI, true return b, e, DecimalSI, true
@ -129,7 +191,7 @@ func (sh *suffixHandler) interpret(suffix suffix) (base, exponent int, fmt Forma
if err != nil { if err != nil {
return 0, 0, DecimalExponent, false return 0, 0, DecimalExponent, false
} }
return 10, int(parsed), DecimalExponent, true return 10, int32(parsed), DecimalExponent, true
} }
return 0, 0, DecimalExponent, false return 0, 0, DecimalExponent, false