Skip to content

Commit 736ce06

Browse files
Merge pull request #61 from tg123/v1.8_quantity
add support for quantity
2 parents f46c02f + 9d30adb commit 736ce06

File tree

3 files changed

+613
-0
lines changed

3 files changed

+613
-0
lines changed

src/KubernetesClient.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
</ItemGroup>
1818
<ItemGroup>
1919
<PackageReference Include="BouncyCastle.NetCore" Version="1.8.1.3" />
20+
<PackageReference Include="Fractions" Version="3.0.1" />
2021
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="1.1.2" />
2122
<PackageReference Include="Microsoft.Rest.ClientRuntime" Version="2.3.10" />
2223
<PackageReference Include="Newtonsoft.Json" Version="10.0.2" />
24+
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
2325
<PackageReference Include="YamlDotNet.NetCore" Version="1.0.0" />
2426
</ItemGroup>
2527
</Project>

src/ResourceQuantity.cs

Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Numerics;
5+
using Fractions;
6+
using Newtonsoft.Json;
7+
8+
namespace k8s.Models
9+
{
10+
internal class QuantityConverter : JsonConverter
11+
{
12+
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
13+
{
14+
var q = (ResourceQuantity) value;
15+
16+
if (q != null)
17+
{
18+
serializer.Serialize(writer, q.ToString());
19+
return;
20+
}
21+
22+
serializer.Serialize(writer, value);
23+
}
24+
25+
public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
26+
JsonSerializer serializer)
27+
{
28+
return new ResourceQuantity(serializer.Deserialize<string>(reader));
29+
}
30+
31+
public override bool CanConvert(Type objectType)
32+
{
33+
return objectType == typeof(string);
34+
}
35+
}
36+
37+
/// <summary>
38+
/// port https://github.com/kubernetes/apimachinery/blob/master/pkg/api/resource/quantity.go to c#
39+
/// Quantity is a fixed-point representation of a number.
40+
/// It provides convenient marshaling/unmarshaling in JSON and YAML,
41+
/// in addition to String() and Int64() accessors.
42+
/// The serialization format is:
43+
/// quantity ::= signedNumber suffix
44+
/// (Note that suffix may be empty, from the "" case in decimalSI.)
45+
/// digit ::= 0 | 1 | ... | 9
46+
/// digits ::= digit | digitdigits
47+
/// number ::= digits | digits.digits | digits. | .digits
48+
/// sign ::= "+" | "-"
49+
/// signedNumber ::= number | signnumber
50+
/// suffix ::= binarySI | decimalExponent | decimalSI
51+
/// binarySI ::= Ki | Mi | Gi | Ti | Pi | Ei
52+
/// (International System of units; See: http:///physics.nist.gov/cuu/Units/binary.html)
53+
/// decimalSI ::= m | "" | k | M | G | T | P | E
54+
/// (Note that 1024 = 1Ki but 1000 = 1k; I didn't choose the capitalization.)
55+
/// decimalExponent ::= "e" signedNumber | "E" signedNumber
56+
/// No matter which of the three exponent forms is used, no quantity may represent
57+
/// a number greater than 2^63-1 in magnitude, nor may it have more than 3 decimal
58+
/// places. Numbers larger or more precise will be capped or rounded up.
59+
/// (E.g.: 0.1m will rounded up to 1m.)
60+
/// This may be extended in the future if we require larger or smaller quantities.
61+
/// When a Quantity is parsed from a string, it will remember the type of suffix
62+
/// it had, and will use the same type again when it is serialized.
63+
/// Before serializing, Quantity will be put in "canonical form".
64+
/// This means that Exponent/suffix will be adjusted up or down (with a
65+
/// corresponding increase or decrease in Mantissa) such that:
66+
/// a. No precision is lost
67+
/// b. No fractional digits will be emitted
68+
/// c. The exponent (or suffix) is as large as possible.
69+
/// The sign will be omitted unless the number is negative.
70+
/// Examples:
71+
/// 1.5 will be serialized as "1500m"
72+
/// 1.5Gi will be serialized as "1536Mi"
73+
/// NOTE: We reserve the right to amend this canonical format, perhaps to
74+
/// allow 1.5 to be canonical.
75+
/// TODO: Remove above disclaimer after all bikeshedding about format is over,
76+
/// or after March 2015.
77+
/// Note that the quantity will NEVER be internally represented by a
78+
/// floating point number. That is the whole point of this exercise.
79+
/// Non-canonical values will still parse as long as they are well formed,
80+
/// but will be re-emitted in their canonical form. (So always use canonical
81+
/// form, or don't diff.)
82+
/// This format is intended to make it difficult to use these numbers without
83+
/// writing some sort of special handling code in the hopes that that will
84+
/// cause implementors to also use a fixed point implementation.
85+
/// </summary>
86+
[JsonConverter(typeof(QuantityConverter))]
87+
public partial class ResourceQuantity
88+
{
89+
public enum SuffixFormat
90+
{
91+
DecimalExponent,
92+
BinarySI,
93+
DecimalSI
94+
}
95+
96+
public static readonly decimal MaxAllowed = (decimal)BigInteger.Pow(2, 63) - 1;
97+
98+
private static readonly char[] SuffixChars = "eEinumkKMGTP".ToCharArray();
99+
private Fraction _unitlessValue;
100+
101+
public ResourceQuantity(decimal n, int exp, SuffixFormat format)
102+
{
103+
_unitlessValue = Fraction.FromDecimal(n) * Fraction.Pow(10, exp);
104+
Format = format;
105+
}
106+
107+
public SuffixFormat Format { get; private set; }
108+
109+
public string CanonicalizeString()
110+
{
111+
return CanonicalizeString(Format);
112+
}
113+
114+
public override string ToString()
115+
{
116+
return CanonicalizeString();
117+
}
118+
119+
protected bool Equals(ResourceQuantity other)
120+
{
121+
return Format == other.Format && _unitlessValue.Equals(other._unitlessValue);
122+
}
123+
124+
public override bool Equals(object obj)
125+
{
126+
if (ReferenceEquals(null, obj))
127+
{
128+
return false;
129+
}
130+
if (ReferenceEquals(this, obj))
131+
{
132+
return true;
133+
}
134+
if (obj.GetType() != GetType())
135+
{
136+
return false;
137+
}
138+
return Equals((ResourceQuantity) obj);
139+
}
140+
141+
public override int GetHashCode()
142+
{
143+
unchecked
144+
{
145+
return ((int) Format * 397) ^ _unitlessValue.GetHashCode();
146+
}
147+
}
148+
149+
//
150+
// CanonicalizeString = go version CanonicalizeBytes
151+
// CanonicalizeBytes returns the canonical form of q and its suffix (see comment on Quantity).
152+
//
153+
// Note about BinarySI:
154+
// * If q.Format is set to BinarySI and q.Amount represents a non-zero value between
155+
// -1 and +1, it will be emitted as if q.Format were DecimalSI.
156+
// * Otherwise, if q.Format is set to BinarySI, fractional parts of q.Amount will be
157+
// rounded up. (1.1i becomes 2i.)
158+
public string CanonicalizeString(SuffixFormat suffixFormat)
159+
{
160+
if (suffixFormat == SuffixFormat.BinarySI)
161+
{
162+
if (-1024 < _unitlessValue && _unitlessValue < 1024)
163+
{
164+
return Suffixer.AppendMaxSuffix(_unitlessValue, SuffixFormat.DecimalSI);
165+
}
166+
167+
if (HasMantissa(_unitlessValue))
168+
{
169+
return Suffixer.AppendMaxSuffix(_unitlessValue, SuffixFormat.DecimalSI);
170+
}
171+
}
172+
173+
return Suffixer.AppendMaxSuffix(_unitlessValue, suffixFormat);
174+
}
175+
176+
// ctor
177+
partial void CustomInit()
178+
{
179+
var value = (Value ?? "").Trim();
180+
181+
var si = value.IndexOfAny(SuffixChars);
182+
if (si == -1)
183+
{
184+
si = value.Length;
185+
}
186+
187+
var literal = Fraction.FromString(value.Substring(0, si));
188+
var suffixer = new Suffixer(value.Substring(si));
189+
190+
_unitlessValue = literal.Multiply(Fraction.Pow(suffixer.Base, suffixer.Exponent));
191+
Format = suffixer.Format;
192+
193+
if (Format == SuffixFormat.BinarySI && _unitlessValue > Fraction.FromDecimal(MaxAllowed))
194+
{
195+
_unitlessValue = Fraction.FromDecimal(MaxAllowed);
196+
}
197+
}
198+
199+
private static bool HasMantissa(Fraction value)
200+
{
201+
if (value.IsZero)
202+
{
203+
return false;
204+
}
205+
206+
return BigInteger.Remainder(value.Numerator, value.Denominator) > 0;
207+
}
208+
209+
public static implicit operator decimal(ResourceQuantity v)
210+
{
211+
return v._unitlessValue.ToDecimal();
212+
}
213+
214+
public static implicit operator ResourceQuantity(decimal v)
215+
{
216+
return new ResourceQuantity(v, 0, SuffixFormat.DecimalExponent);
217+
}
218+
219+
#region suffixer
220+
221+
private class Suffixer
222+
{
223+
private static readonly IReadOnlyDictionary<string, (int, int)> BinSuffixes =
224+
new Dictionary<string, (int, int)>
225+
{
226+
// Don't emit an error when trying to produce
227+
// a suffix for 2^0.
228+
{"", (2, 0)},
229+
{"Ki", (2, 10)},
230+
{"Mi", (2, 20)},
231+
{"Gi", (2, 30)},
232+
{"Ti", (2, 40)},
233+
{"Pi", (2, 50)},
234+
{"Ei", (2, 60)}
235+
};
236+
237+
private static readonly IReadOnlyDictionary<string, (int, int)> DecSuffixes =
238+
new Dictionary<string, (int, int)>
239+
{
240+
{"n", (10, -9)},
241+
{"u", (10, -6)},
242+
{"m", (10, -3)},
243+
{"", (10, 0)},
244+
{"k", (10, 3)},
245+
{"M", (10, 6)},
246+
{"G", (10, 9)},
247+
{"T", (10, 12)},
248+
{"P", (10, 15)},
249+
{"E", (10, 18)}
250+
};
251+
252+
public Suffixer(string suffix)
253+
{
254+
// looked up
255+
{
256+
if (DecSuffixes.TryGetValue(suffix, out var be))
257+
{
258+
(Base, Exponent) = be;
259+
Format = SuffixFormat.DecimalSI;
260+
261+
return;
262+
}
263+
}
264+
265+
{
266+
if (BinSuffixes.TryGetValue(suffix, out var be))
267+
{
268+
(Base, Exponent) = be;
269+
Format = SuffixFormat.BinarySI;
270+
271+
return;
272+
}
273+
}
274+
275+
if (char.ToLower(suffix[0]) == 'e')
276+
{
277+
Base = 10;
278+
Exponent = int.Parse(suffix.Substring(1));
279+
Format = SuffixFormat.DecimalExponent;
280+
return;
281+
}
282+
283+
throw new ArgumentException("unable to parse quantity's suffix");
284+
}
285+
286+
public SuffixFormat Format { get; }
287+
288+
public int Base { get; }
289+
public int Exponent { get; }
290+
291+
292+
public static string AppendMaxSuffix(Fraction value, SuffixFormat format)
293+
{
294+
if (value.IsZero)
295+
{
296+
return "0";
297+
}
298+
299+
switch (format)
300+
{
301+
case SuffixFormat.DecimalExponent:
302+
{
303+
var minE = -9;
304+
var lastv = Roundup(value * Fraction.Pow(10, -minE));
305+
306+
for (var exp = minE;; exp += 3)
307+
{
308+
var v = value * Fraction.Pow(10, -exp);
309+
if (HasMantissa(v))
310+
{
311+
break;
312+
}
313+
314+
minE = exp;
315+
lastv = v;
316+
}
317+
318+
319+
if (minE == 0)
320+
{
321+
return $"{(decimal) lastv}";
322+
}
323+
324+
return $"{(decimal) lastv}e{minE}";
325+
}
326+
327+
break;
328+
case SuffixFormat.BinarySI:
329+
return AppendMaxSuffix(value, BinSuffixes);
330+
break;
331+
case SuffixFormat.DecimalSI:
332+
return AppendMaxSuffix(value, DecSuffixes);
333+
break;
334+
default:
335+
throw new ArgumentOutOfRangeException(nameof(format), format, null);
336+
}
337+
}
338+
339+
private static string AppendMaxSuffix(Fraction value, IReadOnlyDictionary<string, (int, int)> suffixes)
340+
{
341+
var min = suffixes.First();
342+
var suffix = min.Key;
343+
var lastv = Roundup(value * Fraction.Pow(min.Value.Item1, -min.Value.Item2));
344+
345+
foreach (var kv in suffixes.Skip(1))
346+
{
347+
var v = value * Fraction.Pow(kv.Value.Item1, -kv.Value.Item2);
348+
if (HasMantissa(v))
349+
{
350+
break;
351+
}
352+
353+
suffix = kv.Key;
354+
lastv = v;
355+
}
356+
357+
return $"{(decimal) lastv}{suffix}";
358+
}
359+
360+
private static Fraction Roundup(Fraction lastv)
361+
{
362+
var round = BigInteger.DivRem(lastv.Numerator, lastv.Denominator, out var remainder);
363+
if (!remainder.IsZero)
364+
{
365+
lastv = round + 1;
366+
}
367+
return lastv;
368+
}
369+
}
370+
371+
#endregion
372+
}
373+
}

0 commit comments

Comments
 (0)