Click here to Skip to main content
15,036,145 members
Articles / Programming Languages / C#
Tip/Trick
Posted 14 Sep 2021

Tagged as

Stats

2.1K views
2 bookmarked

How to Implement a Robust C# Cloud Id in the New Nullable Context with Generics?

Rate me:
Please Sign up or sign in to vote.
4.56/5 (2 votes)
14 Sep 2021CPOL8 min read
An overview of nulls and generics with id implementation details.
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:

C#
using System;
namespace Pyramid.Kernel.Up
{
 /// <summary>An identifying <see cref="JIt"/>.</summary>
 public partial interface JId : JIt
 {
  /// <summary>Its <see cref="Guid"/>.</summary>
  Guid XGuid { get; init; }
 }
 /// <summary>An autonomous <see cref="JId"/>.</summary>
 /// <typeparam name="J">Its self-referencing <see cref="JId{J}"/>.</typeparam>
 public partial interface JId<J> : JId, JIt<J> where J : JId<J>, new() { }
 /// <summary>An invariant <see cref="JId"/>.</summary>
 [Serializable]
 public readonly partial struct Id : JId<Id>
 {
  /// <inheritdoc cref="JId.XGuid"/>
  public Guid XGuid { get; init; }
 }
 /// <summary>An autonomous <see cref="JId{J}"/>.</summary>
 /// <typeparam name="J">Its self-referencing <see cref="Id{J}"/>.</typeparam>
 [Serializable]
 public abstract partial class Id<J> : It<J>, JId<J> where J : Id<J>, new()
 {
  /// <inheritdoc cref="JId.XGuid"/>
  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:

C#
namespace Pyramid.Kernel.Up
{
 /// <summary>A <see cref="double"/> <see cref="JId"/>.</summary>
 public partial interface JReal : JId
 {
  /// <summary>Its <see cref="double"/>.</summary>
  double XDouble { get; init; }
 }
 /// <summary>An autonomous <see cref="JReal"/>.</summary>
 /// <typeparam name="J">Its self-referencing <see cref="JReal{J}"/>.</typeparam>
 public partial interface JReal<J> : JId<J>, JReal where J : JReal<J>, new() { }
 /// <summary>An invariant <see cref="JReal"/>.</summary>
 [Serializable]
 public readonly partial struct Real : JReal<Real>
 {
  /// <inheritdoc cref="JReal.XDouble"/>
  public double XDouble { get; init; }
  /// <inheritdoc cref="JId.XGuid"/>
  public Guid XGuid { get; init; }
 }
 /// <summary>An autonomous <see cref="JReal{J}"/>.</summary>
 /// <typeparam name="J">Its self-referencing <see cref="Real{J}"/>.</typeparam>
 [Serializable]
 public abstract partial class Real<J> : Id<J>, JReal<J> where J : Real<J>, new()
 {
  /// <inheritdoc cref="JReal.XDouble"/>
  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.

C#
namespace Pyramid.Kernel.Up
{
 /// <summary>A <see cref="decimal"/> <see cref="JId"/>.</summary>
 public partial interface JDime : JId
 {
  /// <summary>Its <see cref="decimal"/>.</summary>
  decimal XDecimal { get; init; }
 }
 /// <summary>An autonomous <see cref="JDime"/>.</summary>
 /// <typeparam name="J">Its self-referencing <see cref="JDime{J}"/>.</typeparam>
 public partial interface JDime<J> : JDime, JId<J> where J : JDime<J>, new() { }
 /// <summary>An invariant <see cref="JDime"/>.</summary>
 [Serializable]
 public readonly partial struct Dime : JDime
 {
  /// <inheritdoc cref="JDime.XDecimal"/>
  public decimal XDecimal { get; init; }
  /// <inheritdoc cref="JId.XGuid"/>
  public Guid XGuid { get; init; }
 }
 /// <summary>An autonomous <see cref="JDime{J}"/>.</summary>
 /// <typeparam name="J">Its self-referencing <see cref="Dime{J}"/>.</typeparam>
 [Serializable]
 public abstract partial class Dime<J> : Id<J>, JDime<J> where J : Dime<J>, new()
 {
  /// <inheritdoc cref="JDime.XDecimal"/>
  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.

C#
namespace Pyramid.Kernel.Up
{
 /// <summary>A <see cref="long"/> <see cref="JDime"/>.</summary>
 public partial interface JHit : JDime
 {
  /// <summary>Its <see cref="long"/>.</summary>
  long XInt64 { get; init; }
 }
 /// <summary>An autonomous <see cref="JHit"/>.</summary>
 /// <typeparam name="J">Its self-referencing <see cref="JHit{J}"/>.</typeparam>
 public partial interface JHit<J> : JDime<J>, JHit where J : JHit<J>, new() { }
 /// <summary>An invariant <see cref="JHit"/>.</summary>
 [Serializable]
 public readonly partial struct Hit : JHit<Hit>
 {
  /// <inheritdoc cref="JDime.XDecimal"/>
  public decimal XDecimal { get => XInt64; init => XInt64 = (long)value; }
  /// <inheritdoc cref="JId.XGuid"/>
  public Guid XGuid { get; init; }
  /// <inheritdoc cref="JHit.XInt64"/>
  public long XInt64 { get; init; }
 }
 /// <summary>An autonomous <see cref="JHit{J}"/>.</summary>
 /// <typeparam name="J">Its self-referencing <see cref="Hit{J}"/>.</typeparam>
 [Serializable]
 public abstract partial class Hit<J> : Dime<J>, JHit<J> where J : Hit<J>, new()
 {
  /// <inheritdoc cref="JDime.XDecimal"/>
  public sealed override decimal XDecimal
  {
   get => new Hit { XInt64 = XInt64 }.XDecimal;
   init => XInt64 = new Hit { XDecimal = value }.XInt64;
  }
  /// <inheritdoc cref="JId.XGuid"/>
  public sealed override Guid XGuid
  {
   get => new Hit { XInt64 = XInt64 }.XGuid;
   init => XInt64 = new Hit { XGuid = value }.XInt64;
  }
  /// <inheritdoc cref="JHit.XInt64"/>
  public long XInt64 { get; init; }
 }
}

Now, let's start writing some code, finally, beginning with Hit, the simplest one:

C#
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();
  /// <inheritdoc cref="JId.XGuid"/>
  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:

C#
namespace Pyramid.Kernel.Up
{
 /// <summary>A 0-ary variant <see cref="JBe.ToBe{JOut, JIn}"/>.</summary>
 /// <typeparam name="JOut">Its output <see cref="object"/>.</typeparam>
 [Serializable]
 public delegate JOut? ToBe<out JOut
  >(
  );
 /// <summary>A 1-ary variant <see cref="JBe.ToBe{JOut, JIn}"/>.</summary>
 /// <typeparam name="JOut">Its output <see cref="object"/>.</typeparam>
 /// <typeparam name="JA">Its 1-st input <see cref="object"/>.</typeparam>
 /// <param name="a">An <typeparamref name="JA"/>.</param>
 [Serializable]
 public delegate JOut? ToBe<out JOut,
  in JA>(
  JA? a = default);
 /// <summary>A 2-ary variant <see cref="JBe.ToBe{JOut, JIn}"/>.</summary>
 /// <typeparam name="JOut">Its output <see cref="object"/>.</typeparam>
 /// <typeparam name="JA">Its 1-st input <see cref="object"/>.</typeparam>
 /// <typeparam name="JB">Its 2-nd input <see cref="object"/>.</typeparam>
 /// <param name="a">An <typeparamref name="JA"/>.</param>
 /// <param name="b">A <typeparamref name="JB"/>.</param>
 [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:

C#
namespace Pyramid.Kernel.Up
{
 partial class It
 {
  /// <summary>Get its <see cref="decimal"/> absolute value to <see langword="return"/>.
  /// </summary>
  /// <param name="it">A <see cref="decimal"/>.</param>
  public static decimal Abs(this decimal it) => Math.Abs(it);
  /// <summary>Get its <see cref="double"/> absolute value to <see langword="return"/>.
  /// </summary>
  /// <param name="it">A <see cref="double"/>.</param>
  public static double Abs(this double it) => Math.Abs(it);
  /// <summary>Get its <see cref="float"/> absolute value to <see langword="return"/>.
  /// </summary>
  /// <param name="it">A <see cref="float"/>.</param>
  public static float Abs(this float it) => Math.Abs(it);
  /// <summary>Get its <see cref="int"/> absolute value to <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/>.</param>
  public static int Abs(this int it) => Math.Abs(it);
  /// <summary>Get its <see cref="long"/> absolute value to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="long"/>.</param>
  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:

C#
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();
  /// <inheritdoc cref="JId.XGuid"/>
  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!

C#
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;
  /// <inheritdoc cref="JId.XGuid"/>
  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.

C#
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:

C#
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
 {
  /// <summary>The extra scale <see cref="double"/> above the <see cref="decimal.MaxValue"/>.
  /// </summary>
  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:

C#
namespace Pyramid.Kernel.Up
{
 partial class It
 {
  /// <summary>Minus inversely by a 0 <see cref="decimal"/> to <see langword="return"/>.
  /// </summary>
  /// <param name="it">A <see cref="decimal"/>.</param>
  public static decimal Minus(this decimal it) => -it;
  /// <summary>Minus <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="decimal"/>.</param>
  /// <param name="that">Another <see cref="decimal"/>.</param>
  public static decimal Minus(this decimal it, decimal that) => it - that;
  /// <summary>Minus inversely by a 0 <see cref="double"/> to <see langword="return"/>.
  /// </summary>
  /// <param name="it">A <see cref="double"/>.</param>
  public static double Minus(this double it) => -it;
  /// <summary>Minus <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="double"/>.</param>
  /// <param name="that">Another <see cref="double"/>.</param>
  public static double Minus(this double it, double that) => it - that;
  /// <summary>Minus inversely by a 0 <see cref="float"/> to <see langword="return"/>.
  /// </summary>
  /// <param name="it">A <see cref="float"/>.</param>
  public static float Minus(this float it) => -it;
  /// <summary>Minus <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="float"/>.</param>
  /// <param name="that">Another <see cref="float"/>.</param>
  public static float Minus(this float it, float that) => it - that;
  /// <summary>Modulo <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="decimal"/>.</param>
  /// <param name="that">Another <see cref="decimal"/>.</param>
  public static decimal Modulo(this decimal it, decimal that) => it % that;
  /// <summary>Modulo <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="double"/>.</param>
  /// <param name="that">Another <see cref="double"/>.</param>
  public static double Modulo(this double it, double that) => it % that;
  /// <summary>Modulo <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="float"/>.</param>
  /// <param name="that">Another <see cref="float"/>.</param>
  public static float Modulo(this float it, float that) => it % that;
  /// <summary>Over inversely by a <see cref="decimal"/> 1 to <see langword="return"/>.
  /// </summary>
  /// <param name="it">A <see cref="decimal"/>.</param>
  public static decimal Over(this decimal it) => 1M.Over(it);
  /// <summary>Over <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="decimal"/>.</param>
  /// <param name="that">Another <see cref="decimal"/>.</param>
  public static decimal Over(this decimal it, decimal that) => it / that;
  /// <summary>Over inversely by a <see cref="double"/> 1 to <see langword="return"/>.
  /// </summary>
  /// <param name="it">A <see cref="double"/>.</param>
  public static double Over(this double it) => 1D.Over(it);
  /// <summary>Over <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="double"/>.</param>
  /// <param name="that">Another <see cref="double"/>.</param>
  public static double Over(this double it, double that) => it / that;
  /// <summary>Over inversely by a <see cref="float"/> 1 to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="double"/>.</param>
  public static float Over(this float it) => 1F.Over(it);
  /// <summary>Over <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="float"/>.</param>
  /// <param name="that">Another <see cref="float"/>.</param>
  public static float Over(this float it, float that) => it / that;
  /// <summary>Plus <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="decimal"/>.</param>
  /// <param name="that">Another <see cref="decimal"/>.</param>
  public static decimal Plus(this decimal it, decimal that) => it + that;
  /// <summary>Plus <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="double"/>.</param>
  /// <param name="that">Another <see cref="double"/>.</param>
  public static double Plus(this double it, double that) => it + that;
  /// <summary>Plus <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="float"/>.</param>
  /// <param name="that">Another <see cref="float"/>.</param>
  public static float Plus(this float it, float that) => it + that;
  /// <summary>Times <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="decimal"/>.</param>
  /// <param name="that">Another <see cref="decimal"/>.</param>
  public static decimal Times(this decimal it, decimal that) => it * that;
  /// <summary>Times <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="double"/>.</param>
  /// <param name="that">Another <see cref="double"/>.</param>
  public static double Times(this double it, double that) => it * that;
  /// <summary>Times <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="float"/>.</param>
  /// <param name="that">Another <see cref="float"/>.</param>
  public static float Times(this float it, float that) => it * that;
  /// <summary>To the power of <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="double"/>.</param>
  /// <param name="that">Another <see cref="double"/>.</param>
  public static double To(this double it, double that) => Math.Pow(it, that);
  /// <summary>To the power of <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="float"/>.</param>
  /// <param name="that">Another <see cref="float"/>.</param>
  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:

C#
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:

C#
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];
  /// <summary>The negative powers of <see cref="decimal"/> 2.</summary>
  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

  1. 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.
  2. The .Net decimal type has a highly efficient and flexible internal format, which we can leverage for all our numeric conversion needs.
  3. 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Code Fan
Software Developer
Canada Canada
Montreal is the second largest French city in the world, next to Paris. I like the fact that real estate is dirt cheap here, so cheap that software development alone enables a financial capacity to afford a nearly 2,000-square-foot luxurious condo right in the middle of Downtown Montreal, a 5-minute walk from my office, beside the largest and oldest art museum in Canada with visitors and tourists from all over the planet, including Hollywood stars. I've chosen C# as my first language at Code Project, because it is the only garbage-collected language and platform meeting the performance requirements for real-time game programming, proven by Unity. Code must be perfect, providing safety, security, performance, scalability, availability, reliability, maintainability, extensibility, portability, compatibility, interoperability, readability, productivity, just to name a few. C# is the only language that comes close, with Rust second to it. That being said, even C# is far from being perfect. I dream my own programming language, while on my journey to it. We will see how it goes!

Comments and Discussions

 
SuggestionThis article give no indication of the problem its trying to solve. Pin
theperm16-Sep-21 4:00
Membertheperm16-Sep-21 4:00 
GeneralRe: This article give no indication of the problem its trying to solve. Pin
Code Fan16-Sep-21 13:36
MemberCode Fan16-Sep-21 13:36 
GeneralMy vote of 5 Pin
Franc Morales15-Sep-21 10:25
MemberFranc Morales15-Sep-21 10:25 
GeneralRe: My vote of 5 Pin
Code Fan15-Sep-21 14:03
MemberCode Fan15-Sep-21 14:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.