Files
csharp/src/KubernetesClient/ResourceQuantity.cs
2020-03-10 15:19:37 -07:00

404 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Fractions;
using Newtonsoft.Json;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.Serialization;
namespace k8s.Models
{
internal class QuantityConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var q = (ResourceQuantity)value;
if (q != null)
{
serializer.Serialize(writer, q.ToString());
return;
}
serializer.Serialize(writer, value);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
JsonSerializer serializer)
{
return new ResourceQuantity(serializer.Deserialize<string>(reader));
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(string);
}
}
/// <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(QuantityConverter))]
public partial class ResourceQuantity : IYamlConvertible
{
public enum SuffixFormat
{
DecimalExponent,
BinarySI,
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 (-1024 < _unitlessValue && _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));
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;
}
/// <inheritdoc/>
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();
}
}
/// <inheritdoc/>
public void Write(IEmitter emitter, ObjectSerializer nestedObjectSerializer)
{
emitter.Emit(new Scalar(this.ToString()));
}
public static implicit operator decimal(ResourceQuantity v)
{
return v._unitlessValue.ToDecimal();
}
public static implicit operator ResourceQuantity(decimal v)
{
return new ResourceQuantity(v, 0, SuffixFormat.DecimalExponent);
}
#region suffixer
private 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;
}
}
#endregion
}
}