From 40e90886e6599575a08e3ab0b7b254c877b65ac7 Mon Sep 17 00:00:00 2001 From: "Tim St. Clair" Date: Mon, 4 Jan 2016 14:08:24 -0800 Subject: [PATCH] Add micro- and nano- suffixes --- pkg/api/resource/quantity.go | 96 ++++++++++++++------- pkg/api/resource/quantity_test.go | 131 ++++++++++++++++++++++++++--- pkg/api/resource/scale_int_test.go | 2 +- pkg/api/resource/suffix.go | 2 + 4 files changed, 187 insertions(+), 44 deletions(-) diff --git a/pkg/api/resource/quantity.go b/pkg/api/resource/quantity.go index 4c6d669e523..19d497df829 100644 --- a/pkg/api/resource/quantity.go +++ b/pkg/api/resource/quantity.go @@ -117,10 +117,27 @@ func MustParse(str string) Quantity { 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. - splitREString = "^([+-]?[0-9.]+)([eEimkKMGTP]*[-+]?[0-9]*)$" + splitREString = "^([+-]?[0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$" ) var ( @@ -176,7 +193,7 @@ func ParseQuantity(str string) (*Quantity, error) { // So that no one but us has to think about suffixes, remove it. if base == 10 { - amount.SetScale(amount.Scale() + inf.Scale(-exponent)) + amount.SetScale(amount.Scale() + Scale(exponent).infScale()) } else if base == 2 { // numericSuffix = 2 ** exponent numericSuffix := big.NewInt(1).Lsh(bigOne, uint(exponent)) @@ -189,14 +206,13 @@ func ParseQuantity(str string) (*Quantity, error) { if sign == -1 { amount.Neg(amount) } - // This rounds non-zero values up to the minimum representable - // value, under the theory that if you want some resources, you - // should get some resources, even if you asked for way too small - // of an amount. - // Arguably, this should be inf.RoundHalfUp (normal rounding), but - // that would have the side effect of rounding values < .5m to zero. + + // This rounds non-zero values up to the minimum representable value, under the theory that + // if you want some resources, you should get some resources, even if you asked for way too small + // of an amount. Arguably, this should be inf.RoundHalfUp (normal rounding), but that would have + // the side effect of rounding values < .5n to zero. if v, ok := amount.Unscaled(); v != int64(0) || !ok { - amount.Round(amount, 3, inf.RoundUp) + amount.Round(amount, Nano.infScale(), inf.RoundUp) } // The max is just a simple cap. @@ -313,18 +329,16 @@ func (q *Quantity) String() string { // +1 if q > y // func (q *Quantity) Cmp(y Quantity) int { - num1 := q.Value() - num2 := y.Value() - if num1 < MaxMilliValue && num2 < MaxMilliValue { - num1 = q.MilliValue() - num2 = y.MilliValue() + if q.Amount == nil { + if y.Amount == nil { + return 0 + } + return -y.Amount.Sign() } - if num1 < num2 { - return -1 - } else if num1 > num2 { - return 1 + if y.Amount == nil { + return q.Amount.Sign() } - return 0 + return q.Amount.Cmp(y.Amount) } func (q *Quantity) Add(y Quantity) error { @@ -390,39 +404,52 @@ func NewMilliQuantity(value int64, format Format) *Quantity { } } -// Value returns the value of q; any fractional part will be lost. -func (q *Quantity) Value() int64 { - if q.Amount == nil { - return 0 +// NewScaledQuantity returns a new Quantity representing the given +// value * 10^scale in DecimalSI format. +func NewScaledQuantity(value int64, scale Scale) *Quantity { + return &Quantity{ + Amount: inf.NewDec(value, scale.infScale()), + Format: DecimalSI, } - return scaledValue(q.Amount.UnscaledBig(), int(q.Amount.Scale()), 0) } -// MilliValue returns the value of q * 1000; this could overflow an int64; +// Value returns the value of q; any fractional part will be lost. +func (q *Quantity) Value() int64 { + return q.ScaledValue(0) +} + +// MilliValue returns the value of ceil(q * 1000); this could overflow an int64; // if that's a concern, call Value() first to verify the number is small enough. func (q *Quantity) MilliValue() int64 { + return q.ScaledValue(Milli) +} + +// 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 } - return scaledValue(q.Amount.UnscaledBig(), int(q.Amount.Scale()), 3) + return scaledValue(q.Amount.UnscaledBig(), int(q.Amount.Scale()), int(scale.infScale())) } // Set sets q's value to be value. func (q *Quantity) Set(value int64) { - if q.Amount == nil { - q.Amount = &inf.Dec{} - } - q.Amount.SetUnscaled(value) - q.Amount.SetScale(0) + q.SetScaled(value, 0) } // SetMilli sets q's value to be value * 1/1000. func (q *Quantity) SetMilli(value int64) { + q.SetScaled(value, Milli) +} + +// 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(3) + q.Amount.SetScale(scale.infScale()) } // Copy is a convenience function that makes a deep copy for you. Non-deep @@ -477,3 +504,8 @@ 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_test.go b/pkg/api/resource/quantity_test.go index c600294c60d..a9f4d6aaf4d 100644 --- a/pkg/api/resource/quantity_test.go +++ b/pkg/api/resource/quantity_test.go @@ -95,6 +95,28 @@ func TestQuantityCmp(t *testing.T) { t.Errorf("X: %v, Y: %v, Expected: %v, Actual: %v", testCase.x, testCase.y, testCase.expect, result) } } + + nils := []struct { + x *inf.Dec + y *inf.Dec + expect int + }{ + {dec(0, 0), dec(0, 0), 0}, + {nil, dec(0, 0), 0}, + {dec(0, 0), 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}, + } + for _, nilCase := range nils { + q1 := Quantity{nilCase.x, DecimalSI} + q2 := Quantity{nilCase.y, 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 TestQuantityParse(t *testing.T) { @@ -103,6 +125,8 @@ func TestQuantityParse(t *testing.T) { 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}}, @@ -126,6 +150,8 @@ func TestQuantityParse(t *testing.T) { {"100Ti", Quantity{dec(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}}, @@ -186,15 +212,15 @@ func TestQuantityParse(t *testing.T) { {"1.01E", Quantity{dec(101, 16), DecimalSI}}, // Things that saturate/round - {"3.001m", Quantity{dec(4, -3), DecimalSI}}, - {"1.1E-3", Quantity{dec(2, -3), DecimalExponent}}, - {"0.0001", Quantity{dec(1, -3), DecimalSI}}, - {"0.0005", Quantity{dec(1, -3), DecimalSI}}, - {"0.00050", Quantity{dec(1, -3), DecimalSI}}, - {"0.5e-3", Quantity{dec(1, -3), DecimalExponent}}, - {"0.9m", Quantity{dec(1, -3), DecimalSI}}, - {"0.12345", Quantity{dec(124, -3), DecimalSI}}, - {"0.12354", Quantity{dec(124, -3), DecimalSI}}, + {"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}}, @@ -206,7 +232,7 @@ func TestQuantityParse(t *testing.T) { {"0.025Ti", Quantity{dec(274877906944, -1), BinarySI}}, // Things written by trolls - {"0.000001Ki", Quantity{dec(2, -3), DecimalSI}}, // rounds up, changes format + {"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}}, @@ -308,6 +334,7 @@ func TestQuantityString(t *testing.T) { {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"}, @@ -315,6 +342,14 @@ func TestQuantityString(t *testing.T) { {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"}, } for _, item := range table { got := item.in.String() @@ -346,7 +381,10 @@ func TestQuantityParseEmit(t *testing.T) { {"1Gi", "1Gi"}, {"1024Mi", "1Gi"}, {"1000M", "1G"}, - {".000001Ki", "2m"}, + {".001Ki", "1024m"}, + {".000001Ki", "1024u"}, + {".000000001Ki", "1024n"}, + {".000000000001Ki", "2n"}, } for _, item := range table { @@ -502,6 +540,77 @@ func TestNewSet(t *testing.T) { } } +func TestNewScaledSet(t *testing.T) { + table := []struct { + value int64 + scale Scale + expect string + }{ + {1, Nano, "1n"}, + {1000, Nano, "1u"}, + {1, Micro, "1u"}, + {1000, Micro, "1m"}, + {1, Milli, "1m"}, + {1000, Milli, "1"}, + {1, 0, "1"}, + {0, Nano, "0"}, + {0, Micro, "0"}, + {0, Milli, "0"}, + {0, 0, "0"}, + } + + for _, item := range table { + q := NewScaledQuantity(item.value, item.scale) + 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.ScaledValue(item.scale); e != a { + t.Errorf("Expected %v, got %v", e, a) + } + q3 := NewQuantity(0, DecimalSI) + q3.SetScaled(item.value, item.scale) + if q.Cmp(*q3) != 0 { + t.Errorf("Expected %v and %v to be equal", q, q3) + } + } +} + +func TestScaledValue(t *testing.T) { + table := []struct { + fromScale Scale + toScale Scale + expected int64 + }{ + {Nano, Nano, 1}, + {Nano, Micro, 1}, + {Nano, Milli, 1}, + {Nano, 0, 1}, + {Micro, Nano, 1000}, + {Micro, Micro, 1}, + {Micro, Milli, 1}, + {Micro, 0, 1}, + {Milli, Nano, 1000 * 1000}, + {Milli, Micro, 1000}, + {Milli, Milli, 1}, + {Milli, 0, 1}, + {0, Nano, 1000 * 1000 * 1000}, + {0, Micro, 1000 * 1000}, + {0, Milli, 1000}, + {0, 0, 1}, + } + + for _, item := range table { + q := NewScaledQuantity(1, item.fromScale) + if e, a := item.expected, q.ScaledValue(item.toScale); e != a { + t.Errorf("%v to %v: Expected %v, got %v", item.fromScale, item.toScale, e, a) + } + } +} + func TestUninitializedNoCrash(t *testing.T) { var q Quantity diff --git a/pkg/api/resource/scale_int_test.go b/pkg/api/resource/scale_int_test.go index 558b196f674..1b4390e5599 100644 --- a/pkg/api/resource/scale_int_test.go +++ b/pkg/api/resource/scale_int_test.go @@ -22,7 +22,7 @@ import ( "testing" ) -func TestScaledValue(t *testing.T) { +func TestScaledValueInternal(t *testing.T) { tests := []struct { unscaled *big.Int scale int diff --git a/pkg/api/resource/suffix.go b/pkg/api/resource/suffix.go index 5dc837dc150..529712365d7 100644 --- a/pkg/api/resource/suffix.go +++ b/pkg/api/resource/suffix.go @@ -83,6 +83,8 @@ func newSuffixer() suffixer { // a suffix for 2^0. sh.decSuffixes.addSuffix("", bePair{2, 0}) + sh.decSuffixes.addSuffix("n", bePair{10, -9}) + sh.decSuffixes.addSuffix("u", bePair{10, -6}) sh.decSuffixes.addSuffix("m", bePair{10, -3}) sh.decSuffixes.addSuffix("", bePair{10, 0}) sh.decSuffixes.addSuffix("k", bePair{10, 3})