using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Numerics; using Fractions; using Newtonsoft.Json; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Serialization; namespace k8s.Models { /// /// port https://github.com/kubernetes/apimachinery/blob/master/pkg/api/resource/quantity.go to c# /// Quantity is a fixed-point representation of a number. /// It provides convenient marshaling/unmarshaling in JSON and YAML, /// in addition to String() and Int64() accessors. /// The serialization format is: /// quantity ::= signedNumber suffix /// (Note that suffix may be empty, from the "" case in decimalSI.) /// digit ::= 0 | 1 | ... | 9 /// digits ::= digit | digitdigits /// number ::= digits | digits.digits | digits. | .digits /// sign ::= "+" | "-" /// signedNumber ::= number | signnumber /// suffix ::= binarySI | decimalExponent | decimalSI /// binarySI ::= Ki | Mi | Gi | Ti | Pi | Ei /// (International System of units; See: http:///physics.nist.gov/cuu/Units/binary.html) /// decimalSI ::= m | "" | k | M | G | T | P | E /// (Note that 1024 = 1Ki but 1000 = 1k; I didn't choose the capitalization.) /// decimalExponent ::= "e" signedNumber | "E" signedNumber /// 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 decimal /// places. Numbers larger or more precise will be capped or rounded up. /// (E.g.: 0.1m will rounded up to 1m.) /// This may be extended in the future if we require larger or smaller quantities. /// 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. /// Before serializing, Quantity will be put in "canonical form". /// This means that Exponent/suffix will be adjusted up or down (with a /// corresponding increase or decrease in Mantissa) such that: /// a. No precision is lost /// b. No fractional digits will be emitted /// c. The exponent (or suffix) is as large as possible. /// The sign will be omitted unless the number is negative. /// Examples: /// 1.5 will be serialized as "1500m" /// 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. /// Non-canonical values will still parse as long as they are well formed, /// but will be re-emitted in their canonical form. (So always use canonical /// form, or don't diff.) /// This format is intended to make it difficult to use these numbers without /// writing some sort of special handling code in the hopes that that will /// cause implementors to also use a fixed point implementation. /// [JsonConverter(typeof(QuantityConverter))] public partial class ResourceQuantity : IYamlConvertible { public enum SuffixFormat { /// /// e.g., 12e6 /// DecimalExponent, /// /// e.g., 12Mi (12 * 2^20) /// BinarySI, /// /// e.g., 12M (12 * 10^6) /// DecimalSI, } public static readonly decimal MaxAllowed = (decimal)BigInteger.Pow(2, 63) - 1; private static readonly char[] SuffixChars = "eEinumkKMGTP".ToCharArray(); private Fraction _unitlessValue; public ResourceQuantity(decimal n, int exp, SuffixFormat format) { _unitlessValue = Fraction.FromDecimal(n) * Fraction.Pow(10, exp); Format = format; } public SuffixFormat Format { get; private set; } public string CanonicalizeString() { return CanonicalizeString(Format); } public override string ToString() { return CanonicalizeString(); } protected bool Equals(ResourceQuantity other) { return Format == other?.Format && _unitlessValue.Equals(other._unitlessValue); } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) { return false; } if (ReferenceEquals(this, obj)) { return true; } if (obj.GetType() != GetType()) { return false; } return Equals((ResourceQuantity)obj); } public override int GetHashCode() { unchecked { return ((int)Format * 397) ^ _unitlessValue.GetHashCode(); } } // // CanonicalizeString = go version CanonicalizeBytes // CanonicalizeBytes 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 // -1 and +1, it will be emitted as if q.Format were DecimalSI. // * Otherwise, if q.Format is set to BinarySI, fractional parts of q.Amount will be // rounded up. (1.1i becomes 2i.) public string CanonicalizeString(SuffixFormat suffixFormat) { if (suffixFormat == SuffixFormat.BinarySI) { if (_unitlessValue > -1024 && _unitlessValue < 1024) { return Suffixer.AppendMaxSuffix(_unitlessValue, SuffixFormat.DecimalSI); } if (HasMantissa(_unitlessValue)) { return Suffixer.AppendMaxSuffix(_unitlessValue, SuffixFormat.DecimalSI); } } return Suffixer.AppendMaxSuffix(_unitlessValue, suffixFormat); } // ctor partial void CustomInit() { if (Value == null) { // No value has been defined, initialize to 0. _unitlessValue = new Fraction(0); Format = SuffixFormat.BinarySI; return; } var value = Value.Trim(); var si = value.IndexOfAny(SuffixChars); if (si == -1) { si = value.Length; } var literal = Fraction.FromString(value.Substring(0, si), CultureInfo.InvariantCulture); var suffixer = new Suffixer(value.Substring(si)); _unitlessValue = literal.Multiply(Fraction.Pow(suffixer.Base, suffixer.Exponent)); Format = suffixer.Format; if (Format == SuffixFormat.BinarySI && _unitlessValue > Fraction.FromDecimal(MaxAllowed)) { _unitlessValue = Fraction.FromDecimal(MaxAllowed); } } private static bool HasMantissa(Fraction value) { if (value.IsZero) { return false; } return BigInteger.Remainder(value.Numerator, value.Denominator) > 0; } /// public void Read(IParser parser, Type expectedType, ObjectDeserializer nestedObjectDeserializer) { if (expectedType != typeof(ResourceQuantity)) { throw new ArgumentOutOfRangeException(nameof(expectedType)); } if (parser?.Current is Scalar) { Value = ((Scalar)parser.Current).Value; parser.MoveNext(); CustomInit(); } } /// public void Write(IEmitter emitter, ObjectSerializer nestedObjectSerializer) { emitter?.Emit(new Scalar(ToString())); } public static implicit operator ResourceQuantity(decimal v) { return new ResourceQuantity(v, 0, SuffixFormat.DecimalExponent); } private class Suffixer { private static readonly IReadOnlyDictionary BinSuffixes = new Dictionary { // Don't emit an error when trying to produce // a suffix for 2^0. { "", (2, 0) }, { "Ki", (2, 10) }, { "Mi", (2, 20) }, { "Gi", (2, 30) }, { "Ti", (2, 40) }, { "Pi", (2, 50) }, { "Ei", (2, 60) }, }; private static readonly IReadOnlyDictionary DecSuffixes = new Dictionary { { "n", (10, -9) }, { "u", (10, -6) }, { "m", (10, -3) }, { "", (10, 0) }, { "k", (10, 3) }, { "M", (10, 6) }, { "G", (10, 9) }, { "T", (10, 12) }, { "P", (10, 15) }, { "E", (10, 18) }, }; public Suffixer(string suffix) { // looked up { if (DecSuffixes.TryGetValue(suffix, out var be)) { (Base, Exponent) = be; Format = SuffixFormat.DecimalSI; return; } } { if (BinSuffixes.TryGetValue(suffix, out var be)) { (Base, Exponent) = be; Format = SuffixFormat.BinarySI; return; } } if (char.ToLower(suffix[0]) == 'e') { Base = 10; Exponent = int.Parse(suffix.Substring(1)); Format = SuffixFormat.DecimalExponent; return; } throw new ArgumentException("unable to parse quantity's suffix"); } public SuffixFormat Format { get; } public int Base { get; } public int Exponent { get; } public static string AppendMaxSuffix(Fraction value, SuffixFormat format) { if (value.IsZero) { return "0"; } switch (format) { case SuffixFormat.DecimalExponent: { var minE = -9; var lastv = Roundup(value * Fraction.Pow(10, -minE)); for (var exp = minE; ; exp += 3) { var v = value * Fraction.Pow(10, -exp); if (HasMantissa(v)) { break; } minE = exp; lastv = v; } if (minE == 0) { return $"{(decimal)lastv}"; } return $"{(decimal)lastv}e{minE}"; } case SuffixFormat.BinarySI: return AppendMaxSuffix(value, BinSuffixes); case SuffixFormat.DecimalSI: return AppendMaxSuffix(value, DecSuffixes); default: throw new ArgumentOutOfRangeException(nameof(format), format, null); } } private static string AppendMaxSuffix(Fraction value, IReadOnlyDictionary suffixes) { var min = suffixes.First(); var suffix = min.Key; var lastv = Roundup(value * Fraction.Pow(min.Value.Item1, -min.Value.Item2)); foreach (var kv in suffixes.Skip(1)) { var v = value * Fraction.Pow(kv.Value.Item1, -kv.Value.Item2); if (HasMantissa(v)) { break; } suffix = kv.Key; lastv = v; } return $"{(decimal)lastv}{suffix}"; } private static Fraction Roundup(Fraction lastv) { var round = BigInteger.DivRem(lastv.Numerator, lastv.Denominator, out var remainder); if (!remainder.IsZero) { lastv = round + 1; } return lastv; } } } }