Files
csharp/src/KubernetesClient.Models/ResourceQuantity.cs
Manuel Menegazzo 3702fd6e90 Standardization of using order and object initialization (#1028)
* Code cleanup KubernetesClient

* KubernetesClient.Basic code cleanup

* KubernetesClient.Models cleanup

* LibKubernetesGenerator code cleanup

* Improved readability of object initialization

* FIx namespace order

* Fixed some compilation warning
2022-09-28 13:34:32 -07:00

393 lines
13 KiB
C#

using Fractions;
using System.Globalization;
using System.Numerics;
namespace k8s.Models
{
/// <summary>
/// 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.
/// </summary>
[JsonConverter(typeof(ResourceQuantityJsonConverter))]
public partial class ResourceQuantity
{
public enum SuffixFormat
{
/// <summary>
/// e.g., 12e6
/// </summary>
DecimalExponent,
/// <summary>
/// e.g., 12Mi (12 * 2^20)
/// </summary>
BinarySI,
/// <summary>
/// e.g., 12M (12 * 10^6)
/// </summary>
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 static implicit operator decimal(ResourceQuantity v)
{
return v?.ToDecimal() ?? 0;
}
public static implicit operator ResourceQuantity(decimal v)
{
return new ResourceQuantity(v, 0, SuffixFormat.DecimalExponent);
}
private sealed class Suffixer
{
private static readonly IReadOnlyDictionary<string, (int, int)> BinSuffixes =
new Dictionary<string, (int, int)>
{
// 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<string, (int, int)> DecSuffixes =
new Dictionary<string, (int, int)>
{
{ "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<string, (int, int)> 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;
}
}
public int ToInt32()
{
return _unitlessValue.ToInt32();
}
public long ToInt64()
{
return _unitlessValue.ToInt64();
}
public uint ToUInt32()
{
return _unitlessValue.ToUInt32();
}
public ulong ToUInt64()
{
return _unitlessValue.ToUInt64();
}
public BigInteger ToBigInteger()
{
return _unitlessValue.ToBigInteger();
}
public decimal ToDecimal()
{
return _unitlessValue.ToDecimal();
}
public double ToDouble()
{
return _unitlessValue.ToDouble();
}
}
}