diff --git a/pkg/api/resource/quantity.go b/pkg/api/resource/quantity.go index e0020e1d991..7fc2e633e34 100644 --- a/pkg/api/resource/quantity.go +++ b/pkg/api/resource/quantity.go @@ -33,18 +33,19 @@ import ( // // The serialization format is: // -// ::= | -// ::= | . -// (Note that may be ""!) -// ::= "+" | "-" -// ::= | -// ::= | +// ::= +// (Note that may be empty, from the "" case in .) // ::= 0 | 1 | ... | 9 -// ::= | | -// ::= i | Ki | Mi | Gi | Ti | Pi | Ei -// ::= m | "" | k | M | G | T | P | E -// ::= "e" | "E" +// ::= | +// ::= | . | . | . +// ::= "+" | "-" +// ::= | +// ::= | | +// ::= i | Ki | Mi | Gi | Ti | Pi | Ei +// (International System of units; See: http://physics.nist.gov/cuu/Units/binary.html) +// ::= m | "" | k | M | G | T | P | E // (Note that 1024 = 1Ki but 1000 = 1k; I didn't choose the capitalization.) +// ::= "e" | "E" // // No matter which of the three exponent forms is used, no quantity may represent // a number greater than 2^63-1 in magnitude, nor may it have more than 3 digits @@ -54,7 +55,7 @@ import ( // // When a Quantity is parsed from a string, it will remember the type of suffix // it had, and will use the same type again when it is serialized. -// One exception: numbers with a Binary SI suffix less than one will be changed +// One exception: numbers with a Binary SI suffix less than bigOne will be changed // to Decimal SI suffix. E.g., .5i becomes 500m. [NOT 512m!] // // Before serializing, Quantity will be put in "canonical form". @@ -67,7 +68,12 @@ import ( // // Examples: // 1.5 will be serialized as "1500m" -// 1.5Gi will be serialized as "1576Mi" +// 1.5Gi will be serialized as "1536Mi" +// +// NOTE: We reserve the right to amend this canonical format, perhaps to +// allow 1.5 to be canonical. +// TODO: Remove above disclaimer after all bikeshedding about format is over, +// or after March 2015. // // Note that the quantity will NEVER be internally represented by a // floating point number. That is the whole point of this exercise. @@ -93,16 +99,25 @@ type Quantity struct { type Format string const ( - DecimalExponent = Format("DecExponent") - // SI = International System of units. - BinarySI = Format("BinSI") - DecimalSI = Format("DecSI") + DecimalExponent = Format("DecimalExponent") // e.g., 12e6 + BinarySI = Format("BinarySI") // e.g., 12Mi (12 * 2^20) + DecimalSI = Format("DecimalSI") // e.g., 12M (12 * 10^6) ) +// ParseOrDie turns the given string into a quantity or panics; for tests +// or others cases where you know the string is valid. +func ParseOrDie(str string) *Quantity { + q, err := ParseQuantity(str) + if err != nil { + panic(fmt.Errorf("cannot parse '%v': %v", str, err)) + } + return q +} + 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 = "^([+-]?[0123456789.]+)([eEimkKMGTP]*[-+]?[0123456789]*)$" + splitREString = "^([+-]?[0-9.]+)([eEimkKMGTP]*[-+]?[0-9]*)$" ) var ( @@ -115,11 +130,11 @@ var ( ErrSuffix = errors.New("unable to parse quantity's suffix") // Commonly needed big.Int values-- treat as read only! - ten = big.NewInt(10) - zero = big.NewInt(0) - one = big.NewInt(1) - thousand = big.NewInt(1000) - ten24 = big.NewInt(1024) + 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) @@ -127,8 +142,7 @@ var ( decMinusOne = inf.NewDec(-1, 0) decThousand = inf.NewDec(1000, 0) - // Smallest and largest (in magnitude) numbers allowed. - minAllowed = inf.NewDec(1, 3) // == 1/1000 + // Largest (in magnitude) number allowed. maxAllowed = inf.NewDec((1<<63)-1, 0) // == max int64 // The maximum value we can represent milli-units for. @@ -140,6 +154,7 @@ var ( // 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 } @@ -154,13 +169,14 @@ func ParseQuantity(str string) (*Quantity, error) { return nil, ErrSuffix } - // So that no one but us has to think about suffixes, remove it. + // So that no bigOne but us has to think about suffixes, remove it. if base == 10 { amount.SetScale(amount.Scale() + inf.Scale(-exponent)) } else if base == 2 { // numericSuffix = 2 ** exponent - numericSuffix := big.NewInt(1).Lsh(one, uint(exponent)) - amount.UnscaledBig().Mul(amount.UnscaledBig(), numericSuffix) + numericSuffix := big.NewInt(1).Lsh(bigOne, uint(exponent)) + ub := amount.UnscaledBig() + amount.SetUnscaledBig(ub.Mul(ub, numericSuffix)) } // Cap at min/max bounds. @@ -168,12 +184,12 @@ func ParseQuantity(str string) (*Quantity, error) { if sign == -1 { amount.Neg(amount) } - // This rounds non-zero values up to the minimum representable + // This rounds non-bigZero 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. + // that would have the side effect of rounding values < .5m to bigZero. amount.Round(amount, 3, inf.RoundUp) // The max is just a simple cap. @@ -198,9 +214,9 @@ func ParseQuantity(str string) (*Quantity, error) { func removeFactors(d, factor *big.Int) (result *big.Int, times int) { q := big.NewInt(0) m := big.NewInt(0) - for d.Cmp(zero) != 0 { + for d.Cmp(bigZero) != 0 { q.DivMod(d, factor, m) - if m.Cmp(zero) != 0 { + if m.Cmp(bigZero) != 0 { break } times++ @@ -212,7 +228,7 @@ func removeFactors(d, factor *big.Int) (result *big.Int, times int) { // Canonicalize returns the canonical form of q and its suffix (see comment on Quantity). // // Note about BinarySI: -// * If q.Format is set to BinarySI and q.Amount represents a non-zero value between +// * If q.Format is set to BinarySI and q.Amount represents a non-bigZero value between // -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.) @@ -243,19 +259,19 @@ func (q *Quantity) Canonicalize() (string, suffix) { } // TODO: If BinarySI formatting is requested but would cause rounding, upgrade to - // one of the other formats. + // bigOne 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, ten) + amount, times := removeFactors(amount, bigTen) exponent += times // make sure exponent is a multiple of 3 for exponent%3 != 0 { - amount.Mul(amount, ten) + amount.Mul(amount, bigTen) exponent-- } @@ -264,18 +280,17 @@ func (q *Quantity) Canonicalize() (string, suffix) { return number, suffix case BinarySI: tmp := &inf.Dec{} - //tmp.Set(q.Amount) tmp.Round(q.Amount, 0, inf.RoundUp) amount := tmp.UnscaledBig() exponent := int(-q.Amount.Scale()) // Apply the (base-10) shift. This will lose any fractional // part, which is intentional. for exponent > 0 { - amount.Mul(amount, ten) + amount.Mul(amount, bigTen) exponent-- } - amount, exponent = removeFactors(amount, ten24) + amount, exponent = removeFactors(amount, big1024) suffix, _ := quantitySuffixer.construct(2, exponent*10, format) number := amount.String() return number, suffix @@ -317,7 +332,8 @@ func NewQuantity(value int64, format Format) *Quantity { // NewMilliQuantity returns a new Quantity representing the given // value * 1/1000 in the given format. Note that BinarySI formatting -// will cause rounding for fractional values. +// will round fractional values, and will be changed to DecimalSI for +// values x where (-1 < x < 1) && (x != 0). func NewMilliQuantity(value int64, format Format) *Quantity { return &Quantity{ Amount: inf.NewDec(value, 3), diff --git a/pkg/api/resource/quantity_example_test.go b/pkg/api/resource/quantity_example_test.go new file mode 100644 index 00000000000..5442cb546c4 --- /dev/null +++ b/pkg/api/resource/quantity_example_test.go @@ -0,0 +1,59 @@ +/* +Copyright 2014 Google Inc. 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_test + +import ( + "fmt" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" +) + +func ExampleFormat() { + memorySize := resource.NewQuantity(5*1024*1024*1024, resource.BinarySI) + fmt.Printf("memorySize = %v\n", memorySize) + + diskSize := resource.NewQuantity(5*1000*1000*1000, resource.DecimalSI) + fmt.Printf("diskSize = %v\n", diskSize) + + cores := resource.NewMilliQuantity(5300, resource.DecimalSI) + fmt.Printf("cores = %v\n", cores) + + // Output: + // memorySize = 5Gi + // diskSize = 5G + // cores = 5300m +} + +func ExampleParseOrDie() { + memorySize := resource.ParseOrDie("5Gi") + fmt.Printf("memorySize = %v (%v)\n", memorySize.Value(), memorySize.Format) + + diskSize := resource.ParseOrDie("5G") + fmt.Printf("diskSize = %v (%v)\n", diskSize.Value(), diskSize.Format) + + cores := resource.ParseOrDie("5300m") + fmt.Printf("milliCores = %v (%v)\n", cores.MilliValue(), cores.Format) + + cores2 := resource.ParseOrDie("5.4") + fmt.Printf("milliCores = %v (%v)\n", cores2.MilliValue(), cores2.Format) + + // Output: + // memorySize = 5368709120 (BinarySI) + // diskSize = 5000000000 (DecimalSI) + // milliCores = 5300 (DecimalSI) + // milliCores = 5400 (DecimalSI) +} diff --git a/pkg/api/resource/quantity_test.go b/pkg/api/resource/quantity_test.go index 5a4d71bb3cb..f34873ffb89 100644 --- a/pkg/api/resource/quantity_test.go +++ b/pkg/api/resource/quantity_test.go @@ -162,6 +162,10 @@ func TestQuantityParse(t *testing.T) { // Even if it were, 500 * 1/1024 = .48828125, NOT .512 // I cannot recommend using this feature, it is confusing. {"0.5i", Quantity{dec(500, -3), DecimalSI}}, + + // Things written by trolls + {".001", Quantity{dec(1, -3), DecimalSI}}, + {"1.", Quantity{dec(1, 0), DecimalSI}}, } for _, item := range table {