diff --git a/src/libraries/System.Private.CoreLib/src/System/Double.cs b/src/libraries/System.Private.CoreLib/src/System/Double.cs index 26e65460ef24f3..8d66af450f327a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Double.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Double.cs @@ -354,33 +354,33 @@ public override int GetHashCode() public override string ToString() { - return Number.FormatDouble(m_value, null, NumberFormatInfo.CurrentInfo); + return Number.FormatFloat(m_value, null, NumberFormatInfo.CurrentInfo); } public string ToString([StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format) { - return Number.FormatDouble(m_value, format, NumberFormatInfo.CurrentInfo); + return Number.FormatFloat(m_value, format, NumberFormatInfo.CurrentInfo); } public string ToString(IFormatProvider? provider) { - return Number.FormatDouble(m_value, null, NumberFormatInfo.GetInstance(provider)); + return Number.FormatFloat(m_value, null, NumberFormatInfo.GetInstance(provider)); } public string ToString([StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format, IFormatProvider? provider) { - return Number.FormatDouble(m_value, format, NumberFormatInfo.GetInstance(provider)); + return Number.FormatFloat(m_value, format, NumberFormatInfo.GetInstance(provider)); } public bool TryFormat(Span destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.NumericFormat)] ReadOnlySpan format = default, IFormatProvider? provider = null) { - return Number.TryFormatDouble(m_value, format, NumberFormatInfo.GetInstance(provider), destination, out charsWritten); + return Number.TryFormatFloat(m_value, format, NumberFormatInfo.GetInstance(provider), destination, out charsWritten); } /// public bool TryFormat(Span utf8Destination, out int bytesWritten, [StringSyntax(StringSyntaxAttribute.NumericFormat)] ReadOnlySpan format = default, IFormatProvider? provider = null) { - return Number.TryFormatDouble(m_value, format, NumberFormatInfo.GetInstance(provider), utf8Destination, out bytesWritten); + return Number.TryFormatFloat(m_value, format, NumberFormatInfo.GetInstance(provider), utf8Destination, out bytesWritten); } public static double Parse(string s) => Parse(s, NumberStyles.Float | NumberStyles.AllowThousands, provider: null); @@ -2335,6 +2335,10 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo static ulong IBinaryFloatParseAndFormatInfo.FloatToBits(double value) => BitConverter.DoubleToUInt64Bits(value); + static int IBinaryFloatParseAndFormatInfo.MaxRoundTripDigits => 17; + + static int IBinaryFloatParseAndFormatInfo.MaxPrecisionCustomFormat => 15; + // // Helpers // diff --git a/src/libraries/System.Private.CoreLib/src/System/Half.cs b/src/libraries/System.Private.CoreLib/src/System/Half.cs index 533521970c7e23..73cf14403d59e8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Half.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Half.cs @@ -505,7 +505,7 @@ public override int GetHashCode() /// public override string ToString() { - return Number.FormatHalf(this, null, NumberFormatInfo.CurrentInfo); + return Number.FormatFloat(this, null, NumberFormatInfo.CurrentInfo); } /// @@ -513,7 +513,7 @@ public override string ToString() /// public string ToString([StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format) { - return Number.FormatHalf(this, format, NumberFormatInfo.CurrentInfo); + return Number.FormatFloat(this, format, NumberFormatInfo.CurrentInfo); } /// @@ -521,7 +521,7 @@ public string ToString([StringSyntax(StringSyntaxAttribute.NumericFormat)] strin /// public string ToString(IFormatProvider? provider) { - return Number.FormatHalf(this, null, NumberFormatInfo.GetInstance(provider)); + return Number.FormatFloat(this, null, NumberFormatInfo.GetInstance(provider)); } /// @@ -529,7 +529,7 @@ public string ToString(IFormatProvider? provider) /// public string ToString([StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format, IFormatProvider? provider) { - return Number.FormatHalf(this, format, NumberFormatInfo.GetInstance(provider)); + return Number.FormatFloat(this, format, NumberFormatInfo.GetInstance(provider)); } /// @@ -542,13 +542,13 @@ public string ToString([StringSyntax(StringSyntaxAttribute.NumericFormat)] strin /// public bool TryFormat(Span destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.NumericFormat)] ReadOnlySpan format = default, IFormatProvider? provider = null) { - return Number.TryFormatHalf(this, format, NumberFormatInfo.GetInstance(provider), destination, out charsWritten); + return Number.TryFormatFloat(this, format, NumberFormatInfo.GetInstance(provider), destination, out charsWritten); } /// public bool TryFormat(Span utf8Destination, out int bytesWritten, [StringSyntax(StringSyntaxAttribute.NumericFormat)] ReadOnlySpan format = default, IFormatProvider? provider = null) { - return Number.TryFormatHalf(this, format, NumberFormatInfo.GetInstance(provider), utf8Destination, out bytesWritten); + return Number.TryFormatFloat(this, format, NumberFormatInfo.GetInstance(provider), utf8Destination, out bytesWritten); } // @@ -2373,5 +2373,9 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo static Half IBinaryFloatParseAndFormatInfo.BitsToFloat(ulong bits) => BitConverter.UInt16BitsToHalf((ushort)(bits)); static ulong IBinaryFloatParseAndFormatInfo.FloatToBits(Half value) => BitConverter.HalfToUInt16Bits(value); + + static int IBinaryFloatParseAndFormatInfo.MaxRoundTripDigits => 5; + + static int IBinaryFloatParseAndFormatInfo.MaxPrecisionCustomFormat => 5; } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Number.DiyFp.cs b/src/libraries/System.Private.CoreLib/src/System/Number.DiyFp.cs index 268bd53695e7da..49148306fff0f4 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.DiyFp.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.DiyFp.cs @@ -17,10 +17,6 @@ internal static partial class Number // DiyFp are not designed to contain special doubles (NaN and Infinity). internal readonly ref struct DiyFp { - public const int DoubleImplicitBitIndex = 52; - public const int SingleImplicitBitIndex = 23; - public const int HalfImplicitBitIndex = 10; - public const int SignificandSize = 64; public readonly ulong f; @@ -33,60 +29,21 @@ internal readonly ref struct DiyFp // // Precondition: // The value encoded by value must be greater than 0. - public static DiyFp CreateAndGetBoundaries(double value, out DiyFp mMinus, out DiyFp mPlus) + public static DiyFp CreateAndGetBoundaries(TNumber value, out DiyFp mMinus, out DiyFp mPlus) + where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo { - var result = new DiyFp(value); - result.GetBoundaries(DoubleImplicitBitIndex, out mMinus, out mPlus); + var result = Create(value); + result.GetBoundaries(TNumber.DenormalMantissaBits, out mMinus, out mPlus); return result; } - // Computes the two boundaries of value. - // - // The bigger boundary (mPlus) is normalized. - // The lower boundary has the same exponent as mPlus. - // - // Precondition: - // The value encoded by value must be greater than 0. - public static DiyFp CreateAndGetBoundaries(float value, out DiyFp mMinus, out DiyFp mPlus) - { - var result = new DiyFp(value); - result.GetBoundaries(SingleImplicitBitIndex, out mMinus, out mPlus); - return result; - } - - // Computes the two boundaries of value. - // - // The bigger boundary (mPlus) is normalized. - // The lower boundary has the same exponent as mPlus. - // - // Precondition: - // The value encoded by value must be greater than 0. - public static DiyFp CreateAndGetBoundaries(Half value, out DiyFp mMinus, out DiyFp mPlus) - { - var result = new DiyFp(value); - result.GetBoundaries(HalfImplicitBitIndex, out mMinus, out mPlus); - return result; - } - - public DiyFp(double value) - { - Debug.Assert(double.IsFinite(value)); - Debug.Assert(value > 0.0); - f = ExtractFractionAndBiasedExponent(value, out e); - } - - public DiyFp(float value) - { - Debug.Assert(float.IsFinite(value)); - Debug.Assert(value > 0.0f); - f = ExtractFractionAndBiasedExponent(value, out e); - } - - public DiyFp(Half value) + public static DiyFp Create(TNumber value) + where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo { - Debug.Assert(Half.IsFinite(value)); - Debug.Assert((float)value > 0.0f); - f = ExtractFractionAndBiasedExponent(value, out e); + Debug.Assert(TNumber.IsFinite(value)); + Debug.Assert(value > TNumber.Zero); + ulong f = ExtractFractionAndBiasedExponent(value, out int e); + return new DiyFp(f, e); } public DiyFp(ulong f, int e) diff --git a/src/libraries/System.Private.CoreLib/src/System/Number.Dragon4.cs b/src/libraries/System.Private.CoreLib/src/System/Number.Dragon4.cs index 10fbfcdbee5478..038fdb23947ddc 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.Dragon4.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.Dragon4.cs @@ -10,82 +10,23 @@ namespace System // 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 internal static partial class Number { - public static void Dragon4Double(double value, int cutoffNumber, bool isSignificantDigits, ref NumberBuffer number) + public static unsafe void Dragon4(TNumber value, int cutoffNumber, bool isSignificantDigits, ref NumberBuffer number) + where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo { - double v = double.IsNegative(value) ? -value : value; + TNumber v = TNumber.IsNegative(value) ? -value : value; - Debug.Assert(v > 0); - Debug.Assert(double.IsFinite(v)); + Debug.Assert(v > TNumber.Zero); + Debug.Assert(TNumber.IsFinite(v)); ulong mantissa = ExtractFractionAndBiasedExponent(value, out int exponent); uint mantissaHighBitIdx; bool hasUnequalMargins = false; - if ((mantissa >> DiyFp.DoubleImplicitBitIndex) != 0) + if ((mantissa >> TNumber.DenormalMantissaBits) != 0) { - mantissaHighBitIdx = DiyFp.DoubleImplicitBitIndex; - hasUnequalMargins = (mantissa == (1UL << DiyFp.DoubleImplicitBitIndex)); - } - else - { - Debug.Assert(mantissa != 0); - mantissaHighBitIdx = (uint)BitOperations.Log2(mantissa); - } - - int length = (int)(Dragon4(mantissa, exponent, mantissaHighBitIdx, hasUnequalMargins, cutoffNumber, isSignificantDigits, number.Digits, out int decimalExponent)); - - number.Scale = decimalExponent + 1; - number.Digits[length] = (byte)('\0'); - number.DigitsCount = length; - } - - public static unsafe void Dragon4Half(Half value, int cutoffNumber, bool isSignificantDigits, ref NumberBuffer number) - { - Half v = Half.IsNegative(value) ? Half.Negate(value) : value; - - Debug.Assert((double)v > 0.0); - Debug.Assert(Half.IsFinite(v)); - - ushort mantissa = ExtractFractionAndBiasedExponent(value, out int exponent); - - uint mantissaHighBitIdx; - bool hasUnequalMargins = false; - - if ((mantissa >> DiyFp.HalfImplicitBitIndex) != 0) - { - mantissaHighBitIdx = DiyFp.HalfImplicitBitIndex; - hasUnequalMargins = (mantissa == (1U << DiyFp.HalfImplicitBitIndex)); - } - else - { - Debug.Assert(mantissa != 0); - mantissaHighBitIdx = (uint)BitOperations.Log2(mantissa); - } - - int length = (int)(Dragon4(mantissa, exponent, mantissaHighBitIdx, hasUnequalMargins, cutoffNumber, isSignificantDigits, number.Digits, out int decimalExponent)); - - number.Scale = decimalExponent + 1; - number.Digits[length] = (byte)('\0'); - number.DigitsCount = length; - } - - public static unsafe void Dragon4Single(float value, int cutoffNumber, bool isSignificantDigits, ref NumberBuffer number) - { - float v = float.IsNegative(value) ? -value : value; - - Debug.Assert(v > 0); - Debug.Assert(float.IsFinite(v)); - - uint mantissa = ExtractFractionAndBiasedExponent(value, out int exponent); - - uint mantissaHighBitIdx; - bool hasUnequalMargins = false; - - if ((mantissa >> DiyFp.SingleImplicitBitIndex) != 0) - { - mantissaHighBitIdx = DiyFp.SingleImplicitBitIndex; - hasUnequalMargins = (mantissa == (1U << DiyFp.SingleImplicitBitIndex)); + mantissaHighBitIdx = TNumber.DenormalMantissaBits; + hasUnequalMargins = (mantissa == (1U << TNumber.DenormalMantissaBits)); } else { diff --git a/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs b/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs index 2a89bf96385294..928eaea0d9540a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs @@ -252,23 +252,6 @@ internal static partial class Number { internal const int DecimalPrecision = 29; // Decimal.DecCalc also uses this value - // SinglePrecision and DoublePrecision represent the maximum number of digits required - // to guarantee that any given Single or Double can roundtrip. Some numbers may require - // less, but none will require more. - private const int HalfPrecision = 5; - private const int SinglePrecision = 9; - private const int DoublePrecision = 17; - - // SinglePrecisionCustomFormat and DoublePrecisionCustomFormat are used to ensure that - // custom format strings return the same string as in previous releases when the format - // would return x digits or less (where x is the value of the corresponding constant). - // In order to support more digits, we would need to update ParseFormatSpecifier to pre-parse - // the format and determine exactly how many digits are being requested and whether they - // represent "significant digits" or "digits after the decimal point". - private const int HalfPrecisionCustomFormat = 5; - private const int SinglePrecisionCustomFormat = 7; - private const int DoublePrecisionCustomFormat = 15; - /// The non-inclusive upper bound of . /// /// This is a semi-arbitrary bound. For mono, which is often used for more size-constrained workloads, @@ -394,28 +377,6 @@ internal static unsafe void DecimalToNumber(scoped ref decimal d, ref NumberBuff number.CheckConsistency(); } - public static string FormatDouble(double value, string? format, NumberFormatInfo info) - { - var vlb = new ValueListBuilder(stackalloc char[CharStackBufferSize]); - string result = FormatDouble(ref vlb, value, format, info) ?? vlb.AsSpan().ToString(); - vlb.Dispose(); - return result; - } - - public static bool TryFormatDouble(double value, ReadOnlySpan format, NumberFormatInfo info, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar - { - var vlb = new ValueListBuilder(stackalloc TChar[CharStackBufferSize]); - string? s = FormatDouble(ref vlb, value, format, info); - - Debug.Assert(s is null || typeof(TChar) == typeof(char)); - bool success = s != null ? - TryCopyTo(s, destination, out charsWritten) : - vlb.TryCopyTo(destination, out charsWritten); - - vlb.Dispose(); - return success; - } - private static int GetFloatingPointMaxDigitsAndPrecision(char fmt, ref int precision, NumberFormatInfo info, out bool isSignificantDigits) { if (fmt == 0) @@ -537,105 +498,23 @@ private static int GetFloatingPointMaxDigitsAndPrecision(char fmt, ref int preci return maxDigits; } - /// Formats the specified value according to the specified format and info. - /// - /// Non-null if an existing string can be returned, in which case the builder will be unmodified. - /// Null if no existing string was returned, in which case the formatted output is in the builder. - /// - private static unsafe string? FormatDouble(ref ValueListBuilder vlb, double value, ReadOnlySpan format, NumberFormatInfo info) where TChar : unmanaged, IUtfChar - { - if (!double.IsFinite(value)) - { - if (double.IsNaN(value)) - { - if (typeof(TChar) == typeof(char)) - { - return info.NaNSymbol; - } - else - { - vlb.Append(info.NaNSymbolTChar()); - return null; - } - } - - if (typeof(TChar) == typeof(char)) - { - return double.IsNegative(value) ? info.NegativeInfinitySymbol : info.PositiveInfinitySymbol; - } - else - { - vlb.Append(double.IsNegative(value) ? info.NegativeInfinitySymbolTChar() : info.PositiveInfinitySymbolTChar()); - return null; - } - } - - char fmt = ParseFormatSpecifier(format, out int precision); - byte* pDigits = stackalloc byte[DoubleNumberBufferLength]; - - if (fmt == '\0') - { - // For back-compat we currently specially treat the precision for custom - // format specifiers. The constant has more details as to why. - precision = DoublePrecisionCustomFormat; - } - - NumberBuffer number = new NumberBuffer(NumberBufferKind.FloatingPoint, pDigits, DoubleNumberBufferLength); - number.IsNegative = double.IsNegative(value); - - // We need to track the original precision requested since some formats - // accept values like 0 and others may require additional fixups. - int nMaxDigits = GetFloatingPointMaxDigitsAndPrecision(fmt, ref precision, info, out bool isSignificantDigits); - - if ((value != 0.0) && (!isSignificantDigits || !Grisu3.TryRunDouble(value, precision, ref number))) - { - Dragon4Double(value, precision, isSignificantDigits, ref number); - } - - number.CheckConsistency(); - - // When the number is known to be roundtrippable (either because we requested it be, or - // because we know we have enough digits to satisfy roundtrippability), we should validate - // that the number actually roundtrips back to the original result. - - Debug.Assert(((precision != -1) && (precision < DoublePrecision)) || (BitConverter.DoubleToInt64Bits(value) == BitConverter.DoubleToInt64Bits(NumberToFloat(ref number)))); - - if (fmt != 0) - { - if (precision == -1) - { - Debug.Assert((fmt == 'G') || (fmt == 'g') || (fmt == 'R') || (fmt == 'r')); - - // For the roundtrip and general format specifiers, when returning the shortest roundtrippable - // string, we need to update the maximum number of digits to be the greater of number.DigitsCount - // or DoublePrecision. This ensures that we continue returning "pretty" strings for values with - // less digits. One example this fixes is "-60", which would otherwise be formatted as "-6E+01" - // since DigitsCount would be 1 and the formatter would almost immediately switch to scientific notation. - - nMaxDigits = Math.Max(number.DigitsCount, DoublePrecision); - } - NumberToString(ref vlb, ref number, fmt, nMaxDigits, info); - } - else - { - Debug.Assert(precision == DoublePrecisionCustomFormat); - NumberToStringFormat(ref vlb, ref number, format, info); - } - return null; - } - - public static string FormatSingle(float value, string? format, NumberFormatInfo info) + public static string FormatFloat(TNumber value, string? format, NumberFormatInfo info) + where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo { var vlb = new ValueListBuilder(stackalloc char[CharStackBufferSize]); - string result = FormatSingle(ref vlb, value, format, info) ?? vlb.AsSpan().ToString(); + string result = FormatFloat(ref vlb, value, format, info) ?? vlb.AsSpan().ToString(); vlb.Dispose(); return result; } - public static bool TryFormatSingle(float value, ReadOnlySpan format, NumberFormatInfo info, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + public static bool TryFormatFloat(TNumber value, ReadOnlySpan format, NumberFormatInfo info, Span destination, out int charsWritten) + where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo + where TChar : unmanaged, IUtfChar { + Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); + var vlb = new ValueListBuilder(stackalloc TChar[CharStackBufferSize]); - string? s = FormatSingle(ref vlb, value, format, info); + string? s = FormatFloat(ref vlb, value, format, info); Debug.Assert(s is null || typeof(TChar) == typeof(char)); bool success = s != null ? @@ -651,110 +530,15 @@ public static bool TryFormatSingle(float value, ReadOnlySpan format /// Non-null if an existing string can be returned, in which case the builder will be unmodified. /// Null if no existing string was returned, in which case the formatted output is in the builder. /// - private static unsafe string? FormatSingle(ref ValueListBuilder vlb, float value, ReadOnlySpan format, NumberFormatInfo info) where TChar : unmanaged, IUtfChar - { - Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); - - if (!float.IsFinite(value)) - { - if (float.IsNaN(value)) - { - if (typeof(TChar) == typeof(char)) - { - return info.NaNSymbol; - } - else - { - vlb.Append(info.NaNSymbolTChar()); - return null; - } - } - - if (typeof(TChar) == typeof(char)) - { - return float.IsNegative(value) ? info.NegativeInfinitySymbol : info.PositiveInfinitySymbol; - } - else - { - vlb.Append(float.IsNegative(value) ? info.NegativeInfinitySymbolTChar() : info.PositiveInfinitySymbolTChar()); - return null; - } - } - - char fmt = ParseFormatSpecifier(format, out int precision); - byte* pDigits = stackalloc byte[SingleNumberBufferLength]; - - if (fmt == '\0') - { - // For back-compat we currently specially treat the precision for custom - // format specifiers. The constant has more details as to why. - precision = SinglePrecisionCustomFormat; - } - - NumberBuffer number = new NumberBuffer(NumberBufferKind.FloatingPoint, pDigits, SingleNumberBufferLength); - number.IsNegative = float.IsNegative(value); - - // We need to track the original precision requested since some formats - // accept values like 0 and others may require additional fixups. - int nMaxDigits = GetFloatingPointMaxDigitsAndPrecision(fmt, ref precision, info, out bool isSignificantDigits); - - if ((value != default) && (!isSignificantDigits || !Grisu3.TryRunSingle(value, precision, ref number))) - { - Dragon4Single(value, precision, isSignificantDigits, ref number); - } - - number.CheckConsistency(); - - // When the number is known to be roundtrippable (either because we requested it be, or - // because we know we have enough digits to satisfy roundtrippability), we should validate - // that the number actually roundtrips back to the original result. - - Debug.Assert(((precision != -1) && (precision < SinglePrecision)) || (BitConverter.SingleToInt32Bits(value) == BitConverter.SingleToInt32Bits(NumberToFloat(ref number)))); - - if (fmt != 0) - { - if (precision == -1) - { - Debug.Assert((fmt == 'G') || (fmt == 'g') || (fmt == 'R') || (fmt == 'r')); - - // For the roundtrip and general format specifiers, when returning the shortest roundtrippable - // string, we need to update the maximum number of digits to be the greater of number.DigitsCount - // or SinglePrecision. This ensures that we continue returning "pretty" strings for values with - // less digits. One example this fixes is "-60", which would otherwise be formatted as "-6E+01" - // since DigitsCount would be 1 and the formatter would almost immediately switch to scientific notation. - - nMaxDigits = Math.Max(number.DigitsCount, SinglePrecision); - } - NumberToString(ref vlb, ref number, fmt, nMaxDigits, info); - } - else - { - Debug.Assert(precision == SinglePrecisionCustomFormat); - NumberToStringFormat(ref vlb, ref number, format, info); - } - return null; - } - - public static string FormatHalf(Half value, string? format, NumberFormatInfo info) - { - var vlb = new ValueListBuilder(stackalloc char[CharStackBufferSize]); - string result = FormatHalf(ref vlb, value, format, info) ?? vlb.AsSpan().ToString(); - vlb.Dispose(); - return result; - } - - /// Formats the specified value according to the specified format and info. - /// - /// Non-null if an existing string can be returned, in which case the builder will be unmodified. - /// Null if no existing string was returned, in which case the formatted output is in the builder. - /// - private static unsafe string? FormatHalf(ref ValueListBuilder vlb, Half value, ReadOnlySpan format, NumberFormatInfo info) where TChar : unmanaged, IUtfChar + private static unsafe string? FormatFloat(ref ValueListBuilder vlb, TNumber value, ReadOnlySpan format, NumberFormatInfo info) + where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo + where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); - if (!Half.IsFinite(value)) + if (!TNumber.IsFinite(value)) { - if (Half.IsNaN(value)) + if (TNumber.IsNaN(value)) { if (typeof(TChar) == typeof(char)) { @@ -769,33 +553,33 @@ public static string FormatHalf(Half value, string? format, NumberFormatInfo inf if (typeof(TChar) == typeof(char)) { - return Half.IsNegative(value) ? info.NegativeInfinitySymbol : info.PositiveInfinitySymbol; + return TNumber.IsNegative(value) ? info.NegativeInfinitySymbol : info.PositiveInfinitySymbol; } else { - vlb.Append(Half.IsNegative(value) ? info.NegativeInfinitySymbolTChar() : info.PositiveInfinitySymbolTChar()); + vlb.Append(TNumber.IsNegative(value) ? info.NegativeInfinitySymbolTChar() : info.PositiveInfinitySymbolTChar()); return null; } } char fmt = ParseFormatSpecifier(format, out int precision); - byte* pDigits = stackalloc byte[HalfNumberBufferLength]; + byte* pDigits = stackalloc byte[TNumber.NumberBufferLength]; if (fmt == '\0') { - precision = HalfPrecisionCustomFormat; + precision = TNumber.MaxPrecisionCustomFormat; } - NumberBuffer number = new NumberBuffer(NumberBufferKind.FloatingPoint, pDigits, HalfNumberBufferLength); - number.IsNegative = Half.IsNegative(value); + NumberBuffer number = new NumberBuffer(NumberBufferKind.FloatingPoint, pDigits, TNumber.NumberBufferLength); + number.IsNegative = TNumber.IsNegative(value); // We need to track the original precision requested since some formats // accept values like 0 and others may require additional fixups. int nMaxDigits = GetFloatingPointMaxDigitsAndPrecision(fmt, ref precision, info, out bool isSignificantDigits); - if ((value != default) && (!isSignificantDigits || !Grisu3.TryRunHalf(value, precision, ref number))) + if ((value != default) && (!isSignificantDigits || !Grisu3.TryRun(value, precision, ref number))) { - Dragon4Half(value, precision, isSignificantDigits, ref number); + Dragon4(value, precision, isSignificantDigits, ref number); } number.CheckConsistency(); @@ -804,7 +588,7 @@ public static string FormatHalf(Half value, string? format, NumberFormatInfo inf // because we know we have enough digits to satisfy roundtrippability), we should validate // that the number actually roundtrips back to the original result. - Debug.Assert(((precision != -1) && (precision < HalfPrecision)) || (BitConverter.HalfToInt16Bits(value) == BitConverter.HalfToInt16Bits(NumberToFloat(ref number)))); + Debug.Assert(((precision != -1) && (precision < TNumber.MaxRoundTripDigits)) || (TNumber.FloatToBits(value) == TNumber.FloatToBits(NumberToFloat(ref number)))); if (fmt != 0) { @@ -818,34 +602,18 @@ public static string FormatHalf(Half value, string? format, NumberFormatInfo inf // less digits. One example this fixes is "-60", which would otherwise be formatted as "-6E+01" // since DigitsCount would be 1 and the formatter would almost immediately switch to scientific notation. - nMaxDigits = Math.Max(number.DigitsCount, HalfPrecision); + nMaxDigits = Math.Max(number.DigitsCount, TNumber.MaxRoundTripDigits); } NumberToString(ref vlb, ref number, fmt, nMaxDigits, info); } else { - Debug.Assert(precision == HalfPrecisionCustomFormat); + Debug.Assert(precision == TNumber.MaxPrecisionCustomFormat); NumberToStringFormat(ref vlb, ref number, format, info); } return null; } - public static bool TryFormatHalf(Half value, ReadOnlySpan format, NumberFormatInfo info, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar - { - Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); - - var vlb = new ValueListBuilder(stackalloc TChar[CharStackBufferSize]); - string? s = FormatHalf(ref vlb, value, format, info); - - Debug.Assert(s is null || typeof(TChar) == typeof(char)); - bool success = s != null ? - TryCopyTo(s, destination, out charsWritten) : - vlb.TryCopyTo(destination, out charsWritten); - - vlb.Dispose(); - return success; - } - private static bool TryCopyTo(string source, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); @@ -2867,5 +2635,38 @@ private static uint ExtractFractionAndBiasedExponent(float value, out int expone return fraction; } + + private static ulong ExtractFractionAndBiasedExponent(TNumber value, out int exponent) + where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo + { + ulong bits = TNumber.FloatToBits(value); + ulong fraction = (bits & TNumber.DenormalMantissaMask); + exponent = ((int)(bits >> TNumber.DenormalMantissaBits) & TNumber.InfinityExponent); + + if (exponent != 0) + { + // For normalized value, + // value = 1.fraction * 2^(exp - ExponentBias) + // = (1 + mantissa / 2^TrailingSignificandLength) * 2^(exp - ExponentBias) + // = (2^TrailingSignificandLength + mantissa) * 2^(exp - ExponentBias - TrailingSignificandLength) + // + // So f = (2^TrailingSignificandLength + mantissa), e = exp - ExponentBias - TrailingSignificandLength; + + fraction |= (1UL << TNumber.DenormalMantissaBits); + exponent -= TNumber.ExponentBias + TNumber.DenormalMantissaBits; + } + else + { + // For denormalized value, + // value = 0.fraction * 2^(MinBinaryExponent) + // = (mantissa / 2^TrailingSignificandLength) * 2^(MinBinaryExponent) + // = mantissa * 2^(MinBinaryExponent - TrailingSignificandLength) + // = mantissa * 2^(MinBinaryExponent - TrailingSignificandLength) + // So f = mantissa, e = MinBinaryExponent - TrailingSignificandLength + exponent = TNumber.MinBinaryExponent - TNumber.DenormalMantissaBits; + } + + return fraction; + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Number.Grisu3.cs b/src/libraries/System.Private.CoreLib/src/System/Number.Grisu3.cs index 4a746b38cc2c31..e4a25b9b5939c6 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.Grisu3.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.Grisu3.cs @@ -321,12 +321,13 @@ internal static class Grisu3 1000000000, // 10^9 ]; - public static bool TryRunDouble(double value, int requestedDigits, ref NumberBuffer number) + public static bool TryRun(TNumber value, int requestedDigits, ref NumberBuffer number) + where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo { - double v = double.IsNegative(value) ? -value : value; + TNumber v = TNumber.IsNegative(value) ? -value : value; - Debug.Assert(v > 0); - Debug.Assert(double.IsFinite(v)); + Debug.Assert(v > TNumber.Zero); + Debug.Assert(TNumber.IsFinite(v)); int length; int decimalExponent; @@ -339,75 +340,7 @@ public static bool TryRunDouble(double value, int requestedDigits, ref NumberBuf } else { - DiyFp w = new DiyFp(v).Normalize(); - result = TryRunCounted(in w, requestedDigits, number.Digits, out length, out decimalExponent); - } - - if (result) - { - Debug.Assert((requestedDigits == -1) || (length == requestedDigits)); - - number.Scale = length + decimalExponent; - number.Digits[length] = (byte)('\0'); - number.DigitsCount = length; - } - - return result; - } - - public static bool TryRunHalf(Half value, int requestedDigits, ref NumberBuffer number) - { - Half v = Half.IsNegative(value) ? Half.Negate(value) : value; - - Debug.Assert((double)v > 0); - Debug.Assert(Half.IsFinite(v)); - - int length; - int decimalExponent; - bool result; - - if (requestedDigits == -1) - { - DiyFp w = DiyFp.CreateAndGetBoundaries(v, out DiyFp boundaryMinus, out DiyFp boundaryPlus).Normalize(); - result = TryRunShortest(in boundaryMinus, in w, in boundaryPlus, number.Digits, out length, out decimalExponent); - } - else - { - DiyFp w = new DiyFp(v).Normalize(); - result = TryRunCounted(in w, requestedDigits, number.Digits, out length, out decimalExponent); - } - - if (result) - { - Debug.Assert((requestedDigits == -1) || (length == requestedDigits)); - - number.Scale = length + decimalExponent; - number.Digits[length] = (byte)('\0'); - number.DigitsCount = length; - } - - return result; - } - - public static bool TryRunSingle(float value, int requestedDigits, ref NumberBuffer number) - { - float v = float.IsNegative(value) ? -value : value; - - Debug.Assert(v > 0); - Debug.Assert(float.IsFinite(v)); - - int length; - int decimalExponent; - bool result; - - if (requestedDigits == -1) - { - DiyFp w = DiyFp.CreateAndGetBoundaries(v, out DiyFp boundaryMinus, out DiyFp boundaryPlus).Normalize(); - result = TryRunShortest(in boundaryMinus, in w, in boundaryPlus, number.Digits, out length, out decimalExponent); - } - else - { - DiyFp w = new DiyFp(v).Normalize(); + DiyFp w = DiyFp.Create(v).Normalize(); result = TryRunCounted(in w, requestedDigits, number.Digits, out length, out decimalExponent); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs b/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs index 852b979492c2d5..263f60ad0ba10f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs @@ -85,6 +85,18 @@ internal interface IBinaryFloatParseAndFormatInfo : IBinaryFloatingPointI static abstract TSelf BitsToFloat(ulong bits); static abstract ulong FloatToBits(TSelf value); + + // Maximum number of digits required to guarantee that any given floating point + // number can roundtrip. Some numbers may require less, but none will require more. + static abstract int MaxRoundTripDigits { get; } + + // SinglePrecisionCustomFormat and DoublePrecisionCustomFormat are used to ensure that + // custom format strings return the same string as in previous releases when the format + // would return x digits or less (where x is the value of the corresponding constant). + // In order to support more digits, we would need to update ParseFormatSpecifier to pre-parse + // the format and determine exactly how many digits are being requested and whether they + // represent "significant digits" or "digits after the decimal point". + static abstract int MaxPrecisionCustomFormat { get; } } internal static partial class Number diff --git a/src/libraries/System.Private.CoreLib/src/System/Single.cs b/src/libraries/System.Private.CoreLib/src/System/Single.cs index 88c923892309c1..7ee394a0d8f4c3 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Single.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Single.cs @@ -349,33 +349,33 @@ public override int GetHashCode() public override string ToString() { - return Number.FormatSingle(m_value, null, NumberFormatInfo.CurrentInfo); + return Number.FormatFloat(m_value, null, NumberFormatInfo.CurrentInfo); } public string ToString(IFormatProvider? provider) { - return Number.FormatSingle(m_value, null, NumberFormatInfo.GetInstance(provider)); + return Number.FormatFloat(m_value, null, NumberFormatInfo.GetInstance(provider)); } public string ToString([StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format) { - return Number.FormatSingle(m_value, format, NumberFormatInfo.CurrentInfo); + return Number.FormatFloat(m_value, format, NumberFormatInfo.CurrentInfo); } public string ToString([StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format, IFormatProvider? provider) { - return Number.FormatSingle(m_value, format, NumberFormatInfo.GetInstance(provider)); + return Number.FormatFloat(m_value, format, NumberFormatInfo.GetInstance(provider)); } public bool TryFormat(Span destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.NumericFormat)] ReadOnlySpan format = default, IFormatProvider? provider = null) { - return Number.TryFormatSingle(m_value, format, NumberFormatInfo.GetInstance(provider), destination, out charsWritten); + return Number.TryFormatFloat(m_value, format, NumberFormatInfo.GetInstance(provider), destination, out charsWritten); } /// public bool TryFormat(Span utf8Destination, out int bytesWritten, [StringSyntax(StringSyntaxAttribute.NumericFormat)] ReadOnlySpan format = default, IFormatProvider? provider = null) { - return Number.TryFormatSingle(m_value, format, NumberFormatInfo.GetInstance(provider), utf8Destination, out bytesWritten); + return Number.TryFormatFloat(m_value, format, NumberFormatInfo.GetInstance(provider), utf8Destination, out bytesWritten); } // Parses a float from a String in the given style. If @@ -2217,6 +2217,10 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo static ulong IBinaryFloatParseAndFormatInfo.FloatToBits(float value) => BitConverter.SingleToUInt32Bits(value); + static int IBinaryFloatParseAndFormatInfo.MaxRoundTripDigits => 9; + + static int IBinaryFloatParseAndFormatInfo.MaxPrecisionCustomFormat => 7; + // // Helpers //