Skip to content

Add Decimal32, Decimal64, Decimal128 #100729

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 54 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
ac5e447
add constructor info
RaymondHuy Mar 11, 2024
1af3337
Add Decimal32
RaymondHuy Mar 30, 2024
cee05c8
add test cases
RaymondHuy Apr 5, 2024
da72dcd
Add Decimal128
RaymondHuy Apr 6, 2024
6f3827c
add tests
RaymondHuy Apr 6, 2024
7e9344b
Merge commit '83b0d939bedadf7d782b0b26307c2d8c1d5b76f4' into issue-81376
RaymondHuy Apr 7, 2024
f882b96
Fix digits pointer
RaymondHuy Apr 7, 2024
762f56d
Implement ToString() method.
RaymondHuy Apr 9, 2024
66a1127
resolve comments
RaymondHuy Apr 9, 2024
83937d2
resolve comments
RaymondHuy Apr 9, 2024
19755ba
Refactor shift operators to mask
RaymondHuy Apr 10, 2024
ede6ca3
Extract common mask to property.
RaymondHuy Apr 10, 2024
b4e5d34
add more tests
RaymondHuy Apr 10, 2024
504f042
add comments
RaymondHuy Apr 11, 2024
31ffd5c
Add overflow message.
RaymondHuy Apr 11, 2024
6212873
Add overflow message
RaymondHuy Apr 11, 2024
cb1d8b1
split uint128 to ulong.
RaymondHuy Apr 11, 2024
0a4620e
Change private to internal
RaymondHuy Apr 11, 2024
8f3d4ff
validate overflow decimal case.
RaymondHuy Apr 12, 2024
db834cb
add more tests
RaymondHuy Apr 18, 2024
2ed126b
add more tests
RaymondHuy May 29, 2024
d08b7b5
add more tests
RaymondHuy May 29, 2024
bcfdca6
Merge branch 'master' into issue-81376
RaymondHuy Nov 13, 2024
472a2fe
Merge branch 'master' into issue-81376
RaymondHuy Nov 22, 2024
49f6676
Correct naming
RaymondHuy Dec 17, 2024
f82d9a6
fix naming convention
RaymondHuy Dec 17, 2024
567906c
Merge branch 'main' into issue-81376
tannergooding Jan 30, 2025
70cc09d
Merge branch 'main' into issue-81376
RaymondHuy Mar 5, 2025
715677f
Merge branch 'main' into issue-81376
RaymondHuy Mar 14, 2025
a66d209
Merge branch 'main' into issue-81376
RaymondHuy Mar 20, 2025
2405a15
Merge branch 'main' into issue-81376
RaymondHuy Mar 24, 2025
926db20
Merge branch 'main' into issue-81376
RaymondHuy Mar 30, 2025
d41c56e
Merge branch 'main' into issue-81376
RaymondHuy Apr 3, 2025
ac8f7a6
resolve simple comments
RaymondHuy Apr 4, 2025
9d0fa8e
combine interface
RaymondHuy Apr 4, 2025
4478f54
add XML documentation
RaymondHuy Apr 4, 2025
7b1db43
add XML documentation
RaymondHuy Apr 4, 2025
79bea65
resolve comments
RaymondHuy Apr 4, 2025
e9a5cf7
Revert "resolve comments"
RaymondHuy Apr 4, 2025
0a57a1a
resolve comments
RaymondHuy Apr 4, 2025
ab7c5fb
resolve comments.
RaymondHuy Apr 5, 2025
6a1c2c3
remove unused fields.
RaymondHuy Apr 5, 2025
e584a92
use ArrayPool
RaymondHuy Apr 8, 2025
84ef494
add comments
RaymondHuy Apr 8, 2025
61eaba8
remove constructor
RaymondHuy Apr 15, 2025
2a1fc70
remove constructor
RaymondHuy Apr 15, 2025
afc15e4
add rounding method
RaymondHuy Apr 20, 2025
002d9dd
Merge branch 'main' into issue-81376
RaymondHuy Apr 20, 2025
0d598a2
add digit count condition check
RaymondHuy Apr 24, 2025
313c90d
fix wrong rounding
RaymondHuy Apr 24, 2025
7b6a3c0
correct last significand digit.
RaymondHuy Apr 24, 2025
d93ac2b
add comments
RaymondHuy Apr 24, 2025
c44003e
add more tests
RaymondHuy Apr 25, 2025
9533b39
remove unused test cases
RaymondHuy Apr 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/libraries/Common/src/System/Number.NumberBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ internal static partial class Number
internal const int UInt32NumberBufferLength = 10 + 1; // 10 for the longest input: 4,294,967,295
internal const int UInt64NumberBufferLength = 20 + 1; // 20 for the longest input: 18,446,744,073,709,551,615
internal const int UInt128NumberBufferLength = 39 + 1; // 39 for the longest input: 340,282,366,920,938,463,463,374,607,431,768,211,455
internal const int Decimal32NumberBufferLength = 97 + 1 + 1; // 97 for the longest input + 1 for rounding
internal const int Decimal64NumberBufferLength = 385 + 1 + 1; // 385 for the longest input + 1 for rounding
internal const int Decimal128NumberBufferLength = 6145 + 1 + 1; // 6145 for the longest input + 1 for rounding
Comment on lines +24 to +26
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment explaining why these numbers are correct would be beneficial.

There's many resources out there that break down the algorithm for how you get to 767 digits for the longest double (or 11563 for binary128), as it is:

MaxUnbiasedExponent + ⌊log10((2^SignificandBits – 1) /  2^MaxUnbiasedExponent)⌋ + 1 

But the algorithm for decimal isn't as well known (but should overall be similar).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @tannergooding , I have tried your formula but I can't get the 767 result.
MaxUnbiasedExponent = 1023, SignificandBits = 52
result is 1023 + (−309) + 1= 715.

Copy link
Member

@tannergooding tannergooding Apr 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I did it from memory initially. It should be (MaxUnbiasedExponent + TrailingSignificandBits - 1)

So for double it is 1074 + ⌊log10((2^53 – 1) / 2^1074)⌋ + 1. This is because the largest subnormal expands to a value that is 1074 digits in length (which is why in many comments and other places you'll see 1074 and 1075 being used)


internal unsafe ref struct NumberBuffer
{
Expand Down
18 changes: 18 additions & 0 deletions src/libraries/System.Private.CoreLib/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,15 @@
<data name="Arg_MustBeDecimal" xml:space="preserve">
<value>Object must be of type Decimal.</value>
</data>
<data name="Arg_MustBeDecimal32" xml:space="preserve">
<value>Object must be of type Decimal32.</value>
</data>
<data name="Arg_MustBeDecimal64" xml:space="preserve">
<value>Object must be of type Decimal64.</value>
</data>
<data name="Arg_MustBeDecimal128" xml:space="preserve">
<value>Object must be of type Decimal128.</value>
</data>
<data name="Arg_MustBeDelegate" xml:space="preserve">
<value>Type must derive from Delegate.</value>
</data>
Expand Down Expand Up @@ -3197,6 +3206,15 @@
<data name="Overflow_Decimal" xml:space="preserve">
<value>Value was either too large or too small for a Decimal.</value>
</data>
<data name="Overflow_Decimal32" xml:space="preserve">
<value>Value was either too large or too small for a Decimal32.</value>
</data>
<data name="Overflow_Decimal64" xml:space="preserve">
<value>Value was either too large or too small for a Decimal64.</value>
</data>
<data name="Overflow_Decimal128" xml:space="preserve">
<value>Value was either too large or too small for a Decimal128.</value>
</data>
<data name="Overflow_Duration" xml:space="preserve">
<value>The duration cannot be returned for TimeSpan.MinValue because the absolute value of TimeSpan.MinValue exceeds the value of TimeSpan.MaxValue.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,10 @@
<Compile Include="$(MSBuildThisFileDirectory)System\IFormatProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IFormattable.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Index.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Number.DecimalIeee754.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Numerics\Decimal128.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Numerics\Decimal32.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Numerics\Decimal64.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\SearchValues\Any1CharPackedSearchValues.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\SearchValues\Any1CharPackedIgnoreCaseSearchValues.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\SearchValues\Any2CharPackedIgnoreCaseSearchValues.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Numerics;

namespace System
{
internal interface IDecimalIeee754ConstructorInfo<TSelf, TSignificand, TValue>
where TSelf : unmanaged, IDecimalIeee754ConstructorInfo<TSelf, TSignificand, TValue>
where TSignificand : IBinaryInteger<TSignificand>
where TValue : IBinaryInteger<TValue>
{
static abstract TSignificand MaxSignificand { get; }
static abstract int MaxDecimalExponent { get; }
static abstract int MinDecimalExponent { get; }
static abstract int NumberDigitsPrecision { get; }
static abstract int Bias { get; }
static abstract int CountDigits(TSignificand number);
static abstract TSignificand Power10(int exponent);
static abstract int MostSignificantBitNumberOfSignificand { get; }
static abstract int NumberBitsEncoding { get; }
static abstract int NumberBitsCombinationField { get; }
static abstract int NumberBitsExponent { get; }
static abstract TValue PositiveInfinityBits { get; }
static abstract TValue NegativeInfinityBits { get; }
static abstract TValue Zero { get; }
}

internal interface IDecimalIeee754UnpackInfo<TSelf, TSignificand, TValue>
where TSelf : unmanaged, IDecimalIeee754UnpackInfo<TSelf, TSignificand, TValue>
where TSignificand : IBinaryInteger<TSignificand>
where TValue : IBinaryInteger<TValue>
{
static abstract TValue SignMask { get; }
static abstract int NumberBitsEncoding { get; }
static abstract int NumberBitsExponent { get; }
static abstract int NumberDigitsPrecision { get; }
static abstract int Bias { get; }
static abstract TSignificand TwoPowerMostSignificantBitNumberOfSignificand { get; }
static abstract int ConvertToExponent(TValue value);
static abstract TSignificand ConvertToSignificand(TValue value);
static abstract TSignificand Power10(int exponent);
}

internal static partial class Number
{
internal static TValue CalDecimalIeee754<TDecimal, TSignificand, TValue>(TSignificand significand, int exponent)
where TDecimal : unmanaged, IDecimalIeee754ConstructorInfo<TDecimal, TSignificand, TValue>
where TSignificand : IBinaryInteger<TSignificand>
where TValue : IBinaryInteger<TValue>
{
if (significand == TSignificand.Zero)
{
return TValue.Zero;
}

TSignificand unsignedSignificand = significand > TSignificand.Zero ? significand : -significand;

if (unsignedSignificand > TDecimal.MaxSignificand && exponent > TDecimal.MaxDecimalExponent)
{
return significand > TSignificand.Zero ? TDecimal.PositiveInfinityBits : TDecimal.NegativeInfinityBits;
}

TSignificand ten = TSignificand.CreateTruncating(10);
if (exponent < TDecimal.MinDecimalExponent)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of this logic is complex and doing things that aren't "immediately obvious" if you aren't familiar with the algorithm or format.

We need some comments inserted into the code to help explain, overall, what the goal of various blocks of code are. This helps lay out any invariants, any base math that is being done, and lets readers better assert that the code is doing what is intended.

This does not need to be "obvious" comments or repeat exactly what the code says. But should instead explain the overarching theme such as is done for https://source.dot.net/#System.Private.CoreLib/src/libraries/Common/src/System/Number.Parsing.Common.cs,116 or https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Number.Dragon4.cs,86

Something like the while loop below should similarly cover why it's reducing the significand to bring exponent >= MinDecimalExponent. Some of this is "more obvious" but should still be covered to help explain that's what the loop is doing. That is significand: 100000, exponent: Min - 2 is the same as significand: 1000, exponent: Min, or as another example 10e2 and 1e3 are the same number.

Other cases like why converting 100050e2 into 1000e4 is "safe" are not obvious and initially look to be potential bugs because of how that could theoretically impact rounding to the nearest representable.

{
while (unsignedSignificand >= ten)
{
unsignedSignificand /= ten;
++exponent;
}
if (exponent < TDecimal.MinDecimalExponent)
{
throw new OverflowException(SR.Overflow_Decimal);
}
}

if (unsignedSignificand > TDecimal.MaxSignificand)
{
int numberDigitsRemoving = TDecimal.CountDigits(unsignedSignificand) - TDecimal.NumberDigitsPrecision;

if (exponent + numberDigitsRemoving > TDecimal.MaxDecimalExponent)
{
throw new OverflowException(SR.Overflow_Decimal);
}

exponent += numberDigitsRemoving;
TSignificand two = TSignificand.CreateTruncating(2);
TSignificand divisor = TDecimal.Power10(numberDigitsRemoving);
TSignificand quotient = unsignedSignificand / divisor;
TSignificand remainder = unsignedSignificand % divisor;
TSignificand midPoint = divisor / two;
bool needRouding = remainder > midPoint || (remainder == midPoint && quotient % two == TSignificand.One);

if (needRouding && quotient == TDecimal.MaxSignificand && exponent < TDecimal.MaxDecimalExponent)
{
unsignedSignificand = TDecimal.Power10(TDecimal.NumberDigitsPrecision - 1);
exponent++;
}
else if (needRouding && quotient < TDecimal.MaxSignificand)
{
unsignedSignificand = quotient + TSignificand.One;
}
else
{
unsignedSignificand = quotient;
}
}
else if (exponent > TDecimal.MaxDecimalExponent)
{
int numberZeroDigits = exponent - TDecimal.MaxDecimalExponent;
int numberSignificandDigits = TDecimal.CountDigits(unsignedSignificand);

if (numberSignificandDigits + numberZeroDigits > TDecimal.NumberDigitsPrecision)
{
throw new OverflowException(SR.Overflow_Decimal);
}
unsignedSignificand *= TDecimal.Power10(numberZeroDigits);
exponent -= numberZeroDigits;
}

exponent += TDecimal.Bias;
bool msbSignificand = (unsignedSignificand & TSignificand.One << TDecimal.MostSignificantBitNumberOfSignificand) != TSignificand.Zero;

TValue value = TValue.Zero;
TValue exponentVal = TValue.CreateTruncating(exponent);
TValue significandVal = TValue.CreateTruncating(unsignedSignificand);

if (significand < TSignificand.Zero)
{
value = TValue.One << TDecimal.NumberBitsEncoding - 1;
}

if (msbSignificand)
{
value ^= TValue.One << TDecimal.NumberBitsEncoding - 2;
value ^= TValue.One << TDecimal.NumberBitsEncoding - 3;
exponentVal <<= TDecimal.NumberBitsEncoding - 4;
value ^= exponentVal;
significandVal <<= TDecimal.NumberBitsEncoding - TDecimal.MostSignificantBitNumberOfSignificand;
significandVal >>= TDecimal.NumberBitsCombinationField;
value ^= significandVal;
}
else
{
exponentVal <<= TDecimal.NumberBitsEncoding - TDecimal.NumberBitsExponent - 1;
value ^= exponentVal;
value ^= significandVal;
}

return value;
}

internal struct DecimalIeee754<TSignificand>
where TSignificand : IBinaryInteger<TSignificand>
{
public bool Signed { get; }
public int Exponent { get; }
public TSignificand Significand { get; }

public DecimalIeee754(bool signed, int exponent, TSignificand significand)
{
Signed = signed;
Exponent = exponent;
Significand = significand;
}
}

internal static DecimalIeee754<TSignificand> UnpackDecimalIeee754<TDecimal, TSignificand, TValue>(TValue value)
where TDecimal : unmanaged, IDecimalIeee754UnpackInfo<TDecimal, TSignificand, TValue>
where TSignificand : IBinaryInteger<TSignificand>
where TValue : IBinaryInteger<TValue>
{
bool signed = (value & TDecimal.SignMask) != TValue.Zero;
TValue g0g1Bits = (value << 1) >> TDecimal.NumberBitsEncoding - 2;
TSignificand significand;
int exponent;

if (g0g1Bits == TValue.CreateTruncating(3))
{
exponent = TDecimal.ConvertToExponent((value << 3) >> TDecimal.NumberBitsEncoding - TDecimal.NumberBitsExponent);
significand = TDecimal.ConvertToSignificand((value << TDecimal.NumberBitsEncoding + 3) >> TDecimal.NumberBitsEncoding + 3);
significand += TDecimal.TwoPowerMostSignificantBitNumberOfSignificand;
}
else
{
exponent = TDecimal.ConvertToExponent((value << 1) >> TDecimal.NumberBitsEncoding - TDecimal.NumberBitsExponent);
significand = TDecimal.ConvertToSignificand((value << TDecimal.NumberBitsExponent + 1) >> TDecimal.NumberBitsExponent + 1);
}

return new DecimalIeee754<TSignificand>(signed, exponent - TDecimal.Bias, significand);
}

internal static int CompareDecimalIeee754<TDecimal, TSignificand, TValue>(TValue currentValue, TValue otherValue)
where TDecimal : unmanaged, IDecimalIeee754UnpackInfo<TDecimal, TSignificand, TValue>
where TSignificand : IBinaryInteger<TSignificand>
where TValue : IBinaryInteger<TValue>
{
if (currentValue == otherValue)
{
return 0;
}
DecimalIeee754<TSignificand> current = UnpackDecimalIeee754<TDecimal, TSignificand, TValue>(currentValue);
DecimalIeee754<TSignificand> other = UnpackDecimalIeee754<TDecimal, TSignificand, TValue>(otherValue);

if (current.Signed && !other.Signed) return -1;

if (!current.Signed && other.Signed) return 1;

if (current.Exponent > other.Exponent)
{
return current.Signed ? -InternalUnsignedCompare(current, other) : InternalUnsignedCompare(current, other);
}

if (current.Exponent < other.Exponent)
{
return current.Signed ? InternalUnsignedCompare(other, current) : -InternalUnsignedCompare(current, other);
}

if (current.Significand == other.Significand) return 0;

if (current.Significand > other.Significand)
{
return current.Signed ? -1 : 1;
}
else
{
return current.Signed ? 1 : -1;
}

static int InternalUnsignedCompare(DecimalIeee754<TSignificand> biggerExp, DecimalIeee754<TSignificand> smallerExp)
{
if (biggerExp.Significand >= smallerExp.Significand) return 1;

int diffExponent = biggerExp.Exponent - smallerExp.Exponent;
if (diffExponent < TDecimal.NumberDigitsPrecision)
{
TSignificand factor = TDecimal.Power10(diffExponent);
TSignificand quotient = smallerExp.Significand / biggerExp.Significand;
TSignificand remainder = smallerExp.Significand % biggerExp.Significand;

if (quotient < factor) return 1;
if (quotient > factor) return -1;
if (remainder > TSignificand.Zero) return -1;
return 0;
}

return 1;
}
}
}
}
Loading
Loading