Skip to content

Commit a4407a1

Browse files
authored
Use generic math for floating point formatting (dotnet#102683)
* Generic DiyFp * Generic Grisu3 * Generic Dragon4 * Add MaxRoundTripDigits to MaxPrecisionCustomFormat to FormatInfo * Generic FormatFloat * Adapt with existing FP types * Fix ExtractFractionAndBiasedExponent
1 parent 555dde4 commit a4407a1

File tree

8 files changed

+124
-468
lines changed

8 files changed

+124
-468
lines changed

src/libraries/System.Private.CoreLib/src/System/Double.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -354,33 +354,33 @@ public override int GetHashCode()
354354

355355
public override string ToString()
356356
{
357-
return Number.FormatDouble(m_value, null, NumberFormatInfo.CurrentInfo);
357+
return Number.FormatFloat(m_value, null, NumberFormatInfo.CurrentInfo);
358358
}
359359

360360
public string ToString([StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format)
361361
{
362-
return Number.FormatDouble(m_value, format, NumberFormatInfo.CurrentInfo);
362+
return Number.FormatFloat(m_value, format, NumberFormatInfo.CurrentInfo);
363363
}
364364

365365
public string ToString(IFormatProvider? provider)
366366
{
367-
return Number.FormatDouble(m_value, null, NumberFormatInfo.GetInstance(provider));
367+
return Number.FormatFloat(m_value, null, NumberFormatInfo.GetInstance(provider));
368368
}
369369

370370
public string ToString([StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format, IFormatProvider? provider)
371371
{
372-
return Number.FormatDouble(m_value, format, NumberFormatInfo.GetInstance(provider));
372+
return Number.FormatFloat(m_value, format, NumberFormatInfo.GetInstance(provider));
373373
}
374374

375375
public bool TryFormat(Span<char> destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.NumericFormat)] ReadOnlySpan<char> format = default, IFormatProvider? provider = null)
376376
{
377-
return Number.TryFormatDouble(m_value, format, NumberFormatInfo.GetInstance(provider), destination, out charsWritten);
377+
return Number.TryFormatFloat(m_value, format, NumberFormatInfo.GetInstance(provider), destination, out charsWritten);
378378
}
379379

380380
/// <inheritdoc cref="IUtf8SpanFormattable.TryFormat" />
381381
public bool TryFormat(Span<byte> utf8Destination, out int bytesWritten, [StringSyntax(StringSyntaxAttribute.NumericFormat)] ReadOnlySpan<char> format = default, IFormatProvider? provider = null)
382382
{
383-
return Number.TryFormatDouble(m_value, format, NumberFormatInfo.GetInstance(provider), utf8Destination, out bytesWritten);
383+
return Number.TryFormatFloat(m_value, format, NumberFormatInfo.GetInstance(provider), utf8Destination, out bytesWritten);
384384
}
385385

386386
public static double Parse(string s) => Parse(s, NumberStyles.Float | NumberStyles.AllowThousands, provider: null);
@@ -2335,6 +2335,10 @@ public static bool TryParse(ReadOnlySpan<byte> utf8Text, NumberStyles style, IFo
23352335

23362336
static ulong IBinaryFloatParseAndFormatInfo<double>.FloatToBits(double value) => BitConverter.DoubleToUInt64Bits(value);
23372337

2338+
static int IBinaryFloatParseAndFormatInfo<double>.MaxRoundTripDigits => 17;
2339+
2340+
static int IBinaryFloatParseAndFormatInfo<double>.MaxPrecisionCustomFormat => 15;
2341+
23382342
//
23392343
// Helpers
23402344
//

src/libraries/System.Private.CoreLib/src/System/Half.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -505,31 +505,31 @@ public override int GetHashCode()
505505
/// </summary>
506506
public override string ToString()
507507
{
508-
return Number.FormatHalf(this, null, NumberFormatInfo.CurrentInfo);
508+
return Number.FormatFloat(this, null, NumberFormatInfo.CurrentInfo);
509509
}
510510

511511
/// <summary>
512512
/// Returns a string representation of the current value using the specified <paramref name="format"/>.
513513
/// </summary>
514514
public string ToString([StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format)
515515
{
516-
return Number.FormatHalf(this, format, NumberFormatInfo.CurrentInfo);
516+
return Number.FormatFloat(this, format, NumberFormatInfo.CurrentInfo);
517517
}
518518

519519
/// <summary>
520520
/// Returns a string representation of the current value with the specified <paramref name="provider"/>.
521521
/// </summary>
522522
public string ToString(IFormatProvider? provider)
523523
{
524-
return Number.FormatHalf(this, null, NumberFormatInfo.GetInstance(provider));
524+
return Number.FormatFloat(this, null, NumberFormatInfo.GetInstance(provider));
525525
}
526526

527527
/// <summary>
528528
/// Returns a string representation of the current value using the specified <paramref name="format"/> and <paramref name="provider"/>.
529529
/// </summary>
530530
public string ToString([StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format, IFormatProvider? provider)
531531
{
532-
return Number.FormatHalf(this, format, NumberFormatInfo.GetInstance(provider));
532+
return Number.FormatFloat(this, format, NumberFormatInfo.GetInstance(provider));
533533
}
534534

535535
/// <summary>
@@ -542,13 +542,13 @@ public string ToString([StringSyntax(StringSyntaxAttribute.NumericFormat)] strin
542542
/// <returns></returns>
543543
public bool TryFormat(Span<char> destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.NumericFormat)] ReadOnlySpan<char> format = default, IFormatProvider? provider = null)
544544
{
545-
return Number.TryFormatHalf(this, format, NumberFormatInfo.GetInstance(provider), destination, out charsWritten);
545+
return Number.TryFormatFloat(this, format, NumberFormatInfo.GetInstance(provider), destination, out charsWritten);
546546
}
547547

548548
/// <inheritdoc cref="IUtf8SpanFormattable.TryFormat" />
549549
public bool TryFormat(Span<byte> utf8Destination, out int bytesWritten, [StringSyntax(StringSyntaxAttribute.NumericFormat)] ReadOnlySpan<char> format = default, IFormatProvider? provider = null)
550550
{
551-
return Number.TryFormatHalf(this, format, NumberFormatInfo.GetInstance(provider), utf8Destination, out bytesWritten);
551+
return Number.TryFormatFloat(this, format, NumberFormatInfo.GetInstance(provider), utf8Destination, out bytesWritten);
552552
}
553553

554554
//
@@ -2373,5 +2373,9 @@ public static bool TryParse(ReadOnlySpan<byte> utf8Text, NumberStyles style, IFo
23732373
static Half IBinaryFloatParseAndFormatInfo<Half>.BitsToFloat(ulong bits) => BitConverter.UInt16BitsToHalf((ushort)(bits));
23742374

23752375
static ulong IBinaryFloatParseAndFormatInfo<Half>.FloatToBits(Half value) => BitConverter.HalfToUInt16Bits(value);
2376+
2377+
static int IBinaryFloatParseAndFormatInfo<Half>.MaxRoundTripDigits => 5;
2378+
2379+
static int IBinaryFloatParseAndFormatInfo<Half>.MaxPrecisionCustomFormat => 5;
23762380
}
23772381
}

src/libraries/System.Private.CoreLib/src/System/Number.DiyFp.cs

Lines changed: 10 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,6 @@ internal static partial class Number
1717
// DiyFp are not designed to contain special doubles (NaN and Infinity).
1818
internal readonly ref struct DiyFp
1919
{
20-
public const int DoubleImplicitBitIndex = 52;
21-
public const int SingleImplicitBitIndex = 23;
22-
public const int HalfImplicitBitIndex = 10;
23-
2420
public const int SignificandSize = 64;
2521

2622
public readonly ulong f;
@@ -33,60 +29,21 @@ internal readonly ref struct DiyFp
3329
//
3430
// Precondition:
3531
// The value encoded by value must be greater than 0.
36-
public static DiyFp CreateAndGetBoundaries(double value, out DiyFp mMinus, out DiyFp mPlus)
32+
public static DiyFp CreateAndGetBoundaries<TNumber>(TNumber value, out DiyFp mMinus, out DiyFp mPlus)
33+
where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo<TNumber>
3734
{
38-
var result = new DiyFp(value);
39-
result.GetBoundaries(DoubleImplicitBitIndex, out mMinus, out mPlus);
35+
var result = Create(value);
36+
result.GetBoundaries(TNumber.DenormalMantissaBits, out mMinus, out mPlus);
4037
return result;
4138
}
4239

43-
// Computes the two boundaries of value.
44-
//
45-
// The bigger boundary (mPlus) is normalized.
46-
// The lower boundary has the same exponent as mPlus.
47-
//
48-
// Precondition:
49-
// The value encoded by value must be greater than 0.
50-
public static DiyFp CreateAndGetBoundaries(float value, out DiyFp mMinus, out DiyFp mPlus)
51-
{
52-
var result = new DiyFp(value);
53-
result.GetBoundaries(SingleImplicitBitIndex, out mMinus, out mPlus);
54-
return result;
55-
}
56-
57-
// Computes the two boundaries of value.
58-
//
59-
// The bigger boundary (mPlus) is normalized.
60-
// The lower boundary has the same exponent as mPlus.
61-
//
62-
// Precondition:
63-
// The value encoded by value must be greater than 0.
64-
public static DiyFp CreateAndGetBoundaries(Half value, out DiyFp mMinus, out DiyFp mPlus)
65-
{
66-
var result = new DiyFp(value);
67-
result.GetBoundaries(HalfImplicitBitIndex, out mMinus, out mPlus);
68-
return result;
69-
}
70-
71-
public DiyFp(double value)
72-
{
73-
Debug.Assert(double.IsFinite(value));
74-
Debug.Assert(value > 0.0);
75-
f = ExtractFractionAndBiasedExponent(value, out e);
76-
}
77-
78-
public DiyFp(float value)
79-
{
80-
Debug.Assert(float.IsFinite(value));
81-
Debug.Assert(value > 0.0f);
82-
f = ExtractFractionAndBiasedExponent(value, out e);
83-
}
84-
85-
public DiyFp(Half value)
40+
public static DiyFp Create<TNumber>(TNumber value)
41+
where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo<TNumber>
8642
{
87-
Debug.Assert(Half.IsFinite(value));
88-
Debug.Assert((float)value > 0.0f);
89-
f = ExtractFractionAndBiasedExponent(value, out e);
43+
Debug.Assert(TNumber.IsFinite(value));
44+
Debug.Assert(value > TNumber.Zero);
45+
ulong f = ExtractFractionAndBiasedExponent(value, out int e);
46+
return new DiyFp(f, e);
9047
}
9148

9249
public DiyFp(ulong f, int e)

src/libraries/System.Private.CoreLib/src/System/Number.Dragon4.cs

Lines changed: 8 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -10,82 +10,23 @@ namespace System
1010
// The backing algorithm and the proofs behind it are described in more detail here: https://www.cs.indiana.edu/~dyb/pubs/FP-Printing-PLDI96.pdf
1111
internal static partial class Number
1212
{
13-
public static void Dragon4Double(double value, int cutoffNumber, bool isSignificantDigits, ref NumberBuffer number)
13+
public static unsafe void Dragon4<TNumber>(TNumber value, int cutoffNumber, bool isSignificantDigits, ref NumberBuffer number)
14+
where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo<TNumber>
1415
{
15-
double v = double.IsNegative(value) ? -value : value;
16+
TNumber v = TNumber.IsNegative(value) ? -value : value;
1617

17-
Debug.Assert(v > 0);
18-
Debug.Assert(double.IsFinite(v));
18+
Debug.Assert(v > TNumber.Zero);
19+
Debug.Assert(TNumber.IsFinite(v));
1920

2021
ulong mantissa = ExtractFractionAndBiasedExponent(value, out int exponent);
2122

2223
uint mantissaHighBitIdx;
2324
bool hasUnequalMargins = false;
2425

25-
if ((mantissa >> DiyFp.DoubleImplicitBitIndex) != 0)
26+
if ((mantissa >> TNumber.DenormalMantissaBits) != 0)
2627
{
27-
mantissaHighBitIdx = DiyFp.DoubleImplicitBitIndex;
28-
hasUnequalMargins = (mantissa == (1UL << DiyFp.DoubleImplicitBitIndex));
29-
}
30-
else
31-
{
32-
Debug.Assert(mantissa != 0);
33-
mantissaHighBitIdx = (uint)BitOperations.Log2(mantissa);
34-
}
35-
36-
int length = (int)(Dragon4(mantissa, exponent, mantissaHighBitIdx, hasUnequalMargins, cutoffNumber, isSignificantDigits, number.Digits, out int decimalExponent));
37-
38-
number.Scale = decimalExponent + 1;
39-
number.Digits[length] = (byte)('\0');
40-
number.DigitsCount = length;
41-
}
42-
43-
public static unsafe void Dragon4Half(Half value, int cutoffNumber, bool isSignificantDigits, ref NumberBuffer number)
44-
{
45-
Half v = Half.IsNegative(value) ? Half.Negate(value) : value;
46-
47-
Debug.Assert((double)v > 0.0);
48-
Debug.Assert(Half.IsFinite(v));
49-
50-
ushort mantissa = ExtractFractionAndBiasedExponent(value, out int exponent);
51-
52-
uint mantissaHighBitIdx;
53-
bool hasUnequalMargins = false;
54-
55-
if ((mantissa >> DiyFp.HalfImplicitBitIndex) != 0)
56-
{
57-
mantissaHighBitIdx = DiyFp.HalfImplicitBitIndex;
58-
hasUnequalMargins = (mantissa == (1U << DiyFp.HalfImplicitBitIndex));
59-
}
60-
else
61-
{
62-
Debug.Assert(mantissa != 0);
63-
mantissaHighBitIdx = (uint)BitOperations.Log2(mantissa);
64-
}
65-
66-
int length = (int)(Dragon4(mantissa, exponent, mantissaHighBitIdx, hasUnequalMargins, cutoffNumber, isSignificantDigits, number.Digits, out int decimalExponent));
67-
68-
number.Scale = decimalExponent + 1;
69-
number.Digits[length] = (byte)('\0');
70-
number.DigitsCount = length;
71-
}
72-
73-
public static unsafe void Dragon4Single(float value, int cutoffNumber, bool isSignificantDigits, ref NumberBuffer number)
74-
{
75-
float v = float.IsNegative(value) ? -value : value;
76-
77-
Debug.Assert(v > 0);
78-
Debug.Assert(float.IsFinite(v));
79-
80-
uint mantissa = ExtractFractionAndBiasedExponent(value, out int exponent);
81-
82-
uint mantissaHighBitIdx;
83-
bool hasUnequalMargins = false;
84-
85-
if ((mantissa >> DiyFp.SingleImplicitBitIndex) != 0)
86-
{
87-
mantissaHighBitIdx = DiyFp.SingleImplicitBitIndex;
88-
hasUnequalMargins = (mantissa == (1U << DiyFp.SingleImplicitBitIndex));
28+
mantissaHighBitIdx = TNumber.DenormalMantissaBits;
29+
hasUnequalMargins = (mantissa == (1U << TNumber.DenormalMantissaBits));
8930
}
9031
else
9132
{

0 commit comments

Comments
 (0)