diff --git a/pkg/api/resource/amount.go b/pkg/api/resource/amount.go new file mode 100644 index 00000000000..49caf3c1736 --- /dev/null +++ b/pkg/api/resource/amount.go @@ -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 +} diff --git a/pkg/api/resource/amount_test.go b/pkg/api/resource/amount_test.go new file mode 100644 index 00000000000..acaad6307b6 --- /dev/null +++ b/pkg/api/resource/amount_test.go @@ -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) + } + } +} diff --git a/pkg/api/resource/deep_copy_generated.go b/pkg/api/resource/deep_copy_generated.go index 6a34e2d4342..3f72141073e 100644 --- a/pkg/api/resource/deep_copy_generated.go +++ b/pkg/api/resource/deep_copy_generated.go @@ -21,35 +21,27 @@ limitations under the License. package resource import ( - inf_v0 "gopkg.in/inf.v0" conversion "k8s.io/kubernetes/pkg/conversion" ) func DeepCopy_resource_Quantity(in Quantity, out *Quantity, c *conversion.Cloner) error { - if in.Amount != nil { - in, out := in.Amount, &out.Amount - *out = new(inf_v0.Dec) - if newVal, err := c.DeepCopy(*in); err != nil { - return err - } else { - **out = newVal.(inf_v0.Dec) - } + if newVal, err := c.DeepCopy(in.i); err != nil { + return err } else { - out.Amount = nil + out.i = newVal.(int64Amount) } - out.Format = in.Format - return nil -} - -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 + if newVal, err := c.DeepCopy(in.d); err != nil { + return err + } else { + out.d = newVal.(infDecAmount) + } + if in.s != nil { + in, out := in.s, &out.s *out = make([]byte, len(in)) copy(*out, in) } else { - out.Bigint = nil + out.s = nil } + out.Format = in.Format return nil } diff --git a/pkg/api/resource/generated.pb.go b/pkg/api/resource/generated.pb.go index 7484f92761a..cf9447a321f 100644 --- a/pkg/api/resource/generated.pb.go +++ b/pkg/api/resource/generated.pb.go @@ -26,7 +26,6 @@ limitations under the License. It has these top-level messages: Quantity - QuantityProto */ package resource @@ -34,8 +33,6 @@ import proto "github.com/gogo/protobuf/proto" import fmt "fmt" import math "math" -import io "io" - // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf @@ -44,328 +41,6 @@ var _ = math.Inf func (m *Quantity) Reset() { *m = Quantity{} } func (*Quantity) ProtoMessage() {} -func (m *QuantityProto) Reset() { *m = QuantityProto{} } -func (m *QuantityProto) String() string { return proto.CompactTextString(m) } -func (*QuantityProto) ProtoMessage() {} - func init() { 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") -) diff --git a/pkg/api/resource/generated.proto b/pkg/api/resource/generated.proto index 34ede4581bc..8a1376c32e1 100644 --- a/pkg/api/resource/generated.proto +++ b/pkg/api/resource/generated.proto @@ -84,26 +84,10 @@ option go_package = "resource"; // cause implementors to also use a fixed point implementation. // // +protobuf=true -// +protobuf.embed=QuantityProto +// +protobuf.embed=string // +protobuf.options.marshal=false // +protobuf.options.(gogoproto.goproto_stringer)=false message Quantity { - optional QuantityProto QuantityProto = 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; + optional string string = 1; } diff --git a/pkg/api/resource/math.go b/pkg/api/resource/math.go new file mode 100644 index 00000000000..163aafa5db6 --- /dev/null +++ b/pkg/api/resource/math.go @@ -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 +} diff --git a/pkg/api/resource/math_test.go b/pkg/api/resource/math_test.go new file mode 100644 index 00000000000..0fdda12e0a1 --- /dev/null +++ b/pkg/api/resource/math_test.go @@ -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) + } + } +} diff --git a/pkg/api/resource/quantity.go b/pkg/api/resource/quantity.go index e61c8800ec4..47c0ac08e82 100644 --- a/pkg/api/resource/quantity.go +++ b/pkg/api/resource/quantity.go @@ -17,10 +17,12 @@ limitations under the License. package resource import ( + "bytes" "errors" "fmt" "math/big" "regexp" + "strconv" "strings" flag "github.com/spf13/pflag" @@ -86,19 +88,34 @@ import ( // cause implementors to also use a fixed point implementation. // // +protobuf=true -// +protobuf.embed=QuantityProto +// +protobuf.embed=string // +protobuf.options.marshal=false // +protobuf.options.(gogoproto.goproto_stringer)=false type Quantity struct { - // Amount is public, so you can manipulate it if the accessor - // functions are not sufficient. - Amount *inf.Dec + // i is the quantity in int64 scaled form, if d.Dec == nil + i int64Amount + // 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 // more details. 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. type Format string @@ -115,26 +132,9 @@ func MustParse(str string) Quantity { if err != nil { 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 ( // 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. @@ -149,47 +149,189 @@ var ( ErrFormatWrong = errors.New("quantities must match the regular expression '" + splitREString + "'") ErrNumeric = errors.New("unable to parse numeric part of quantity") 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. -func ParseQuantity(str string) (*Quantity, error) { - parts := splitRE.FindStringSubmatch(strings.TrimSpace(str)) - // regexp returns are entire match, followed by an entry for each () section. - if len(parts) != 3 { - return nil, ErrFormatWrong +func ParseQuantity(str string) (Quantity, error) { + if len(str) == 0 { + return Quantity{}, 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) - if _, ok := amount.SetString(parts[1]); !ok { - return nil, ErrNumeric - } - - base, exponent, format, ok := quantitySuffixer.interpret(suffix(parts[2])) - if !ok { - return nil, ErrSuffix + if _, ok := amount.SetString(value); !ok { + return Quantity{}, ErrNumeric } // 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. - if amount.Cmp(maxAllowed) > 0 { - amount.Set(maxAllowed) + // TODO: this prevents accumulating quantities greater than int64, for instance quota across a cluster + if format == BinarySI && amount.Cmp(maxAllowed.Dec) > 0 { + amount.Set(maxAllowed.Dec) } + if format == BinarySI && amount.Cmp(decOne) < 0 && amount.Cmp(decZero) > 0 { // This avoids rounding and hopefully confusion, too. format = DecimalSI @@ -228,25 +372,7 @@ func ParseQuantity(str string) (*Quantity, error) { amount.Neg(amount) } - return &Quantity{amount, 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 + return Quantity{d: infDecAmount{amount}, Format: format}, nil } // 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. // * Otherwise, if q.Format is set to BinarySI, frational parts of q.Amount will be // rounded up. (1.1i becomes 2i.) -func (q *Quantity) Canonicalize() (string, suffix) { - if q.Amount == nil { - return "0", "" - } - - // zero is zero always - if q.Amount.Cmp(&inf.Dec{}) == 0 { - return "0", "" +func (q *Quantity) CanonicalizeBytes(out []byte) (result, suffix []byte) { + if q.IsZero() { + return zeroBytes, nil } + var rounded CanonicalValue format := q.Format switch format { case DecimalExponent, DecimalSI: 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. format = DecimalSI } else { - tmp := &inf.Dec{} - tmp.Round(q.Amount, 0, inf.RoundUp) - if tmp.Cmp(q.Amount) != 0 { + var exact bool + if rounded, exact = q.AsScale(0); !exact { // Don't lose precision-- show as DecimalSI format = DecimalSI } @@ -289,125 +410,223 @@ func (q *Quantity) Canonicalize() (string, suffix) { // one of the other formats. switch format { case DecimalExponent, DecimalSI: - mantissa := q.Amount.UnscaledBig() - exponent := int(-q.Amount.Scale()) - 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() + number, exponent := q.AsCanonicalBytes(out) + suffix, _ := quantitySuffixer.constructBytes(10, exponent, format) return number, suffix - case BinarySI: - tmp := &inf.Dec{} - tmp.Round(q.Amount, 0, inf.RoundUp) - - amount, exponent := removeFactors(tmp.UnscaledBig(), big1024) - suffix, _ := quantitySuffixer.construct(2, exponent*10, format) - number := amount.String() + default: + // format must be BinarySI + number, exponent := rounded.AsCanonicalBase1024Bytes(out) + suffix, _ := quantitySuffixer.constructBytes(2, exponent*10, format) 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. func (q *Quantity) String() string { - number, suffix := q.Canonicalize() - 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 + return string(q.toBytes()) } // MarshalJSON implements the json.Marshaller interface. 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. func (q *Quantity) UnmarshalJSON(value []byte) error { - str := string(value) - parsed, err := ParseQuantity(strings.Trim(str, `"`)) + l := len(value) + 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 { return err } + parsed.s = value // This copy is safe because parsed will not be referred to again. - *q = *parsed + *q = parsed return nil } @@ -415,7 +634,7 @@ func (q *Quantity) UnmarshalJSON(value []byte) error { // value in the given format. func NewQuantity(value int64, format Format) *Quantity { return &Quantity{ - Amount: inf.NewDec(value, 0), + i: int64Amount{value: value}, Format: format, } } @@ -426,7 +645,7 @@ func NewQuantity(value int64, format Format) *Quantity { // values x where (-1 < x < 1) && (x != 0). func NewMilliQuantity(value int64, format Format) *Quantity { return &Quantity{ - Amount: inf.NewDec(value, 3), + i: int64Amount{value: value, scale: -3}, Format: format, } } @@ -435,7 +654,7 @@ func NewMilliQuantity(value int64, format Format) *Quantity { // value * 10^scale in DecimalSI format. func NewScaledQuantity(value int64, scale Scale) *Quantity { return &Quantity{ - Amount: inf.NewDec(value, scale.infScale()), + i: int64Amount{value: value, scale: scale}, 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. // To detect overflow, call Value() first and verify the expected magnitude. func (q *Quantity) ScaledValue(scale Scale) int64 { - if q.Amount == nil { - return 0 + if q.d.Dec == nil { + 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. @@ -472,22 +693,25 @@ func (q *Quantity) SetMilli(value int64) { // SetScaled sets q's value to be value * 10^scale func (q *Quantity) SetScaled(value int64, scale Scale) { - if q.Amount == nil { - q.Amount = &inf.Dec{} - } - q.Amount.SetUnscaled(value) - q.Amount.SetScale(scale.infScale()) + q.s = nil + q.d.Dec = nil + q.i = int64Amount{value: value, scale: scale} } // Copy is a convenience function that makes a deep copy for you. Non-deep // copies of quantities share pointers and you will regret that. func (q *Quantity) Copy() *Quantity { - if q.Amount == nil { - return NewQuantity(0, q.Format) + if q.d.Dec == nil { + return &Quantity{ + s: q.s, + i: q.i, + Format: q.Format, + } } tmp := &inf.Dec{} return &Quantity{ - Amount: tmp.Set(q.Amount), + s: q.s, + d: infDecAmount{tmp.Set(q.d.Dec)}, Format: q.Format, } } @@ -504,7 +728,7 @@ func (qf qFlag) Set(val string) error { return err } // This copy is OK because q will not be referenced again. - *qf.dest = *q + *qf.dest = q return nil } @@ -531,8 +755,3 @@ func QuantityFlag(flagName, defaultValue, description string) *Quantity { func NewQuantityFlagValue(q *Quantity) flag.Value { 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 -} diff --git a/pkg/api/resource/quantity_proto.go b/pkg/api/resource/quantity_proto.go index 01d5c2997ea..0159a4112b7 100644 --- a/pkg/api/resource/quantity_proto.go +++ b/pkg/api/resource/quantity_proto.go @@ -17,62 +17,268 @@ limitations under the License. package resource 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 -// protobuf marshalling/unmarshalling. It is generated into a serialization -// that matches Quantity. Do not use in Go structs. -// -// +protobuf=true -type QuantityProto struct { - // The format of the quantity - Format Format `protobuf:"bytes,1,opt,name=format,casttype=Format"` - // The scale dimension of the value - Scale int32 `protobuf:"varint,2,opt,name=scale"` - // Bigint is serialized as a raw bytes array - Bigint []byte `protobuf:"bytes,3,opt,name=bigint"` +var _ proto.Sizer = &Quantity{} + +func (m *Quantity) 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 } -// ProtoTime returns the Time as a new ProtoTime value. -func (q *Quantity) QuantityProto() *QuantityProto { - if q == nil { - return &QuantityProto{} - } - p := &QuantityProto{ - Format: q.Format, - } - if q.Amount != nil { - p.Scale = int32(q.Amount.Scale()) - p.Bigint = q.Amount.UnscaledBig().Bytes() - } - return p +// MarshalTo is a customized version of the generated Protobuf unmarshaler for a struct +// with a single string field. +func (m *Quantity) MarshalTo(data []byte) (int, error) { + var i int + _ = i + var l int + _ = l + + data[i] = 0xa + i++ + // BEGIN CUSTOM MARSHAL + out := m.toBytes() + 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 (q *Quantity) Size() (n int) { return q.QuantityProto().Size() } - -// Reset implements the protobuf marshalling interface. -func (q *Quantity) Unmarshal(data []byte) error { - p := QuantityProto{} - if err := p.Unmarshal(data); err != nil { - return err +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 *Quantity) Size() (n int) { + var l int + _ = l + + // 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 + } + *m = p + // END CUSTOM DECODE + + 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 } - q.Format = p.Format - b := big.NewInt(0) - b.SetBytes(p.Bigint) - q.Amount = inf.NewDecBig(b, inf.Scale(p.Scale)) return nil } -// Marshal implements the protobuf marshalling interface. -func (q *Quantity) Marshal() (data []byte, err error) { - return q.QuantityProto().Marshal() +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") } -// MarshalTo implements the protobuf marshalling interface. -func (q *Quantity) MarshalTo(data []byte) (int, error) { - return q.QuantityProto().MarshalTo(data) -} +var ( + ErrInvalidLengthGenerated = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowGenerated = fmt.Errorf("proto: integer overflow") +) diff --git a/pkg/api/resource/quantity_test.go b/pkg/api/resource/quantity_test.go index 66d1563a27c..e37a8eca2c1 100644 --- a/pkg/api/resource/quantity_test.go +++ b/pkg/api/resource/quantity_test.go @@ -17,8 +17,8 @@ limitations under the License. package resource import ( - //"reflect" "encoding/json" + "math/rand" "testing" fuzz "github.com/google/gofuzz" @@ -31,14 +31,29 @@ var ( testQuantityFlag = QuantityFlag("quantityFlag", "1M", "dummy flag for testing the quantity flag mechanism") ) -func dec(i int64, exponent int) *inf.Dec { +var useInfDec bool + +func amount(i int64, exponent int) infDecAmount { // See the below test-- scale is the negative of an exponent. - return inf.NewDec(i, inf.Scale(-exponent)) + return infDecAmount{inf.NewDec(i, inf.Scale(-exponent))} +} + +func dec(i int64, exponent int) infDecAmount { + // See the below test-- scale is the negative of an exponent. + return infDecAmount{inf.NewDec(i, inf.Scale(-exponent))} +} + +func decQuantity(i int64, exponent int, format Format) Quantity { + return Quantity{d: dec(i, exponent), Format: format} +} + +func intQuantity(i int64, exponent Scale, format Format) Quantity { + return Quantity{i: int64Amount{value: i, scale: exponent}, Format: format} } func TestDec(t *testing.T) { table := []struct { - got *inf.Dec + got infDecAmount expect string }{ {dec(1, 0), "1"}, @@ -52,7 +67,7 @@ func TestDec(t *testing.T) { } for _, item := range table { - if e, a := item.expect, item.got.String(); e != a { + if e, a := item.expect, item.got.Dec.String(); e != a { t.Errorf("expected %v, got %v", e, a) } } @@ -75,19 +90,14 @@ func TestQuantityAddZeroPreservesSuffix(t *testing.T) { value := MustParse(testValue) v1 := *value.Copy() // ensure non-zero + zero = non-zero (suffix preserved) - err := v1.Add(zero) - if err != nil { - t.Errorf("Unexpected error %v", err) - } + v1.Add(zero) // ensure zero + non-zero = non-zero (suffix preserved) v2 := *zero.Copy() - err = v2.Add(value) - if err != nil { - t.Errorf("Unexpected error %v", err) - } - // ensure we preserved the input value + v2.Add(value) + if v1.String() != testValue { t.Errorf("Expected %v, actual %v", testValue, v1.String()) + continue } if v2.String() != testValue { t.Errorf("Expected %v, actual %v", testValue, v2.String()) @@ -104,10 +114,7 @@ func TestQuantitySubZeroPreservesSuffix(t *testing.T) { value := MustParse(testValue) v1 := *value.Copy() // ensure non-zero - zero = non-zero (suffix preserved) - err := v1.Sub(zero) - if err != nil { - t.Errorf("Unexpected error %v", err) - } + v1.Sub(zero) // ensure we preserved the input value if v1.String() != testValue { t.Errorf("Expected %v, actual %v", testValue, v1.String()) @@ -115,15 +122,9 @@ func TestQuantitySubZeroPreservesSuffix(t *testing.T) { // ensure zero - non-zero = -non-zero (suffix preserved) v2 := *zero.Copy() - err = v2.Sub(value) - if err != nil { - t.Errorf("Unexpected error %v", err) - } + v2.Sub(value) negVal := *value.Copy() - err = negVal.Neg(negVal) - if err != nil { - t.Errorf("Unexpected error %v", err) - } + negVal.Neg() if v2.String() != negVal.String() { t.Errorf("Expected %v, actual %v", negVal.String(), v2.String()) } @@ -133,10 +134,8 @@ func TestQuantitySubZeroPreservesSuffix(t *testing.T) { // Verifies that you get 0 as canonical value if internal value is 0, and not 0 func TestQuantityCanocicalizeZero(t *testing.T) { val := MustParse("1000m") - x := val.Amount - y := dec(1, 0) - z := val.Amount.Sub(x, y) - zero := Quantity{z, DecimalSI} + val.i.Sub(int64Amount{value: 1}) + zero := Quantity{i: val.i, Format: DecimalSI} if expected, actual := "0", zero.String(); expected != actual { t.Errorf("Expected %v, actual %v", expected, actual) } @@ -166,187 +165,300 @@ func TestQuantityCmp(t *testing.T) { y *inf.Dec expect int }{ - {dec(0, 0), dec(0, 0), 0}, - {nil, dec(0, 0), 0}, - {dec(0, 0), nil, 0}, + {dec(0, 0).Dec, dec(0, 0).Dec, 0}, + {nil, dec(0, 0).Dec, 0}, + {dec(0, 0).Dec, nil, 0}, {nil, nil, 0}, - {nil, dec(10, 0), -1}, - {nil, dec(-10, 0), 1}, - {dec(10, 0), nil, 1}, - {dec(-10, 0), nil, -1}, + {nil, dec(10, 0).Dec, -1}, + {nil, dec(-10, 0).Dec, 1}, + {dec(10, 0).Dec, nil, 1}, + {dec(-10, 0).Dec, nil, -1}, } for _, nilCase := range nils { - q1 := Quantity{nilCase.x, DecimalSI} - q2 := Quantity{nilCase.y, DecimalSI} + q1 := Quantity{d: infDecAmount{nilCase.x}, Format: DecimalSI} + q2 := Quantity{d: infDecAmount{nilCase.y}, Format: DecimalSI} if result := q1.Cmp(q2); result != nilCase.expect { t.Errorf("X: %v, Y: %v, Expected: %v, Actual: %v", nilCase.x, nilCase.y, nilCase.expect, result) } } } +func TestParseQuantityString(t *testing.T) { + table := []struct { + input string + positive bool + value string + num, denom, suffix string + }{ + {"0.025Ti", true, "0.025", "0", "025", "Ti"}, + {"1.025Ti", true, "1.025", "1", "025", "Ti"}, + {"-1.025Ti", false, "-1.025", "1", "025", "Ti"}, + {".", true, ".", "0", "", ""}, + {"-.", false, "-.", "0", "", ""}, + {"1E-3", true, "1", "1", "", "E-3"}, + } + for _, test := range table { + positive, value, num, denom, suffix, err := parseQuantityString(test.input) + if err != nil { + t.Errorf("%s: error: %v", test.input, err) + continue + } + if positive != test.positive || value != test.value || num != test.num || denom != test.denom || suffix != test.suffix { + t.Errorf("%s: unmatched: %t %q %q %q %q", test.input, positive, value, num, denom, suffix) + } + } +} + func TestQuantityParse(t *testing.T) { + if _, err := ParseQuantity(""); err == nil { + t.Errorf("expected empty string to return error") + } + table := []struct { input string expect Quantity }{ - {"0", Quantity{dec(0, 0), DecimalSI}}, - {"0n", Quantity{dec(0, 0), DecimalSI}}, - {"0u", Quantity{dec(0, 0), DecimalSI}}, - {"0m", Quantity{dec(0, 0), DecimalSI}}, - {"0Ki", Quantity{dec(0, 0), BinarySI}}, - {"0k", Quantity{dec(0, 0), DecimalSI}}, - {"0Mi", Quantity{dec(0, 0), BinarySI}}, - {"0M", Quantity{dec(0, 0), DecimalSI}}, - {"0Gi", Quantity{dec(0, 0), BinarySI}}, - {"0G", Quantity{dec(0, 0), DecimalSI}}, - {"0Ti", Quantity{dec(0, 0), BinarySI}}, - {"0T", Quantity{dec(0, 0), DecimalSI}}, + {"0", decQuantity(0, 0, DecimalSI)}, + {"0n", decQuantity(0, 0, DecimalSI)}, + {"0u", decQuantity(0, 0, DecimalSI)}, + {"0m", decQuantity(0, 0, DecimalSI)}, + {"0Ki", decQuantity(0, 0, BinarySI)}, + {"0k", decQuantity(0, 0, DecimalSI)}, + {"0Mi", decQuantity(0, 0, BinarySI)}, + {"0M", decQuantity(0, 0, DecimalSI)}, + {"0Gi", decQuantity(0, 0, BinarySI)}, + {"0G", decQuantity(0, 0, DecimalSI)}, + {"0Ti", decQuantity(0, 0, BinarySI)}, + {"0T", decQuantity(0, 0, DecimalSI)}, // Binary suffixes - {"1Ki", Quantity{dec(1024, 0), BinarySI}}, - {"8Ki", Quantity{dec(8*1024, 0), BinarySI}}, - {"7Mi", Quantity{dec(7*1024*1024, 0), BinarySI}}, - {"6Gi", Quantity{dec(6*1024*1024*1024, 0), BinarySI}}, - {"5Ti", Quantity{dec(5*1024*1024*1024*1024, 0), BinarySI}}, - {"4Pi", Quantity{dec(4*1024*1024*1024*1024*1024, 0), BinarySI}}, - {"3Ei", Quantity{dec(3*1024*1024*1024*1024*1024*1024, 0), BinarySI}}, + {"1Ki", decQuantity(1024, 0, BinarySI)}, + {"8Ki", decQuantity(8*1024, 0, BinarySI)}, + {"7Mi", decQuantity(7*1024*1024, 0, BinarySI)}, + {"6Gi", decQuantity(6*1024*1024*1024, 0, BinarySI)}, + {"5Ti", decQuantity(5*1024*1024*1024*1024, 0, BinarySI)}, + {"4Pi", decQuantity(4*1024*1024*1024*1024*1024, 0, BinarySI)}, + {"3Ei", decQuantity(3*1024*1024*1024*1024*1024*1024, 0, BinarySI)}, - {"10Ti", Quantity{dec(10*1024*1024*1024*1024, 0), BinarySI}}, - {"100Ti", Quantity{dec(100*1024*1024*1024*1024, 0), BinarySI}}, + {"10Ti", decQuantity(10*1024*1024*1024*1024, 0, BinarySI)}, + {"100Ti", decQuantity(100*1024*1024*1024*1024, 0, BinarySI)}, // Decimal suffixes - {"5n", Quantity{dec(5, -9), DecimalSI}}, - {"4u", Quantity{dec(4, -6), DecimalSI}}, - {"3m", Quantity{dec(3, -3), DecimalSI}}, - {"9", Quantity{dec(9, 0), DecimalSI}}, - {"8k", Quantity{dec(8, 3), DecimalSI}}, - {"7M", Quantity{dec(7, 6), DecimalSI}}, - {"6G", Quantity{dec(6, 9), DecimalSI}}, - {"5T", Quantity{dec(5, 12), DecimalSI}}, - {"40T", Quantity{dec(4, 13), DecimalSI}}, - {"300T", Quantity{dec(3, 14), DecimalSI}}, - {"2P", Quantity{dec(2, 15), DecimalSI}}, - {"1E", Quantity{dec(1, 18), DecimalSI}}, + {"5n", decQuantity(5, -9, DecimalSI)}, + {"4u", decQuantity(4, -6, DecimalSI)}, + {"3m", decQuantity(3, -3, DecimalSI)}, + {"9", decQuantity(9, 0, DecimalSI)}, + {"8k", decQuantity(8, 3, DecimalSI)}, + {"50k", decQuantity(5, 4, DecimalSI)}, + {"7M", decQuantity(7, 6, DecimalSI)}, + {"6G", decQuantity(6, 9, DecimalSI)}, + {"5T", decQuantity(5, 12, DecimalSI)}, + {"40T", decQuantity(4, 13, DecimalSI)}, + {"300T", decQuantity(3, 14, DecimalSI)}, + {"2P", decQuantity(2, 15, DecimalSI)}, + {"1E", decQuantity(1, 18, DecimalSI)}, // Decimal exponents - {"1E-3", Quantity{dec(1, -3), DecimalExponent}}, - {"1e3", Quantity{dec(1, 3), DecimalExponent}}, - {"1E6", Quantity{dec(1, 6), DecimalExponent}}, - {"1e9", Quantity{dec(1, 9), DecimalExponent}}, - {"1E12", Quantity{dec(1, 12), DecimalExponent}}, - {"1e15", Quantity{dec(1, 15), DecimalExponent}}, - {"1E18", Quantity{dec(1, 18), DecimalExponent}}, + {"1E-3", decQuantity(1, -3, DecimalExponent)}, + {"1e3", decQuantity(1, 3, DecimalExponent)}, + {"1E6", decQuantity(1, 6, DecimalExponent)}, + {"1e9", decQuantity(1, 9, DecimalExponent)}, + {"1E12", decQuantity(1, 12, DecimalExponent)}, + {"1e15", decQuantity(1, 15, DecimalExponent)}, + {"1E18", decQuantity(1, 18, DecimalExponent)}, // Nonstandard but still parsable - {"1e14", Quantity{dec(1, 14), DecimalExponent}}, - {"1e13", Quantity{dec(1, 13), DecimalExponent}}, - {"1e3", Quantity{dec(1, 3), DecimalExponent}}, - {"100.035k", Quantity{dec(100035, 0), DecimalSI}}, + {"1e14", decQuantity(1, 14, DecimalExponent)}, + {"1e13", decQuantity(1, 13, DecimalExponent)}, + {"1e3", decQuantity(1, 3, DecimalExponent)}, + {"100.035k", decQuantity(100035, 0, DecimalSI)}, // Things that look like floating point - {"0.001", Quantity{dec(1, -3), DecimalSI}}, - {"0.0005k", Quantity{dec(5, -1), DecimalSI}}, - {"0.005", Quantity{dec(5, -3), DecimalSI}}, - {"0.05", Quantity{dec(5, -2), DecimalSI}}, - {"0.5", Quantity{dec(5, -1), DecimalSI}}, - {"0.00050k", Quantity{dec(5, -1), DecimalSI}}, - {"0.00500", Quantity{dec(5, -3), DecimalSI}}, - {"0.05000", Quantity{dec(5, -2), DecimalSI}}, - {"0.50000", Quantity{dec(5, -1), DecimalSI}}, - {"0.5e0", Quantity{dec(5, -1), DecimalExponent}}, - {"0.5e-1", Quantity{dec(5, -2), DecimalExponent}}, - {"0.5e-2", Quantity{dec(5, -3), DecimalExponent}}, - {"0.5e0", Quantity{dec(5, -1), DecimalExponent}}, - {"10.035M", Quantity{dec(10035, 3), DecimalSI}}, + {"0.001", decQuantity(1, -3, DecimalSI)}, + {"0.0005k", decQuantity(5, -1, DecimalSI)}, + {"0.005", decQuantity(5, -3, DecimalSI)}, + {"0.05", decQuantity(5, -2, DecimalSI)}, + {"0.5", decQuantity(5, -1, DecimalSI)}, + {"0.00050k", decQuantity(5, -1, DecimalSI)}, + {"0.00500", decQuantity(5, -3, DecimalSI)}, + {"0.05000", decQuantity(5, -2, DecimalSI)}, + {"0.50000", decQuantity(5, -1, DecimalSI)}, + {"0.5e0", decQuantity(5, -1, DecimalExponent)}, + {"0.5e-1", decQuantity(5, -2, DecimalExponent)}, + {"0.5e-2", decQuantity(5, -3, DecimalExponent)}, + {"0.5e0", decQuantity(5, -1, DecimalExponent)}, + {"10.035M", decQuantity(10035, 3, DecimalSI)}, - {"1.2e3", Quantity{dec(12, 2), DecimalExponent}}, - {"1.3E+6", Quantity{dec(13, 5), DecimalExponent}}, - {"1.40e9", Quantity{dec(14, 8), DecimalExponent}}, - {"1.53E12", Quantity{dec(153, 10), DecimalExponent}}, - {"1.6e15", Quantity{dec(16, 14), DecimalExponent}}, - {"1.7E18", Quantity{dec(17, 17), DecimalExponent}}, + {"1.2e3", decQuantity(12, 2, DecimalExponent)}, + {"1.3E+6", decQuantity(13, 5, DecimalExponent)}, + {"1.40e9", decQuantity(14, 8, DecimalExponent)}, + {"1.53E12", decQuantity(153, 10, DecimalExponent)}, + {"1.6e15", decQuantity(16, 14, DecimalExponent)}, + {"1.7E18", decQuantity(17, 17, DecimalExponent)}, - {"9.01", Quantity{dec(901, -2), DecimalSI}}, - {"8.1k", Quantity{dec(81, 2), DecimalSI}}, - {"7.123456M", Quantity{dec(7123456, 0), DecimalSI}}, - {"6.987654321G", Quantity{dec(6987654321, 0), DecimalSI}}, - {"5.444T", Quantity{dec(5444, 9), DecimalSI}}, - {"40.1T", Quantity{dec(401, 11), DecimalSI}}, - {"300.2T", Quantity{dec(3002, 11), DecimalSI}}, - {"2.5P", Quantity{dec(25, 14), DecimalSI}}, - {"1.01E", Quantity{dec(101, 16), DecimalSI}}, + {"9.01", decQuantity(901, -2, DecimalSI)}, + {"8.1k", decQuantity(81, 2, DecimalSI)}, + {"7.123456M", decQuantity(7123456, 0, DecimalSI)}, + {"6.987654321G", decQuantity(6987654321, 0, DecimalSI)}, + {"5.444T", decQuantity(5444, 9, DecimalSI)}, + {"40.1T", decQuantity(401, 11, DecimalSI)}, + {"300.2T", decQuantity(3002, 11, DecimalSI)}, + {"2.5P", decQuantity(25, 14, DecimalSI)}, + {"1.01E", decQuantity(101, 16, DecimalSI)}, // Things that saturate/round - {"3.001n", Quantity{dec(4, -9), DecimalSI}}, - {"1.1E-9", Quantity{dec(2, -9), DecimalExponent}}, - {"0.0000000001", Quantity{dec(1, -9), DecimalSI}}, - {"0.0000000005", Quantity{dec(1, -9), DecimalSI}}, - {"0.00000000050", Quantity{dec(1, -9), DecimalSI}}, - {"0.5e-9", Quantity{dec(1, -9), DecimalExponent}}, - {"0.9n", Quantity{dec(1, -9), DecimalSI}}, - {"0.00000012345", Quantity{dec(124, -9), DecimalSI}}, - {"0.00000012354", Quantity{dec(124, -9), DecimalSI}}, - {"9Ei", Quantity{maxAllowed, BinarySI}}, - {"9223372036854775807Ki", Quantity{maxAllowed, BinarySI}}, - {"12E", Quantity{maxAllowed, DecimalSI}}, + {"3.001n", decQuantity(4, -9, DecimalSI)}, + {"1.1E-9", decQuantity(2, -9, DecimalExponent)}, + {"0.0000000001", decQuantity(1, -9, DecimalSI)}, + {"0.0000000005", decQuantity(1, -9, DecimalSI)}, + {"0.00000000050", decQuantity(1, -9, DecimalSI)}, + {"0.5e-9", decQuantity(1, -9, DecimalExponent)}, + {"0.9n", decQuantity(1, -9, DecimalSI)}, + {"0.00000012345", decQuantity(124, -9, DecimalSI)}, + {"0.00000012354", decQuantity(124, -9, DecimalSI)}, + {"9Ei", Quantity{d: maxAllowed, Format: BinarySI}}, + {"9223372036854775807Ki", Quantity{d: maxAllowed, Format: BinarySI}}, + {"12E", decQuantity(12, 18, DecimalSI)}, // We'll accept fractional binary stuff, too. - {"100.035Ki", Quantity{dec(10243584, -2), BinarySI}}, - {"0.5Mi", Quantity{dec(.5*1024*1024, 0), BinarySI}}, - {"0.05Gi", Quantity{dec(536870912, -1), BinarySI}}, - {"0.025Ti", Quantity{dec(274877906944, -1), BinarySI}}, + {"100.035Ki", decQuantity(10243584, -2, BinarySI)}, + {"0.5Mi", decQuantity(.5*1024*1024, 0, BinarySI)}, + {"0.05Gi", decQuantity(536870912, -1, BinarySI)}, + {"0.025Ti", decQuantity(274877906944, -1, BinarySI)}, // Things written by trolls - {"0.000000000001Ki", Quantity{dec(2, -9), DecimalSI}}, // rounds up, changes format - {".001", Quantity{dec(1, -3), DecimalSI}}, - {".0001k", Quantity{dec(100, -3), DecimalSI}}, - {"1.", Quantity{dec(1, 0), DecimalSI}}, - {"1.G", Quantity{dec(1, 9), DecimalSI}}, + {"0.000000000001Ki", decQuantity(2, -9, DecimalSI)}, // rounds up, changes format + {".001", decQuantity(1, -3, DecimalSI)}, + {".0001k", decQuantity(100, -3, DecimalSI)}, + {"1.", decQuantity(1, 0, DecimalSI)}, + {"1.G", decQuantity(1, 9, DecimalSI)}, } - for _, item := range table { - got, err := ParseQuantity(item.input) - if err != nil { - t.Errorf("%v: unexpected error: %v", item.input, err) - continue - } - if e, a := item.expect.Amount, got.Amount; e.Cmp(a) != 0 { - t.Errorf("%v: expected %v, got %v", item.input, e, a) - } - if e, a := item.expect.Format, got.Format; e != a { - t.Errorf("%v: expected %#v, got %#v", item.input, e, a) - } - } + for _, asDec := range []bool{false, true} { + for _, item := range table { + got, err := ParseQuantity(item.input) + if err != nil { + t.Errorf("%v: unexpected error: %v", item.input, err) + continue + } + if asDec { + got.AsDec() + } - // Try the negative version of everything - desired := &inf.Dec{} - for _, item := range table { - got, err := ParseQuantity("-" + item.input) - if err != nil { - t.Errorf("-%v: unexpected error: %v", item.input, err) - continue - } - desired.Neg(item.expect.Amount) - if e, a := desired, got.Amount; e.Cmp(a) != 0 { - t.Errorf("%v: expected %v, got %v", item.input, e, a) - } - if e, a := item.expect.Format, got.Format; e != a { - t.Errorf("%v: expected %#v, got %#v", item.input, e, a) - } - } + if e, a := item.expect, got; e.Cmp(a) != 0 { + t.Errorf("%v: expected %v, got %v", item.input, e.String(), a.String()) + } + if e, a := item.expect.Format, got.Format; e != a { + t.Errorf("%v: expected %#v, got %#v", item.input, e, a) + } - // Try everything with an explicit + - for _, item := range table { - got, err := ParseQuantity("+" + item.input) - if err != nil { - t.Errorf("-%v: unexpected error: %v", item.input, err) - continue + if asDec { + if i, ok := got.AsInt64(); i != 0 || ok { + t.Errorf("%v: expected inf.Dec to return false for AsInt64: %d", item.input, i) + } + continue + } + i, ok := item.expect.AsInt64() + if !ok { + continue + } + j, ok := got.AsInt64() + if !ok { + if got.d.Dec == nil && got.i.scale >= 0 { + t.Errorf("%v: is an int64Amount, but can't return AsInt64: %v", item.input, got) + } + continue + } + if i != j { + t.Errorf("%v: expected equivalent representation as int64: %d %d", item.input, i, j) + } } - if e, a := item.expect.Amount, got.Amount; e.Cmp(a) != 0 { - t.Errorf("%v: expected %v, got %v", item.input, e, a) + + for _, item := range table { + got, err := ParseQuantity(item.input) + if err != nil { + t.Errorf("%v: unexpected error: %v", item.input, err) + continue + } + + if asDec { + got.AsDec() + } + + // verify that we can decompose the input and get the same result by building up from the base. + positive, _, num, denom, suffix, err := parseQuantityString(item.input) + if err != nil { + t.Errorf("%v: unexpected error: %v", item.input, err) + continue + } + if got.Sign() >= 0 && !positive || got.Sign() < 0 && positive { + t.Errorf("%v: positive was incorrect: %t", item.input, positive) + continue + } + var value string + if !positive { + value = "-" + } + value += num + if len(denom) > 0 { + value += "." + denom + } + value += suffix + if len(value) == 0 { + t.Errorf("%v: did not parse correctly, %q %q %q", item.input, num, denom, suffix) + } + expected, err := ParseQuantity(value) + if err != nil { + t.Errorf("%v: unexpected error for %s: %v", item.input, value, err) + continue + } + if expected.Cmp(got) != 0 { + t.Errorf("%v: not the same as %s", item.input, value) + continue + } } - if e, a := item.expect.Format, got.Format; e != a { - t.Errorf("%v: expected %#v, got %#v", item.input, e, a) + + // Try the negative version of everything + desired := &inf.Dec{} + expect := Quantity{d: infDecAmount{Dec: desired}} + for _, item := range table { + got, err := ParseQuantity("-" + item.input) + if err != nil { + t.Errorf("-%v: unexpected error: %v", item.input, err) + continue + } + if asDec { + got.AsDec() + } + + expected := item.expect + desired.Neg(expected.AsDec()) + + if e, a := expect, got; e.Cmp(a) != 0 { + t.Errorf("%v: expected %s, got %s", item.input, e.String(), a.String()) + } + if e, a := expected.Format, got.Format; e != a { + t.Errorf("%v: expected %#v, got %#v", item.input, e, a) + } + } + + // Try everything with an explicit + + for _, item := range table { + got, err := ParseQuantity("+" + item.input) + if err != nil { + t.Errorf("-%v: unexpected error: %v", item.input, err) + continue + } + if asDec { + got.AsDec() + } + + if e, a := item.expect, got; e.Cmp(a) != 0 { + t.Errorf("%v(%t): expected %s, got %s", item.input, asDec, e.String(), a.String()) + } + if e, a := item.expect.Format, got.Format; e != a { + t.Errorf("%v: expected %#v, got %#v", item.input, e, a) + } } } @@ -359,6 +471,7 @@ func TestQuantityParse(t *testing.T) { ".5i", "1i", "-3.01i", + "-3.01e-", } for _, item := range invalid { _, err := ParseQuantity(item) @@ -368,53 +481,193 @@ func TestQuantityParse(t *testing.T) { } } +func TestQuantityRoundUp(t *testing.T) { + table := []struct { + in string + scale Scale + expect Quantity + ok bool + }{ + {"9.01", -3, decQuantity(901, -2, DecimalSI), true}, + {"9.01", -2, decQuantity(901, -2, DecimalSI), true}, + {"9.01", -1, decQuantity(91, -1, DecimalSI), false}, + {"9.01", 0, decQuantity(10, 0, DecimalSI), false}, + {"9.01", 1, decQuantity(10, 0, DecimalSI), false}, + {"9.01", 2, decQuantity(100, 0, DecimalSI), false}, + + {"-9.01", -3, decQuantity(-901, -2, DecimalSI), true}, + {"-9.01", -2, decQuantity(-901, -2, DecimalSI), true}, + {"-9.01", -1, decQuantity(-91, -1, DecimalSI), false}, + {"-9.01", 0, decQuantity(-10, 0, DecimalSI), false}, + {"-9.01", 1, decQuantity(-10, 0, DecimalSI), false}, + {"-9.01", 2, decQuantity(-100, 0, DecimalSI), false}, + } + + for _, asDec := range []bool{false, true} { + for _, item := range table { + got, err := ParseQuantity(item.in) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + expect := *item.expect.Copy() + if asDec { + got.AsDec() + } + if ok := got.RoundUp(item.scale); ok != item.ok { + t.Errorf("%s(%d,%t): unexpected ok: %t", item.in, item.scale, asDec, ok) + } + if got.Cmp(expect) != 0 { + t.Errorf("%s(%d,%t): unexpected round: %s vs %s", item.in, item.scale, asDec, got.String(), expect.String()) + } + } + } +} + +func TestQuantityCmpInt64AndDec(t *testing.T) { + table := []struct { + a, b Quantity + cmp int + }{ + {intQuantity(901, -2, DecimalSI), intQuantity(901, -2, DecimalSI), 0}, + {intQuantity(90, -1, DecimalSI), intQuantity(901, -2, DecimalSI), -1}, + {intQuantity(901, -2, DecimalSI), intQuantity(900, -2, DecimalSI), 1}, + {intQuantity(0, 0, DecimalSI), intQuantity(0, 0, DecimalSI), 0}, + {intQuantity(0, 1, DecimalSI), intQuantity(0, -1, DecimalSI), 0}, + {intQuantity(0, -1, DecimalSI), intQuantity(0, 1, DecimalSI), 0}, + {intQuantity(800, -3, DecimalSI), intQuantity(1, 0, DecimalSI), -1}, + {intQuantity(800, -3, DecimalSI), intQuantity(79, -2, DecimalSI), 1}, + + {intQuantity(mostPositive, 0, DecimalSI), intQuantity(1, -1, DecimalSI), 1}, + {intQuantity(mostPositive, 1, DecimalSI), intQuantity(1, 0, DecimalSI), 1}, + {intQuantity(mostPositive, 1, DecimalSI), intQuantity(1, 1, DecimalSI), 1}, + {intQuantity(mostPositive, 1, DecimalSI), intQuantity(0, 1, DecimalSI), 1}, + {intQuantity(mostPositive, -16, DecimalSI), intQuantity(1, 3, DecimalSI), -1}, + + {intQuantity(mostNegative, 0, DecimalSI), intQuantity(0, 0, DecimalSI), -1}, + {intQuantity(mostNegative, -18, DecimalSI), intQuantity(-1, 0, DecimalSI), -1}, + {intQuantity(mostNegative, -19, DecimalSI), intQuantity(-1, 0, DecimalSI), 1}, + + {intQuantity(1*1000000*1000000*1000000, -17, DecimalSI), intQuantity(1, 1, DecimalSI), 0}, + {intQuantity(1*1000000*1000000*1000000, -17, DecimalSI), intQuantity(-10, 0, DecimalSI), 1}, + {intQuantity(-1*1000000*1000000*1000000, -17, DecimalSI), intQuantity(-10, 0, DecimalSI), 0}, + {intQuantity(1*1000000*1000000*1000000, -17, DecimalSI), intQuantity(1, 0, DecimalSI), 1}, + + {intQuantity(1*1000000*1000000*1000000+1, -17, DecimalSI), intQuantity(1, 1, DecimalSI), 1}, + {intQuantity(1*1000000*1000000*1000000-1, -17, DecimalSI), intQuantity(1, 1, DecimalSI), -1}, + } + + for _, item := range table { + if cmp := item.a.Cmp(item.b); cmp != item.cmp { + t.Errorf("%#v: unexpected Cmp: %d", item, cmp) + } + if cmp := item.b.Cmp(item.a); cmp != -item.cmp { + t.Errorf("%#v: unexpected inverted Cmp: %d", item, cmp) + } + } + + for _, item := range table { + a, b := *item.a.Copy(), *item.b.Copy() + a.AsDec() + if cmp := a.Cmp(b); cmp != item.cmp { + t.Errorf("%#v: unexpected Cmp: %d", item, cmp) + } + if cmp := b.Cmp(a); cmp != -item.cmp { + t.Errorf("%#v: unexpected inverted Cmp: %d", item, cmp) + } + } + + for _, item := range table { + a, b := *item.a.Copy(), *item.b.Copy() + b.AsDec() + if cmp := a.Cmp(b); cmp != item.cmp { + t.Errorf("%#v: unexpected Cmp: %d", item, cmp) + } + if cmp := b.Cmp(a); cmp != -item.cmp { + t.Errorf("%#v: unexpected inverted Cmp: %d", item, cmp) + } + } + + for _, item := range table { + a, b := *item.a.Copy(), *item.b.Copy() + a.AsDec() + b.AsDec() + if cmp := a.Cmp(b); cmp != item.cmp { + t.Errorf("%#v: unexpected Cmp: %d", item, cmp) + } + if cmp := b.Cmp(a); cmp != -item.cmp { + t.Errorf("%#v: unexpected inverted Cmp: %d", item, cmp) + } + } +} + +func TestQuantityNeg(t *testing.T) { + table := []struct { + a Quantity + out string + }{ + {intQuantity(901, -2, DecimalSI), "-9010m"}, + {decQuantity(901, -2, DecimalSI), "-9010m"}, + } + + for i, item := range table { + out := *item.a.Copy() + out.Neg() + if out.Cmp(item.a) == 0 { + t.Errorf("%d: negating an item should not mutate the source: %s", i, out.String()) + } + if out.String() != item.out { + t.Errorf("%d: negating did not equal exact value: %s", i, out.String()) + } + } +} + func TestQuantityString(t *testing.T) { table := []struct { in Quantity expect string }{ - {Quantity{dec(1024*1024*1024, 0), BinarySI}, "1Gi"}, - {Quantity{dec(300*1024*1024, 0), BinarySI}, "300Mi"}, - {Quantity{dec(6*1024, 0), BinarySI}, "6Ki"}, - {Quantity{dec(1001*1024*1024*1024, 0), BinarySI}, "1001Gi"}, - {Quantity{dec(1024*1024*1024*1024, 0), BinarySI}, "1Ti"}, - {Quantity{dec(5, 0), BinarySI}, "5"}, - {Quantity{dec(500, -3), BinarySI}, "500m"}, - {Quantity{dec(1, 9), DecimalSI}, "1G"}, - {Quantity{dec(1000, 6), DecimalSI}, "1G"}, - {Quantity{dec(1000000, 3), DecimalSI}, "1G"}, - {Quantity{dec(1000000000, 0), DecimalSI}, "1G"}, - {Quantity{dec(1, -3), DecimalSI}, "1m"}, - {Quantity{dec(80, -3), DecimalSI}, "80m"}, - {Quantity{dec(1080, -3), DecimalSI}, "1080m"}, - {Quantity{dec(108, -2), DecimalSI}, "1080m"}, - {Quantity{dec(10800, -4), DecimalSI}, "1080m"}, - {Quantity{dec(300, 6), DecimalSI}, "300M"}, - {Quantity{dec(1, 12), DecimalSI}, "1T"}, - {Quantity{dec(1234567, 6), DecimalSI}, "1234567M"}, - {Quantity{dec(1234567, -3), BinarySI}, "1234567m"}, - {Quantity{dec(3, 3), DecimalSI}, "3k"}, - {Quantity{dec(1025, 0), BinarySI}, "1025"}, - {Quantity{dec(0, 0), DecimalSI}, "0"}, - {Quantity{dec(0, 0), BinarySI}, "0"}, - {Quantity{dec(1, 9), DecimalExponent}, "1e9"}, - {Quantity{dec(1, -3), DecimalExponent}, "1e-3"}, - {Quantity{dec(1, -9), DecimalExponent}, "1e-9"}, - {Quantity{dec(80, -3), DecimalExponent}, "80e-3"}, - {Quantity{dec(300, 6), DecimalExponent}, "300e6"}, - {Quantity{dec(1, 12), DecimalExponent}, "1e12"}, - {Quantity{dec(1, 3), DecimalExponent}, "1e3"}, - {Quantity{dec(3, 3), DecimalExponent}, "3e3"}, - {Quantity{dec(3, 3), DecimalSI}, "3k"}, - {Quantity{dec(0, 0), DecimalExponent}, "0"}, - {Quantity{dec(1, -9), DecimalSI}, "1n"}, - {Quantity{dec(80, -9), DecimalSI}, "80n"}, - {Quantity{dec(1080, -9), DecimalSI}, "1080n"}, - {Quantity{dec(108, -8), DecimalSI}, "1080n"}, - {Quantity{dec(10800, -10), DecimalSI}, "1080n"}, - {Quantity{dec(1, -6), DecimalSI}, "1u"}, - {Quantity{dec(80, -6), DecimalSI}, "80u"}, - {Quantity{dec(1080, -6), DecimalSI}, "1080u"}, + {decQuantity(1024*1024*1024, 0, BinarySI), "1Gi"}, + {decQuantity(300*1024*1024, 0, BinarySI), "300Mi"}, + {decQuantity(6*1024, 0, BinarySI), "6Ki"}, + {decQuantity(1001*1024*1024*1024, 0, BinarySI), "1001Gi"}, + {decQuantity(1024*1024*1024*1024, 0, BinarySI), "1Ti"}, + {decQuantity(5, 0, BinarySI), "5"}, + {decQuantity(500, -3, BinarySI), "500m"}, + {decQuantity(1, 9, DecimalSI), "1G"}, + {decQuantity(1000, 6, DecimalSI), "1G"}, + {decQuantity(1000000, 3, DecimalSI), "1G"}, + {decQuantity(1000000000, 0, DecimalSI), "1G"}, + {decQuantity(1, -3, DecimalSI), "1m"}, + {decQuantity(80, -3, DecimalSI), "80m"}, + {decQuantity(1080, -3, DecimalSI), "1080m"}, + {decQuantity(108, -2, DecimalSI), "1080m"}, + {decQuantity(10800, -4, DecimalSI), "1080m"}, + {decQuantity(300, 6, DecimalSI), "300M"}, + {decQuantity(1, 12, DecimalSI), "1T"}, + {decQuantity(1234567, 6, DecimalSI), "1234567M"}, + {decQuantity(1234567, -3, BinarySI), "1234567m"}, + {decQuantity(3, 3, DecimalSI), "3k"}, + {decQuantity(1025, 0, BinarySI), "1025"}, + {decQuantity(0, 0, DecimalSI), "0"}, + {decQuantity(0, 0, BinarySI), "0"}, + {decQuantity(1, 9, DecimalExponent), "1e9"}, + {decQuantity(1, -3, DecimalExponent), "1e-3"}, + {decQuantity(1, -9, DecimalExponent), "1e-9"}, + {decQuantity(80, -3, DecimalExponent), "80e-3"}, + {decQuantity(300, 6, DecimalExponent), "300e6"}, + {decQuantity(1, 12, DecimalExponent), "1e12"}, + {decQuantity(1, 3, DecimalExponent), "1e3"}, + {decQuantity(3, 3, DecimalExponent), "3e3"}, + {decQuantity(3, 3, DecimalSI), "3k"}, + {decQuantity(0, 0, DecimalExponent), "0"}, + {decQuantity(1, -9, DecimalSI), "1n"}, + {decQuantity(80, -9, DecimalSI), "80n"}, + {decQuantity(1080, -9, DecimalSI), "1080n"}, + {decQuantity(108, -8, DecimalSI), "1080n"}, + {decQuantity(10800, -10, DecimalSI), "1080n"}, + {decQuantity(1, -6, DecimalSI), "1u"}, + {decQuantity(80, -6, DecimalSI), "80u"}, + {decQuantity(1080, -6, DecimalSI), "1080u"}, } for _, item := range table { got := item.in.String() @@ -424,12 +677,12 @@ func TestQuantityString(t *testing.T) { } desired := &inf.Dec{} // Avoid modifying the values in the table. for _, item := range table { - if item.in.Amount.Cmp(decZero) == 0 { + if item.in.Cmp(Quantity{}) == 0 { // Don't expect it to print "-0" ever continue } q := item.in - q.Amount = desired.Neg(q.Amount) + q.d = infDecAmount{desired.Neg(q.AsDec())} if e, a := "-"+item.expect, q.String(); e != a { t.Errorf("%#v: expected %v, got %v", item.in, e, a) } @@ -468,28 +721,32 @@ func TestQuantityParseEmit(t *testing.T) { t.Errorf("Couldn't parse %v", item.in) continue } - if q.Amount.Cmp(decZero) == 0 { + if q.Cmp(Quantity{}) == 0 { continue } if e, a := "-"+item.expect, q.String(); e != a { - t.Errorf("%#v: expected %v, got %v", item.in, e, a) + t.Errorf("%#v: expected %v, got %v (%#v)", item.in, e, a, q.i) } } } var fuzzer = fuzz.New().Funcs( func(q *Quantity, c fuzz.Continue) { - q.Amount = &inf.Dec{} + q.i = Zero if c.RandBool() { q.Format = BinarySI if c.RandBool() { - q.Amount.SetScale(0) - q.Amount.SetUnscaled(c.Int63()) + dec := &inf.Dec{} + q.d = infDecAmount{Dec: dec} + dec.SetScale(0) + dec.SetUnscaled(c.Int63()) return } // Be sure to test cases like 1Mi - q.Amount.SetScale(0) - q.Amount.SetUnscaled(c.Int63n(1024) << uint(10*c.Intn(5))) + dec := &inf.Dec{} + q.d = infDecAmount{Dec: dec} + dec.SetScale(0) + dec.SetUnscaled(c.Int63n(1024) << uint(10*c.Intn(5))) return } if c.RandBool() { @@ -498,13 +755,17 @@ var fuzzer = fuzz.New().Funcs( q.Format = DecimalExponent } if c.RandBool() { - q.Amount.SetScale(inf.Scale(c.Intn(4))) - q.Amount.SetUnscaled(c.Int63()) + dec := &inf.Dec{} + q.d = infDecAmount{Dec: dec} + dec.SetScale(inf.Scale(c.Intn(4))) + dec.SetUnscaled(c.Int63()) return } // Be sure to test cases like 1M - q.Amount.SetScale(inf.Scale(3 - c.Intn(15))) - q.Amount.SetUnscaled(c.Int63n(1000)) + dec := &inf.Dec{} + q.d = infDecAmount{Dec: dec} + dec.SetScale(inf.Scale(3 - c.Intn(15))) + dec.SetUnscaled(c.Int63n(1000)) }, ) @@ -514,14 +775,16 @@ func TestJSON(t *testing.T) { fuzzer.Fuzz(q) b, err := json.Marshal(q) if err != nil { - t.Errorf("error encoding %v", q) + t.Errorf("error encoding %v: %v", q, err) + continue } q2 := &Quantity{} err = json.Unmarshal(b, q2) if err != nil { - t.Errorf("%v: error decoding %v", q, string(b)) + t.Logf("%d: %s", i, string(b)) + t.Errorf("%v: error decoding %v: %v", q, string(b), err) } - if q2.Amount.Cmp(q.Amount) != 0 { + if q2.Cmp(*q) != 0 { t.Errorf("Expected equal: %v, %v (json was '%v')", q, q2, string(b)) } } @@ -582,25 +845,33 @@ func TestNewSet(t *testing.T) { {1024 * 1024, BinarySI, "1Mi"}, } - for _, item := range table { - q := NewQuantity(item.value, item.format) - if e, a := item.expect, q.String(); e != a { - t.Errorf("Expected %v, got %v; %#v", e, a, q) + for _, asDec := range []bool{false, true} { + for _, item := range table { + q := NewQuantity(item.value, item.format) + if asDec { + q.ToDec() + } + if e, a := item.expect, q.String(); e != a { + t.Errorf("Expected %v, got %v; %#v", e, a, q) + } + q2, err := ParseQuantity(q.String()) + if err != nil { + t.Errorf("Round trip failed on %v", q) + } + if e, a := item.value, q2.Value(); e != a { + t.Errorf("Expected %v, got %v", e, a) + } } - q2, err := ParseQuantity(q.String()) - if err != nil { - t.Errorf("Round trip failed on %v", q) - } - if e, a := item.value, q2.Value(); e != a { - t.Errorf("Expected %v, got %v", e, a) - } - } - for _, item := range table { - q := NewQuantity(0, item.format) - q.Set(item.value) - if e, a := item.expect, q.String(); e != a { - t.Errorf("Set: Expected %v, got %v; %#v", e, a, q) + for _, item := range table { + q := NewQuantity(0, item.format) + q.Set(item.value) + if asDec { + q.ToDec() + } + if e, a := item.expect, q.String(); e != a { + t.Errorf("Set: Expected %v, got %v; %#v", e, a, q) + } } } } @@ -716,12 +987,12 @@ func TestSub(t *testing.T) { b Quantity expected Quantity }{ - {Quantity{dec(10, 0), DecimalSI}, Quantity{dec(1, 1), DecimalSI}, Quantity{dec(0, 0), DecimalSI}}, - {Quantity{dec(10, 0), DecimalSI}, Quantity{dec(1, 0), BinarySI}, Quantity{dec(9, 0), DecimalSI}}, - {Quantity{dec(10, 0), BinarySI}, Quantity{dec(1, 0), DecimalSI}, Quantity{dec(9, 0), BinarySI}}, - {Quantity{nil, DecimalSI}, Quantity{dec(50, 0), DecimalSI}, Quantity{dec(-50, 0), DecimalSI}}, - {Quantity{dec(50, 0), DecimalSI}, Quantity{nil, DecimalSI}, Quantity{dec(50, 0), DecimalSI}}, - {Quantity{nil, DecimalSI}, Quantity{nil, DecimalSI}, Quantity{dec(0, 0), DecimalSI}}, + {decQuantity(10, 0, DecimalSI), decQuantity(1, 1, DecimalSI), decQuantity(0, 0, DecimalSI)}, + {decQuantity(10, 0, DecimalSI), decQuantity(1, 0, BinarySI), decQuantity(9, 0, DecimalSI)}, + {decQuantity(10, 0, BinarySI), decQuantity(1, 0, DecimalSI), decQuantity(9, 0, BinarySI)}, + {Quantity{Format: DecimalSI}, decQuantity(50, 0, DecimalSI), decQuantity(-50, 0, DecimalSI)}, + {decQuantity(50, 0, DecimalSI), Quantity{Format: DecimalSI}, decQuantity(50, 0, DecimalSI)}, + {Quantity{Format: DecimalSI}, Quantity{Format: DecimalSI}, decQuantity(0, 0, DecimalSI)}, } for i, test := range tests { @@ -738,52 +1009,21 @@ func TestNeg(t *testing.T) { b Quantity expected Quantity }{ - { - a: Quantity{dec(0, 0), DecimalSI}, - b: Quantity{dec(10, 0), DecimalSI}, - expected: Quantity{dec(-10, 0), DecimalSI}, - }, - { - a: Quantity{dec(0, 0), DecimalSI}, - b: Quantity{dec(-10, 0), DecimalSI}, - expected: Quantity{dec(10, 0), DecimalSI}, - }, - { - a: Quantity{dec(0, 0), DecimalSI}, - b: Quantity{dec(10, 0), BinarySI}, - expected: Quantity{dec(-10, 0), BinarySI}, - }, - { - a: Quantity{dec(0, 0), DecimalSI}, - b: Quantity{dec(0, 0), BinarySI}, - expected: Quantity{dec(0, 0), BinarySI}, - }, - { - a: Quantity{}, - b: Quantity{dec(10, 0), BinarySI}, - expected: Quantity{dec(-10, 0), BinarySI}, - }, - { - a: Quantity{dec(10, 0), BinarySI}, - b: Quantity{}, - expected: Quantity{}, - }, - { - a: Quantity{dec(10, 0), BinarySI}, - b: Quantity{Format: DecimalSI}, - expected: Quantity{dec(0, 0), DecimalSI}, - }, + {a: intQuantity(0, 0, DecimalSI), expected: intQuantity(0, 0, DecimalSI)}, + {a: Quantity{}, expected: Quantity{}}, + {a: intQuantity(10, 0, BinarySI), expected: intQuantity(-10, 0, BinarySI)}, + {a: intQuantity(-10, 0, BinarySI), expected: intQuantity(10, 0, BinarySI)}, + {a: decQuantity(0, 0, DecimalSI), expected: intQuantity(0, 0, DecimalSI)}, + {a: decQuantity(10, 0, BinarySI), expected: intQuantity(-10, 0, BinarySI)}, + {a: decQuantity(-10, 0, BinarySI), expected: intQuantity(10, 0, BinarySI)}, } for i, test := range tests { - test.a.Neg(test.b) + a := test.a.Copy() + a.Neg() // ensure value is same - if test.a.Cmp(test.expected) != 0 { - t.Errorf("[%d] Expected %q, got %q", i, test.expected.String(), test.a.String()) - } - // ensure format is updated - if test.a.Format != test.expected.Format { - t.Errorf("[%d] Expected format %v, got format %v", i, test.expected.Format, test.a.Format) + if a.Cmp(test.expected) != 0 { + t.Errorf("[%d] Expected %q, got %q", i, test.expected.String(), a.String()) } } } @@ -794,12 +1034,12 @@ func TestAdd(t *testing.T) { b Quantity expected Quantity }{ - {Quantity{dec(10, 0), DecimalSI}, Quantity{dec(1, 1), DecimalSI}, Quantity{dec(20, 0), DecimalSI}}, - {Quantity{dec(10, 0), DecimalSI}, Quantity{dec(1, 0), BinarySI}, Quantity{dec(11, 0), DecimalSI}}, - {Quantity{dec(10, 0), BinarySI}, Quantity{dec(1, 0), DecimalSI}, Quantity{dec(11, 0), BinarySI}}, - {Quantity{nil, DecimalSI}, Quantity{dec(50, 0), DecimalSI}, Quantity{dec(50, 0), DecimalSI}}, - {Quantity{dec(50, 0), DecimalSI}, Quantity{nil, DecimalSI}, Quantity{dec(50, 0), DecimalSI}}, - {Quantity{nil, DecimalSI}, Quantity{nil, DecimalSI}, Quantity{dec(0, 0), DecimalSI}}, + {decQuantity(10, 0, DecimalSI), decQuantity(1, 1, DecimalSI), decQuantity(20, 0, DecimalSI)}, + {decQuantity(10, 0, DecimalSI), decQuantity(1, 0, BinarySI), decQuantity(11, 0, DecimalSI)}, + {decQuantity(10, 0, BinarySI), decQuantity(1, 0, DecimalSI), decQuantity(11, 0, BinarySI)}, + {Quantity{Format: DecimalSI}, decQuantity(50, 0, DecimalSI), decQuantity(50, 0, DecimalSI)}, + {decQuantity(50, 0, DecimalSI), Quantity{Format: DecimalSI}, decQuantity(50, 0, DecimalSI)}, + {Quantity{Format: DecimalSI}, Quantity{Format: DecimalSI}, decQuantity(0, 0, DecimalSI)}, } for i, test := range tests { @@ -810,18 +1050,73 @@ func TestAdd(t *testing.T) { } } +func TestAddSubRoundTrip(t *testing.T) { + for k := -10; k <= 10; k++ { + q := Quantity{Format: DecimalSI} + var order []int64 + for i := 0; i < 100; i++ { + j := rand.Int63() + order = append(order, j) + q.Add(*NewScaledQuantity(j, Scale(k))) + } + for _, j := range order { + q.Sub(*NewScaledQuantity(j, Scale(k))) + } + if !q.IsZero() { + t.Errorf("addition and subtraction did not cancel: %s", &q) + } + } +} + +func TestAddSubRoundTripAcrossScales(t *testing.T) { + q := Quantity{Format: DecimalSI} + var order []int64 + for i := 0; i < 100; i++ { + j := rand.Int63() + order = append(order, j) + q.Add(*NewScaledQuantity(j, Scale(j%20-10))) + } + for _, j := range order { + q.Sub(*NewScaledQuantity(j, Scale(j%20-10))) + } + if !q.IsZero() { + t.Errorf("addition and subtraction did not cancel: %s", &q) + } +} + +func TestNegateRoundTrip(t *testing.T) { + for _, asDec := range []bool{false, true} { + for k := -10; k <= 10; k++ { + for i := 0; i < 100; i++ { + j := rand.Int63() + q := *NewScaledQuantity(j, Scale(k)) + if asDec { + q.AsDec() + } + + b := q.Copy() + b.Neg() + b.Neg() + if b.Cmp(q) != 0 { + t.Errorf("double negation did not cancel: %s", &q) + } + } + } + } +} func benchmarkQuantities() []Quantity { return []Quantity{ - {dec(1024*1024*1024, 0), BinarySI}, - {dec(1024*1024*1024*1024, 0), BinarySI}, - {dec(1000000, 3), DecimalSI}, - {dec(1000000000, 0), DecimalSI}, - {dec(1, -3), DecimalSI}, - {dec(80, -3), DecimalSI}, - {dec(1080, -3), DecimalSI}, - {dec(0, 0), BinarySI}, - {dec(1, 9), DecimalExponent}, - {dec(1, -9), DecimalSI}, + intQuantity(1024*1024*1024, 0, BinarySI), + intQuantity(1024*1024*1024*1024, 0, BinarySI), + intQuantity(1000000, 3, DecimalSI), + intQuantity(1000000000, 0, DecimalSI), + intQuantity(1, -3, DecimalSI), + intQuantity(80, -3, DecimalSI), + intQuantity(1080, -3, DecimalSI), + intQuantity(0, 0, BinarySI), + intQuantity(1, 9, DecimalExponent), + intQuantity(1, -9, DecimalSI), + intQuantity(1000000, 10, DecimalSI), } } @@ -830,7 +1125,27 @@ func BenchmarkQuantityString(b *testing.B) { b.ResetTimer() var s string for i := 0; i < b.N; i++ { - s = values[i%len(values)].String() + q := values[i%len(values)] + q.s = nil + s = q.String() + } + b.StopTimer() + if len(s) == 0 { + b.Fatal(s) + } +} + +func BenchmarkQuantityStringBinarySI(b *testing.B) { + values := benchmarkQuantities() + for i := range values { + values[i].Format = BinarySI + } + b.ResetTimer() + var s string + for i := 0; i < b.N; i++ { + q := values[i%len(values)] + q.s = nil + s = q.String() } b.StopTimer() if len(s) == 0 { @@ -842,7 +1157,9 @@ func BenchmarkQuantityMarshalJSON(b *testing.B) { values := benchmarkQuantities() b.ResetTimer() for i := 0; i < b.N; i++ { - if _, err := values[i%len(values)].MarshalJSON(); err != nil { + q := values[i%len(values)] + q.s = nil + if _, err := q.MarshalJSON(); err != nil { b.Fatal(err) } } @@ -885,8 +1202,9 @@ func BenchmarkParseQuantity(b *testing.B) { func BenchmarkCanonicalize(b *testing.B) { values := benchmarkQuantities() b.ResetTimer() + buffer := make([]byte, 0, 100) for i := 0; i < b.N; i++ { - s, _ := values[i%len(values)].Canonicalize() + s, _ := values[i%len(values)].CanonicalizeBytes(buffer) if len(s) == 0 { b.Fatal(s) } @@ -894,31 +1212,40 @@ func BenchmarkCanonicalize(b *testing.B) { b.StopTimer() } +func BenchmarkQuantityRoundUp(b *testing.B) { + values := benchmarkQuantities() + b.ResetTimer() + for i := 0; i < b.N; i++ { + q := values[i%len(values)] + copied := q + copied.RoundUp(-3) + } + b.StopTimer() +} + func BenchmarkQuantityCopy(b *testing.B) { values := benchmarkQuantities() b.ResetTimer() for i := 0; i < b.N; i++ { - q := values[i%len(values)].Copy() - if q.Amount == nil { - b.Fatal(q) - } + values[i%len(values)].Copy() } b.StopTimer() } func BenchmarkQuantityAdd(b *testing.B) { values := benchmarkQuantities() + base := &Quantity{} b.ResetTimer() for i := 0; i < b.N; i++ { q := values[i%len(values)] - if err := q.Add(q); err != nil { - b.Fatal(err) - } + base.d.Dec = nil + base.i = int64Amount{value: 100} + base.Add(q) } b.StopTimer() } -func BenchmarkQuantityRound(b *testing.B) { +func BenchmarkQuantityCmp(b *testing.B) { values := benchmarkQuantities() b.ResetTimer() for i := 0; i < b.N; i++ { diff --git a/pkg/api/resource/suffix.go b/pkg/api/resource/suffix.go index 529712365d7..0aa2ce2bf60 100644 --- a/pkg/api/resource/suffix.go +++ b/pkg/api/resource/suffix.go @@ -24,8 +24,9 @@ type suffix string // suffixer can interpret and construct suffixes. type suffixer interface { - interpret(suffix) (base, exponent int, fmt Format, ok bool) - construct(base, exponent int, fmt Format) (s suffix, ok bool) + interpret(suffix) (base, exponent int32, fmt Format, 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 @@ -33,12 +34,13 @@ type suffixer interface { var quantitySuffixer = newSuffixer() type bePair struct { - base, exponent int + base, exponent int32 } type listSuffixer struct { - suffixToBE map[suffix]bePair - beToSuffix map[bePair]suffix + suffixToBE map[suffix]bePair + beToSuffix map[bePair]suffix + beToSuffixBytes map[bePair][]byte } func (ls *listSuffixer) addSuffix(s suffix, pair bePair) { @@ -48,11 +50,15 @@ func (ls *listSuffixer) addSuffix(s suffix, pair bePair) { if ls.beToSuffix == nil { ls.beToSuffix = map[bePair]suffix{} } + if ls.beToSuffixBytes == nil { + ls.beToSuffixBytes = map[bePair][]byte{} + } ls.suffixToBE[s] = pair 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] if !ok { 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 } -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}] return } +func (ls *listSuffixer) constructBytes(base, exponent int32) (s []byte, ok bool) { + s, ok = ls.beToSuffixBytes[bePair{base, exponent}] + return +} + type suffixHandler struct { decSuffixes 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 { sh := &suffixHandler{} + // IMPORTANT: if you change this section you must change fastLookup + sh.binSuffixes.addSuffix("Ki", bePair{2, 10}) sh.binSuffixes.addSuffix("Mi", bePair{2, 20}) sh.binSuffixes.addSuffix("Gi", bePair{2, 30}) @@ -94,10 +131,10 @@ func newSuffixer() suffixer { sh.decSuffixes.addSuffix("P", bePair{10, 15}) 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 { case DecimalSI: return sh.decSuffixes.construct(base, exponent) @@ -115,7 +152,32 @@ func (sh *suffixHandler) construct(base, exponent int, fmt Format) (s suffix, ok 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 if b, e, ok := sh.decSuffixes.lookup(suffix); ok { return b, e, DecimalSI, true @@ -129,7 +191,7 @@ func (sh *suffixHandler) interpret(suffix suffix) (base, exponent int, fmt Forma if err != nil { return 0, 0, DecimalExponent, false } - return 10, int(parsed), DecimalExponent, true + return 10, int32(parsed), DecimalExponent, true } return 0, 0, DecimalExponent, false