In this article, you will get an overview of nulls and generics with id implementation details, just so you can love the new C# again.
Introduction
The new nullable context can be enabled via the <Project><PropertyGroup><Nullable>enable</Nullable>
element in your C# project (.csproj) file. It gives you full null-state static analysis at compile-time and promises to eliminate every single NullReferenceException
once and for all. So far, we have covered the following:
Today, we want to actually implement this long promised Id
value type. Code quality is of paramount importance and deserves your second look. This article aims to provide an overview of nulls and generics with id implementation details, just so you can love the new C# again. Little things matter.
Using the code
It's probably about the right time to talk about how I organize my cloud objects. In my dictionary, everything is a JIt, with all cloud objects having their corresponding interfaces, which we can export to COM if we wish, but more importantly, we absolutely want to support multiple inheritance just like C++. C++ is often considered a difficult language. That's why I chose C# for all my tips-&-tricks, an easy-to-learn language the closest in spirit to C++, where performance is everything, second only to safety. C++ is slightly different, because performance is simply everything, second to nothing. Performance is challenging, while safety is meant to be easy to protect you like a child programmer. Safety is designed for children, so it must be easy. Please keep that in mind when you're designing your own programming language. Let's call our new Id
cloud object JId
:
using System;
namespace Pyramid.Kernel.Up
{
public partial interface JId : JIt
{
Guid XGuid { get; init; }
}
public partial interface JId<J> : JId, JIt<J> where J : JId<J>, new() { }
[Serializable]
public readonly partial struct Id : JId<Id>
{
public Guid XGuid { get; init; }
}
[Serializable]
public abstract partial class Id<J> : It<J>, JId<J> where J : Id<J>, new()
{
public abstract Guid XGuid { get; init; }
}
}
Scientific computing was the primary motivation for the invention of computers. To perform it, we must include the binary floating-point with the highest precision directly supported by our present hardware. Let's call it JReal
, because it's intended to simulate a real number:
namespace Pyramid.Kernel.Up
{
public partial interface JReal : JId
{
double XDouble { get; init; }
}
public partial interface JReal<J> : JId<J>, JReal where J : JReal<J>, new() { }
[Serializable]
public readonly partial struct Real : JReal<Real>
{
public double XDouble { get; init; }
public Guid XGuid { get; init; }
}
[Serializable]
public abstract partial class Real<J> : Id<J>, JReal<J> where J : Real<J>, new()
{
public double XDouble { get; init; }
}
}
Financial computing is arguably the second most important player in the computing world. To perform it, we must include the decimal floating-point with the highest precision directly supported by our present software. Let's call it JDime
, because it's intended to simulate a monetary amount. Apparently, financial computing isn't as prominent as scientific computing, since we don't even bother with direct hardware support. Banks are just not paying us enough.
namespace Pyramid.Kernel.Up
{
public partial interface JDime : JId
{
decimal XDecimal { get; init; }
}
public partial interface JDime<J> : JDime, JId<J> where J : JDime<J>, new() { }
[Serializable]
public readonly partial struct Dime : JDime
{
public decimal XDecimal { get; init; }
public Guid XGuid { get; init; }
}
[Serializable]
public abstract partial class Dime<J> : Id<J>, JDime<J> where J : Dime<J>, new()
{
public abstract decimal XDecimal { get; init; }
}
}
Whatever you do, you'll always need to manipulate collections of objects and work with indices, which are integers. To perform it, we must include the integral number with the highest precision directly supported by our present hardware. Let's call it JHit
, because it's intended to simulate a hit on a cloud object, which rhymes with a bit also. Code is like poetry, where every token is a well-thought-through word, strung together to form a necklace, eternal to shine.
namespace Pyramid.Kernel.Up
{
public partial interface JHit : JDime
{
long XInt64 { get; init; }
}
public partial interface JHit<J> : JDime<J>, JHit where J : JHit<J>, new() { }
[Serializable]
public readonly partial struct Hit : JHit<Hit>
{
public decimal XDecimal { get => XInt64; init => XInt64 = (long)value; }
public Guid XGuid { get; init; }
public long XInt64 { get; init; }
}
[Serializable]
public abstract partial class Hit<J> : Dime<J>, JHit<J> where J : Hit<J>, new()
{
public sealed override decimal XDecimal
{
get => new Hit { XInt64 = XInt64 }.XDecimal;
init => XInt64 = new Hit { XDecimal = value }.XInt64;
}
public sealed override Guid XGuid
{
get => new Hit { XInt64 = XInt64 }.XGuid;
init => XInt64 = new Hit { XGuid = value }.XInt64;
}
public long XInt64 { get; init; }
}
}
Now, let's start writing some code, finally, beginning with Hit
, the simplest one:
using System.Runtime.CompilerServices;
namespace Pyramid.Kernel.Up
{
partial struct Hit
{
static readonly ToBe<bool, Guid, long, int> _isUnder = (o, p, q) => q.IsntAbove(0x403e);
static readonly ToBe<long, Guid, long, int> _getQuadruple =
(o, p, q) => (long)o._GetQuadruple(p, q);
static readonly ToBe<long, Guid, long, int> _getDecimal =
(o, p, q) => (long)o._GetDecimal(p);
static readonly ToBe<long, Guid, long, int> _getIntegral =
(o, p, q) => o._GetsIntegral(out var r, p) ? (long)r : throw new OverflowException();
static readonly ToBe<long, Guid, long, int> _getInfinity =
(o, p, q) => throw new OverflowException();
static readonly ToBe<long, Guid, long, int> _getNaN =
(o, p, q) => throw new InvalidCastException();
public Guid XGuid
{
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
get
{
Span<byte> qBytes = stackalloc byte[16];
qBytes.Set(XInt64.And(long.MinValue).DownAt(48).Or(0x7fff).At(out short _), 0);
qBytes.Set(0xf2.At(out byte _), 6);
if (XInt64.Isnt(long.MinValue))
{
var qLong = XInt64.Abs();
var qInt = qLong.DownAt(32);
qBytes.Set(qInt.Have(out byte _, 29), 7);
qBytes.Set(0x80.Or(qInt.Have(0x1f, 24)).At(out byte _), 8);
qBytes.Set(qInt.Have(out byte _, 16), 9);
qBytes.Set(qInt.At(out short _), 10);
qBytes.Set(qLong.At(), 12);
}
else qBytes.Set(0x4.At(out byte _), 7);
return new(qBytes);
}
init => XInt64 =
value._Get(_isUnder, _getQuadruple, _getDecimal, _getIntegral, _getInfinity, _getNaN);
}
}
}
Obviously, XGuid
is not meant to be stored. XInt64
is! Therefore, we want to convert it from and to XInt64
. To write XInt64
to XGuid
, we will consider version-15.2 only, just so the version carries our type information. To read XInt64
from XGuid
, nevertheless, we must include all possible versions already discussed in our previous tip-&-trick. Interestingly, the logic to determine which version used in XGuid
is the same across all types, which can be made a higher-order method that takes delegates. Note-worthy is the fact that .Net JIT inlines delegates as well as methods. There's no difference between them, except that you can't tag delegates with MethodImplOptions.AggressiveOptimization
or MethodImplOptions.AggressiveInlining
, so you want to keep them short and sweet for JIT.
To facilitate the pronunciation of our code, which is as important as the sound of poetry to a poem, we prefer infinitives for regular delegates and partiples for event delegates, where present participles are reserved for events occurring before their associated methods and past partiples for those after. This way, our code reads like English, and we can thus easily communicate it verbally. The ToBe
delegates are defined as follows, overloaded all the way from A to Z for convenience. Here, we show the first three for you to get the idea:
namespace Pyramid.Kernel.Up
{
[Serializable]
public delegate JOut? ToBe<out JOut
>(
);
[Serializable]
public delegate JOut? ToBe<out JOut,
in JA>(
JA? a = default);
[Serializable]
public delegate JOut? ToBe<out JOut,
in JA, in JB>(
JA? a = default, JB? b = default);
}
Also, you might have noticed a new method called Abs
, which evidently returns an absolute value:
namespace Pyramid.Kernel.Up
{
partial class It
{
public static decimal Abs(this decimal it) => Math.Abs(it);
public static double Abs(this double it) => Math.Abs(it);
public static float Abs(this float it) => Math.Abs(it);
public static int Abs(this int it) => Math.Abs(it);
public static long Abs(this long it) => Math.Abs(it);
}
}
Is Hit
too simple for your taste? Let's proceed to Dime
. Financial computing isn't a joke, for financial security is constantly a top concern in the field. As you can possibly observe, the code is slightly more sophisticated:
namespace Pyramid.Kernel.Up
{
partial struct Dime
{
static readonly ToBe<bool, Guid, long, int> _isUnder = (o, p, q) => q.IsntAbove(0x405e);
static readonly ToBe<decimal, Guid, long, int> _getQuadruple =
(o, p, q) => o._GetQuadruple(p, q, true);
static readonly ToBe<decimal, Guid, long, int> _getDecimal = (o, p, q) => o._GetDecimal(p);
static readonly ToBe<decimal, Guid, long, int> _getIntegral =
(o, p, q) => o._GetsIntegral(out var r, p) ? r : throw new OverflowException();
static readonly ToBe<decimal, Guid, long, int> _getInfinity =
(o, p, q) => throw new OverflowException();
static readonly ToBe<decimal, Guid, long, int> _getNaN =
(o, p, q) => throw new InvalidCastException();
public Guid XGuid
{
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
get
{
Span<int> qInts = stackalloc int[4];
decimal.GetBits(XDecimal, qInts);
var qGotInts = qInts.As();
Span<byte> qBytes = stackalloc byte[16];
var qIntAt3 = qGotInts.Get(3);
qBytes.Set(qIntAt3.Down(16).Or(0x7fff).At(out short _), 0);
qBytes.Set(qGotInts.Get(2), 2);
qBytes.Set(0xf1.At(out byte _), 6);
var qIntAt1 = qGotInts.Get(1);
qBytes.Set(qIntAt1.Have(out byte _, 24), 7);
qBytes.Set(0x80.Or(qIntAt3.Down(16)).At(out byte _), 8);
qBytes.Set(qIntAt1.Have(out byte _, 16), 9);
qBytes.Set(qIntAt1.At(out short _), 10);
qBytes.Set(qGotInts.Get(0), 12);
return new(qBytes);
}
init => XDecimal =
value._Get(_isUnder, _getQuadruple, _getDecimal, _getIntegral, _getInfinity, _getNaN);
}
}
}
Still way too simple? Let's proceed to Real
. What's more important than your financial security? The answer is your physical security, where scientific computing comes in. This is the very field on the planet where we always aim to achieve everything and anything at all costs, such as biomedicine, nuclear physics, advanced weaponry, the list going on and on. Arguably, whenever we talk about physical security, we actually mean national security. Naturally, the floating-point is the most complex number directly supported by everyday hardware, where everyday literally carries the two most significant meanings of life: dependable and yet affordable, the best of both worlds brought to you by the magic of mass production. We do everything to make it happen!
namespace Pyramid.Kernel.Up
{
partial struct Real
{
static readonly ToBe<bool, Guid, long, int> _isUnder = (o, p, q) => q.IsntAbove(0x43ff);
static readonly ToBe<double, Guid, long, int> _getQuadruple =
(o, p, q) => o._GetDouble(p, q);
static readonly ToBe<double, Guid, long, int> _getDecimal =
(o, p, q) => (double)o._GetDecimal(p);
static readonly ToBe<double, Guid, long, int> _getIntegral = (o, p, q) => o._GetDouble(p);
static readonly ToBe<double, Guid, long, int> _getInfinity =
(o, p, q) => p.IsBelow(0) ? double.NegativeInfinity : double.PositiveInfinity;
static readonly ToBe<double, Guid, long, int> _getNaN = (o, p, q) => double.NaN;
public Guid XGuid
{
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
get
{
var qLong = BitConverter.DoubleToInt64Bits(XDouble);
var qSign = qLong.And(long.MinValue).DownAt(48);
var qExponent = qLong.HaveAt(0x7ff, 52);
Span<byte> qBytes = stackalloc byte[16];
if (qExponent.Isnt(0x7ff))
{
if (qExponent.Is(0x0))
{
qLong = qLong.And(0x3f_ffff_ffff_ffff);
var l = 32;
for (var m = 16; m.Isnt(0); m = m.Down(1))
l = qLong.Down(l).Is(0) ? l.Minus(m) : l.Plus(m);
l = 53.Minus(qLong.Down(l).Is(0) ? l : l.Plus(1));
qLong = qLong.Up(l).And(0x3f_ffff_ffff_ffff);
qBytes.Set(qSign.Or(0x3c01.Minus(l)).At(out short _), 0);
}
else qBytes.Set(qSign.Or(qExponent.Plus(0x3c00)).At(out short _), 0);
qBytes.Set(qLong.DownAt(20), 2);
var qInt = qLong.At();
qBytes.Set(0xf0.Or(qInt.Have(0xf, 16)).At(out byte _), 6);
qBytes.Set(qInt.Down(8).At(out byte _), 7);
qBytes.Set(0x80.Or(qInt.Have(0x1f, 3)).At(out byte _), 8);
qBytes.Set(qInt.Up(5).At(out byte _), 9);
}
else
{
qBytes.Set(qSign.Or(0x7ff).At(out short _), 0);
qBytes.Set((double.IsInfinity(XDouble) ? 0xf0 : 0xff).At(out byte _), 6);
qBytes.Set(0x80.At(out byte _), 8);
}
return new(qBytes);
}
init => XDouble =
value._Get(_isUnder, _getQuadruple, _getDecimal, _getIntegral, _getInfinity, _getNaN);
}
}
}
So much more interesting, isn't it? The IEEE floating-point comes in three forms: special, normal and subnormal, all of these hardwired into each single chip and processor, nearly without any exception. Compared to physical security, financial security means nothing, literally nothing, as you can see even in code complexity. Plus, we simply can't stress this more: it's got real hardware support! When will we support financial computing with specialized hardware? Probably never, for it's never worth our money. How about dedicated hardware supporting scientific computing, such as GPU chips? Any time is good time. Do it now. Do it today. Do it for everyone. That's the difference. You know what's funny. Even computer games get their own hardware support. This is not a joke and I am not complaining. Banks just don't pay us enough. That's it. They don't take your financial security seriously. They only care about themselves. OK, let's not digress and focus on the code behind the mysterious _Get
method in every init
definition.
namespace Pyramid.Kernel.Up
{
partial class It
{
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal static JOut? _Get<JOut>(this Guid it,
ToBe<bool, Guid, long, int>? ifToBeUnder, ToBe<JOut, Guid, long, int>? toGetQuadruple,
ToBe<JOut, Guid, long, int>? toGetDecimal, ToBe<JOut, Guid, long, int>? toGetIntegral,
ToBe<JOut, Guid, long, int>? toGetInfinity, ToBe<JOut, Guid, long, int>? toGetNaN)
{
Span<byte> qBytes = stackalloc byte[16];
it.TryWriteBytes(qBytes);
var qGotBytes = qBytes.As();
var qVersion = qGotBytes.Get(out byte _, 6);
if (qVersion.Has(0xf0) && qGotBytes.Get(out byte _, 8).And(0xe0).Is(0x80))
{
var qSignWithExponent = qGotBytes.Get(out short _, 0);
var qSign = qSignWithExponent.PutAt(0x8000, 48);
var qExponent = qSignWithExponent.And(0x7fff);
if (qExponent.Isnt(0x7fff))
if (ifToBeUnder.Be(it, qSign, qExponent)) return toGetQuadruple.Be(it, qSign, qExponent);
else throw new OverflowException();
else if (qVersion.Is(0xf1)) return toGetDecimal.Be(it, qSign, qExponent);
else if (qVersion.Is(0xf2)) return toGetIntegral.Be(it, qSign, qExponent);
else if (qVersion.Is(0xf0)) return toGetInfinity.Be(it, qSign, qExponent);
else if (qVersion.Is(0xff)) return toGetNaN.Be(it, qSign, qExponent);
}
else if (it.Hasnt()) return default;
throw new InvalidCastException();
}
}
}
So far so good, everything looking self-explanatory. Now, we can talk a little about the methods used in those ToBe
delegates:
namespace Pyramid.Kernel.Up
{
partial class It
{
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal static double _GetDouble(this Guid it, long sign)
{
var qGot = it._GetsIntegral(out var qDecimal, sign);
var qDouble = (double)qDecimal;
if (!qGot)
{
Span<byte> qBytes = stackalloc byte[16];
it.TryWriteBytes(qBytes);
var qGotBytes = qBytes.As();
var qExtra = qGotBytes.Get(out byte _, 2).Down(3);
if (sign.IsBelow(0)) qExtra = qExtra.Minus();
qDouble = qDouble.Plus(JId.ZExtraScale.Times(qExtra));
}
return qDouble;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal static double _GetDouble(this Guid it, long sign, int exponent)
{
Span<byte> qBytes = stackalloc byte[16];
it.TryWriteBytes(qBytes);
var qGotBytes = qBytes.As();
var qSignificand =
qGotBytes._GetSignificandAt0().Or(qGotBytes.Get(out short _, 8).PutAt(0x1fff, 6));
return BitConverter.Int64BitsToDouble(sign.Or(
exponent.IsAbove(0x3c00) ?
exponent.Minus(0x3c00).UpAt(52).Or(qSignificand.And(long.MaxValue).Down(11)) :
exponent.IsAbove(0x3bcc) ? qSignificand.Down(exponent.Minus(0x3b8d)) : 0));
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal static long _GetSignificandAt0(this ReadOnlySpan<byte> it) =>
long.MinValue.Or(it.Get(out int _, 2).UpAt(31)).Or(it.Get(out short _, 6).PutAt(0xfff, 19));
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal static bool _GetsIntegral(this Guid it, out decimal @out, long sign)
{
Span<byte> qBytes = stackalloc byte[16];
it.TryWriteBytes(qBytes);
Span<int> qInts = stackalloc int[4];
var qGotBytes = qBytes.As();
qInts.Set(sign.DownAt(32), 3);
var qIntAt2 = qGotBytes.Get(out int _, 2);
var qByteAt7 = qGotBytes.Get(out byte _, 7);
qInts.Set(qIntAt2.Up(5).Or(qByteAt7.Down(3)), 2);
qInts.Set(qByteAt7.Up(29).Or(qGotBytes.Get(out int _, 8).And(0x1fff_ffff)), 1);
qInts.Set(qGotBytes.Get(out int _, 12), 0);
@out = new(qInts);
return qIntAt2.And(0xf800_0000._At()).Is(0);
}
}
partial interface JId
{
static readonly double ZExtraScale = 2D.To(96);
}
}
These methods could have been longer. Fortunately, the .Net decimal
type has a fairly efficient internal format for us to leverage. Given that it boasts of a 96-bit significand, the longest among all numeric types without resorting to the ultra-slow memory heap, unlike java.math.BigDecimal
or System.Numerics.BigInteger
, we can simply write everything to a decimal
and convert from it to anything we like. If you wonder how I found out about the decimal
internal format, here is an introduction to it (Decimal.GetBits Method). The entire .Net base class library is open source, which you should have known, too. Since most decimal
operations are written in more efficient C++, there's no reason why not to reuse them.
To cover our base, we create shortcut methods, also known as C# inline macros, for all basic floating-point arithmetic operations, binary and decimal:
namespace Pyramid.Kernel.Up
{
partial class It
{
public static decimal Minus(this decimal it) => -it;
public static decimal Minus(this decimal it, decimal that) => it - that;
public static double Minus(this double it) => -it;
public static double Minus(this double it, double that) => it - that;
public static float Minus(this float it) => -it;
public static float Minus(this float it, float that) => it - that;
public static decimal Modulo(this decimal it, decimal that) => it % that;
public static double Modulo(this double it, double that) => it % that;
public static float Modulo(this float it, float that) => it % that;
public static decimal Over(this decimal it) => 1M.Over(it);
public static decimal Over(this decimal it, decimal that) => it / that;
public static double Over(this double it) => 1D.Over(it);
public static double Over(this double it, double that) => it / that;
public static float Over(this float it) => 1F.Over(it);
public static float Over(this float it, float that) => it / that;
public static decimal Plus(this decimal it, decimal that) => it + that;
public static double Plus(this double it, double that) => it + that;
public static float Plus(this float it, float that) => it + that;
public static decimal Times(this decimal it, decimal that) => it * that;
public static double Times(this double it, double that) => it * that;
public static float Times(this float it, float that) => it * that;
public static double To(this double it, double that) => Math.Pow(it, that);
public static float To(this float it, float that) => MathF.Pow(it, that);
}
}
Here is then the sole method to read version-15.1 UUID discussed in our previous tip-&-trick, reused by all our numeric types:
namespace Pyramid.Kernel.Up
{
partial class It
{
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal static decimal _GetDecimal(this Guid it, long sign)
{
Span<byte> qBytes = stackalloc byte[16];
it.TryWriteBytes(qBytes);
Span<int> qInts = stackalloc int[4];
var qGotBytes = qBytes.As();
qInts.Set(sign.DownAt(32).Or(qGotBytes.Get(out byte _, 8).Put(0x1f, 16)), 3);
qInts.Set(qGotBytes.Get(out int _, 2), 2);
qInts.Set(qGotBytes.Get(out byte _, 7).Up(24).Or(
qGotBytes.Get(out byte _, 9).Up(16)).Or(qGotBytes.Get(out short _, 10)), 1);
qInts.Set(qGotBytes.Get(out int _, 12), 0);
return new(qInts);
}
}
}
Now, we are ready to present the most interesting method: _GetQuadruple
. Whatever base we choose for our floating-point, binary or decimal, their intgral parts are always base-independent. The conversion of their fractional parts, on the other hand, can be fairly tricky, especially if you want to go very far with speed optimization. Unfortunately, most financial figures have very short fractions, favoring a brute-force approach on the average-case running time analysis. Is the worst-case time or the average-case time more important? Good question! In real life, we tend to focus on the average, the best example being the quick sort algorithm winning its strongest competitor, the merge sort, which performs far better in the worst case but merely slightly worse on average. The average-case running time is that crucial and critical, beating all other performance analysis models. Below is the code for the simplest brute-force approach:
namespace Pyramid.Kernel.Up
{
partial class It
{
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal static decimal _GetQuadruple(this Guid it,
long sign, int exponent, bool @decimal = false)
{
Span<byte> qBytes = stackalloc byte[16];
it.TryWriteBytes(qBytes);
var qGotBytes = qBytes.As();
var qLongAt1 = qGotBytes.Get(out long _, 8);
if (qLongAt1.And(0x1f_ffff_ffff_ffff).Isnt(0))
{
var qUsedAt1 = false;
var l = exponent.Minus(0x3fff).Be(out var qSignedExponent).And(0x1f).Plus(1);
var qSignificand = qGotBytes._GetSignificandAt0().Or(qLongAt1.Have(0xf_ffff, 41));
qLongAt1 = qLongAt1.Up(22);
var qDecimal = 0M;
if (qSignedExponent.IsntBelow(0))
{
var i = qSignedExponent.Right(5);
Span<int> qInts = stackalloc int[4];
int qSip(int l)
{
var qInt = qSignificand.DownAt(64.Minus(l));
i = i.Minus(1);
qSignificand = qSignificand.Up(l);
return qInt;
}
qInts.Set(qSip(l), i);
if (i.IsAbove(0)) qInts.Set(qSip(32), i);
if (i.IsAbove(0))
{
qUsedAt1 = true;
qSignificand = qSignificand.Or(qLongAt1.DownAt(64.Minus(l)));
qInts.Set(qSip(32), i);
}
qDecimal = new(qInts);
}
if (@decimal)
foreach (var a in JDime.ZBinaryPlaces.Have())
{
if (qSignificand.Is(0))
if (qUsedAt1) break;
else
{
qUsedAt1 = true;
qSignificand = qSignificand.Or(qLongAt1.DownAt(l));
}
if (qSignificand.IsBelow(0))
{
qDecimal = qDecimal.Be(out var qLast).Plus(a);
if (qDecimal.IsAbout(qLast)) break;
}
l = l.Minus(1);
qSignificand = qSignificand.Up(1);
}
return sign.IsBelow(0) ? qDecimal.Minus() : qDecimal;
}
else return (decimal)it._GetDouble(sign, exponent);
}
}
partial interface JDime
{
private static readonly decimal[] _binaryPlaces = new decimal[106];
static readonly ReadOnlyMemory<decimal> ZBinaryPlaces = _binaryPlaces;
static JDime()
{
var qLast = 1M;
for (var i = 0; i < _binaryPlaces.Length; i++) _binaryPlaces[i] = qLast = qLast.Over(2);
}
}
}
Is it already too hard? I'm not trying to scare you. This is truly the simplest conversion algorithm. Next time, we'll cover something a bit easier, like lockless concurrency, which is important for our cloud-based method body object.
Points of Interest
- The new C# supports default implementation for interface methods and operators, together with
Span<T>
and ReadOnlySpan<T>
on stackalloc
arrays without resorting to its unsafe
mode. We can now program in C# almost like C++, finally after 20 years of C# evolution. - The .Net
decimal
type has a highly efficient and flexible internal format, which we can leverage for all our numeric conversion needs. - The average-case running time is by far way more important than the worst-case running time. I can't stress this more. When in doubt, recall why the quick sort is always the winner unless we are working with linked lists or lazy lists, in which scenario we have no choice but use the merge sort. Remember, the quick sort is only slightly faster on the coefficient compared to the merge sort. That alone is enough to make it an instant winner. This is how the average-case running time is more crucial and critical!
History
- 14th September, 2021: Initial version