From 9114779d7f28d2de806918a94ae30ea5984fce44 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Sat, 7 Feb 2026 17:51:59 -0500 Subject: [PATCH 01/11] Add support for floating-point hex formatting and parsing --- .../tests/System/RealFormatterTestsBase.cs | 4 +- .../src/Resources/Strings.resx | 6 + .../src/System/Double.cs | 4 +- .../System/Globalization/NumberFormatInfo.cs | 15 +- .../src/System/Globalization/NumberStyles.cs | 8 + .../System.Private.CoreLib/src/System/Half.cs | 4 +- .../src/System/Number.Formatting.cs | 201 +++++++++++ .../src/System/Number.Parsing.cs | 341 ++++++++++++++++++ .../src/System/Numerics/BFloat16.cs | 4 +- .../src/System/Single.cs | 4 +- .../System.Runtime/ref/System.Runtime.cs | 1 + .../NumberFormatInfoValidateParseStyle.cs | 7 +- .../System/DoubleTests.cs | 299 +++++++++++++++ .../System.Runtime.Tests/System/HalfTests.cs | 143 ++++++++ .../System/Numerics/BFloat16Tests.cs | 105 ++++++ .../System/SingleTests.cs | 177 +++++++++ 16 files changed, 1307 insertions(+), 16 deletions(-) diff --git a/src/libraries/Common/tests/System/RealFormatterTestsBase.cs b/src/libraries/Common/tests/System/RealFormatterTestsBase.cs index 015188e81f7909..ab76dec54ebe24 100644 --- a/src/libraries/Common/tests/System/RealFormatterTestsBase.cs +++ b/src/libraries/Common/tests/System/RealFormatterTestsBase.cs @@ -824,7 +824,7 @@ public abstract class RealFormatterTestsBase public static IEnumerable TestFormatterDouble_InvalidMemberData => from value in new[] { double.Epsilon, double.MaxValue, Math.E, Math.PI, 0.0, 0.84551240822557006, 1.0, 1844674407370955.25 } - from format in new[] { "D", "D4", "D20", "X", "X4", "X20" } + from format in new[] { "D", "D4", "D20" } select new object[] { value, format }; [Theory] @@ -1644,7 +1644,7 @@ protected void TestFormatterDouble_Standard(double value, string format, string public static IEnumerable TestFormatterSingle_InvalidMemberData => from value in new[] { float.Epsilon, float.MaxValue, MathF.E, MathF.PI, 0.0, 0.845512390f, 1.0, 429496.72 } - from format in new[] { "D", "D4", "D20", "X", "X4", "X20" } + from format in new[] { "D", "D4", "D20" } select new object[] { value, format }; [Theory] diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index 5249116e7a8976..2a5ce88d6022dc 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -406,6 +406,12 @@ The number styles AllowHexSpecifier and AllowBinarySpecifier are not supported on floating point data types. + + The number style AllowBinarySpecifier is not supported on floating point data types. + + + With the AllowHexSpecifier bit set in the enum bit field, the only other valid bits that can be combined into the enum value must be AllowLeadingWhite, AllowTrailingWhite, AllowLeadingSign, and AllowDecimalPoint. + Hashtable's capacity overflowed and went negative. Check load factor, capacity and the current size of the table. diff --git a/src/libraries/System.Private.CoreLib/src/System/Double.cs b/src/libraries/System.Private.CoreLib/src/System/Double.cs index 516f61d4e5b0f2..94d7a627d3f70b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Double.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Double.cs @@ -2264,14 +2264,14 @@ public static double TanPi(double x) /// public static double Parse(ReadOnlySpan utf8Text, NumberStyles style = NumberStyles.Float | NumberStyles.AllowThousands, IFormatProvider? provider = null) { - NumberFormatInfo.ValidateParseStyleInteger(style); + NumberFormatInfo.ValidateParseStyleFloatingPoint(style); return Number.ParseFloat(utf8Text, style, NumberFormatInfo.GetInstance(provider)); } /// public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFormatProvider? provider, out double result) { - NumberFormatInfo.ValidateParseStyleInteger(style); + NumberFormatInfo.ValidateParseStyleFloatingPoint(style); return Number.TryParseFloat(utf8Text, style, NumberFormatInfo.GetInstance(provider), out result); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberFormatInfo.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberFormatInfo.cs index 7e41139b4bf6c5..753daa1e88ba14 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberFormatInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberFormatInfo.cs @@ -827,13 +827,20 @@ static void ThrowInvalid(NumberStyles value) internal static void ValidateParseStyleFloatingPoint(NumberStyles style) { - // Check for undefined flags or hex number - if ((style & (InvalidNumberStyles | NumberStyles.AllowHexSpecifier | NumberStyles.AllowBinarySpecifier)) != 0) + // Check for undefined flags, AllowBinarySpecifier (never valid for float), or AllowHexSpecifier with anything other than HexFloat flags. + if ((style & (InvalidNumberStyles | NumberStyles.AllowBinarySpecifier | NumberStyles.AllowHexSpecifier)) != 0 && + (style & ~NumberStyles.HexFloat) != 0) { ThrowInvalid(style); - static void ThrowInvalid(NumberStyles value) => - throw new ArgumentException((value & InvalidNumberStyles) != 0 ? SR.Argument_InvalidNumberStyles : SR.Arg_HexBinaryStylesNotSupported, nameof(style)); + static void ThrowInvalid(NumberStyles value) + { + throw new ArgumentException( + (value & InvalidNumberStyles) != 0 ? SR.Argument_InvalidNumberStyles : + (value & NumberStyles.AllowBinarySpecifier) != 0 ? SR.Arg_BinaryStyleNotSupported : + SR.Arg_InvalidHexFloatStyle, + nameof(style)); + } } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberStyles.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberStyles.cs index 84a8afdf39a3a2..a7bc8000b23545 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberStyles.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberStyles.cs @@ -73,6 +73,14 @@ public enum NumberStyles Float = AllowLeadingWhite | AllowTrailingWhite | AllowLeadingSign | AllowDecimalPoint | AllowExponent, + /// + /// Indicates that the , , + /// , , and + /// styles are used. This is a composite number style used for parsing hexadecimal floating-point values + /// as defined in IEEE 754:2008 §5.12.3. + /// + HexFloat = AllowLeadingWhite | AllowTrailingWhite | AllowLeadingSign | AllowHexSpecifier | AllowDecimalPoint, + Currency = AllowLeadingWhite | AllowTrailingWhite | AllowLeadingSign | AllowTrailingSign | AllowParentheses | AllowDecimalPoint | AllowThousands | AllowCurrencySymbol, diff --git a/src/libraries/System.Private.CoreLib/src/System/Half.cs b/src/libraries/System.Private.CoreLib/src/System/Half.cs index fc6dd2bd0c423a..de4b6b925a544d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Half.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Half.cs @@ -2311,14 +2311,14 @@ public static (Half SinPi, Half CosPi) SinCosPi(Half x) /// public static Half Parse(ReadOnlySpan utf8Text, NumberStyles style = NumberStyles.Float | NumberStyles.AllowThousands, IFormatProvider? provider = null) { - NumberFormatInfo.ValidateParseStyleInteger(style); + NumberFormatInfo.ValidateParseStyleFloatingPoint(style); return Number.ParseFloat(utf8Text, style, NumberFormatInfo.GetInstance(provider)); } /// public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFormatProvider? provider, out Half result) { - NumberFormatInfo.ValidateParseStyleInteger(style); + NumberFormatInfo.ValidateParseStyleFloatingPoint(style); return Number.TryParseFloat(utf8Text, style, NumberFormatInfo.GetInstance(provider), out result); } 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 9b2028fa0f48ff..dd8c1130109410 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs @@ -540,6 +540,199 @@ static int Slow(char fmt, ref int precision, NumberFormatInfo info, out bool isS } } + private static unsafe void FormatFloatingPointAsHex(ref ValueListBuilder vlb, TNumber value, char fmt, int precision, NumberFormatInfo info) + where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo + where TChar : unmanaged, IUtfChar + { + Debug.Assert((fmt | 0x20) == 'x'); + + bool isNegative = TNumber.IsNegative(value); + + if (isNegative) + { + vlb.Append(info.NegativeSignTChar()); + } + + ulong fraction = ExtractFractionAndBiasedExponent(value, out int exponent); + + if (fraction == 0) + { + // +/- 0 + vlb.Append(TChar.CastFrom('0')); + + if (precision > 0) + { + vlb.Append(info.NumberDecimalSeparatorTChar()); + for (int i = 0; i < precision; i++) + { + vlb.Append(TChar.CastFrom('0')); + } + } + + vlb.Append(TChar.CastFrom(fmt == 'X' ? 'P' : 'p')); + vlb.Append(TChar.CastFrom('+')); + vlb.Append(TChar.CastFrom('0')); + + return; + } + + // ExtractFractionAndBiasedExponent returns: + // For normal: fraction = (1 << DenormalMantissaBits) | mantissa, exponent = biasedExp - ExponentBias - DenormalMantissaBits + // For denormal: fraction = mantissa, exponent = MinBinaryExponent - DenormalMantissaBits + // + // We want the form: 1.xxxxx * 2^e + // So we need to normalize so that the leading 1 bit is at bit DenormalMantissaBits. + // For normal numbers, this is already the case. + // For denormal numbers, we need to shift left until the leading 1 is there. + + int mantissaBits = TNumber.DenormalMantissaBits; + + if (fraction < (1UL << mantissaBits)) + { + // Denormal: shift the leading 1 up to the implicit bit position + int lz = BitOperations.LeadingZeroCount(fraction) - (63 - mantissaBits); + fraction <<= lz; + exponent -= lz; + } + + // Now fraction has the leading 1 at bit [mantissaBits], and the remaining bits below. + // The unbiased exponent for the value is: exponent + mantissaBits (since fraction is + // really fraction * 2^exponent, and we want 1.xxx * 2^actualExponent). + int actualExponent = exponent + mantissaBits; + + // Strip the implicit leading 1 to get the fractional bits + ulong significandBits = fraction & ((1UL << mantissaBits) - 1); + + // Leading digit is normally '1' for non-zero (the implicit bit) + int leadingDigit = 1; + + // Determine how many hex digits to emit for the fractional part + int defaultHexDigits = (mantissaBits + 3) / 4; + + if (precision == 0) + { + // Round significandBits into the leading digit + ulong half = (mantissaBits > 0) ? (1UL << (mantissaBits - 1)) : 0; + if (significandBits > half || (significandBits == half && (leadingDigit & 1) != 0)) + { + leadingDigit++; + // leadingDigit can't exceed 2 since it started at 1 + } + + significandBits = 0; + } + + vlb.Append(TChar.CastFrom((char)('0' + leadingDigit))); + + if (precision > 0) + { + ulong shifted; + + if (precision < defaultHexDigits) + { + // Need to round + int bitsToKeep = precision * 4; + int bitsToDiscard = mantissaBits - bitsToKeep; + + if (bitsToDiscard > 0 && bitsToDiscard < 64) + { + ulong roundBit = 1UL << (bitsToDiscard - 1); + ulong discardedBits = significandBits & ((1UL << bitsToDiscard) - 1); + bool roundUp = discardedBits > roundBit || (discardedBits == roundBit && ((significandBits >> bitsToDiscard) & 1) != 0); + + if (roundUp) + { + significandBits = (significandBits >> bitsToDiscard) + 1; + + // Check if rounding overflowed into leading digit + if (significandBits >= (1UL << bitsToKeep)) + { + significandBits = 0; + actualExponent++; + } + } + else + { + significandBits >>= bitsToDiscard; + } + + shifted = significandBits << (64 - bitsToKeep); + } + else + { + shifted = significandBits << (64 - mantissaBits); + } + } + else + { + shifted = significandBits << (64 - mantissaBits); + } + + vlb.Append(info.NumberDecimalSeparatorTChar()); + + // Emit real nibbles + int realDigits = Math.Min(precision, defaultHexDigits); + for (int i = 0; i < realDigits; i++) + { + vlb.Append(TChar.CastFrom(fmt == 'X' ? HexConverter.ToCharUpper((int)(shifted >> 60)) : HexConverter.ToCharLower((int)(shifted >> 60)))); + shifted <<= 4; + } + + // Emit padding zeros (when precision > defaultHexDigits) + for (int i = realDigits; i < precision; i++) + { + vlb.Append(TChar.CastFrom('0')); + } + } + else if (precision < 0) + { + // Default precision: emit significant hex digits, trimming trailing zeros. + // Compute trailing zero nibbles from the nibble-aligned representation. + int trimmedDigits = 0; + if (significandBits != 0) + { + // Align significand to nibble boundary (pad LSB so total bits = defaultHexDigits * 4), + // then count trailing zero nibbles via trailing zero bits. + int paddingBits = defaultHexDigits * 4 - mantissaBits; + ulong nibbleAligned = significandBits << paddingBits; + int trailingZeroBits = BitOperations.TrailingZeroCount(nibbleAligned); + trimmedDigits = defaultHexDigits - (trailingZeroBits / 4); + + if (trimmedDigits > 0) + { + vlb.Append(info.NumberDecimalSeparatorTChar()); + + ulong shifted = significandBits << (64 - mantissaBits); + for (int i = 0; i < trimmedDigits; i++) + { + vlb.Append(TChar.CastFrom(fmt == 'X' ? HexConverter.ToCharUpper((int)(shifted >> 60)) : HexConverter.ToCharLower((int)(shifted >> 60)))); + shifted <<= 4; + } + } + } + } + + // Emit exponent: p+NNN or p-NNN + vlb.Append(TChar.CastFrom(fmt == 'X' ? 'P' : 'p')); + + if (actualExponent >= 0) + { + vlb.Append(TChar.CastFrom('+')); + } + else + { + vlb.Append(TChar.CastFrom('-')); + actualExponent = -actualExponent; + } + + // Write exponent digits + Debug.Assert(actualExponent >= 0); + int digitCount = FormattingHelpers.CountDigits((uint)actualExponent); + TChar* pExponent = stackalloc TChar[digitCount]; + UInt32ToDecChars(pExponent + digitCount, (uint)actualExponent); + vlb.Append(new ReadOnlySpan(pExponent, digitCount)); + } + public static string FormatFloat(TNumber value, string? format, NumberFormatInfo info) where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo { @@ -605,6 +798,14 @@ public static bool TryFormatFloat(TNumber value, ReadOnlySpan(ReadOnlySpan span } } + private static bool TryParseHexFloatingPoint(ReadOnlySpan value, NumberStyles styles, NumberFormatInfo info, out TFloat result) + where TChar : unmanaged, IUtfChar + where TFloat : unmanaged, IBinaryFloatParseAndFormatInfo + { + result = TFloat.Zero; + + if (value.IsEmpty) + { + return false; + } + + int index = 0; + + // Skip leading whitespace + if ((styles & NumberStyles.AllowLeadingWhite) != 0) + { + while (index < value.Length && IsWhite(TChar.CastToUInt32(value[index]))) + { + index++; + } + } + + if (index >= value.Length) + { + return false; + } + + // Parse optional sign + bool isNegative = false; + if ((styles & NumberStyles.AllowLeadingSign) != 0) + { + ReadOnlySpan negativeSign = info.NegativeSignTChar(); + if (value.Slice(index).StartsWith(negativeSign)) + { + isNegative = true; + index += negativeSign.Length; + } + else + { + ReadOnlySpan positiveSign = info.PositiveSignTChar(); + if (!positiveSign.IsEmpty && value.Slice(index).StartsWith(positiveSign)) + { + index += positiveSign.Length; + } + } + } + + if (index >= value.Length) + { + return false; + } + + // Skip optional "0x" or "0X" prefix (consistent with integer hex parsing) + if (TChar.CastToUInt32(value[index]) == '0' && + index + 1 < value.Length && + (TChar.CastToUInt32(value[index + 1]) | 0x20) == 'x') + { + index += 2; + } + + if (index >= value.Length) + { + return false; + } + + // Parse hex significand. + // We accumulate up to 16 significant hex digits into a ulong. + // We track the exponent adjustment due to digit position. + // + // The value is: significand * 2^(binaryExponent - 4 * fractionalDigitsConsumed + 4 * overflowIntegerDigits) + + ulong significand = 0; + int significandDigits = 0; // Count of significant (non-leading-zero) digits consumed into significand + int overflowIntegerDigits = 0; // Integer digits that didn't fit + bool hasDiscardedNonZeroDigits = false; // IEEE 754 "sticky bit": any nonzero digit discarded beyond significand capacity + + int integerPartStart = index; + while (index < value.Length) + { + uint ch = TChar.CastToUInt32(value[index]); + int digit = HexConverter.FromChar((int)ch); + if (digit >= 16) + { + break; + } + + if (significandDigits < 16 || significand == 0) + { + if (significand != 0 || digit != 0) + { + significand = (significand << 4) | (uint)digit; + significandDigits++; + } + } + else + { + overflowIntegerDigits++; + hasDiscardedNonZeroDigits |= digit != 0; + } + + index++; + } + bool hasIntegerPart = index > integerPartStart; + + // Parse fractional part + int fractionalDigitsConsumed = 0; + bool hasFractionalPart = false; + + if ((styles & NumberStyles.AllowDecimalPoint) != 0 && index < value.Length) + { + ReadOnlySpan decimalSeparator = info.NumberDecimalSeparatorTChar(); + if (value.Slice(index).StartsWith(decimalSeparator)) + { + index += decimalSeparator.Length; + + int fractionalPartStart = index; + while (index < value.Length) + { + uint ch = TChar.CastToUInt32(value[index]); + int digit = HexConverter.FromChar((int)ch); + if (digit >= 16) + { + break; + } + + if (significandDigits < 16 || significand == 0) + { + if (significand != 0 || digit != 0) + { + significand = (significand << 4) | (uint)digit; + significandDigits++; + } + + // Always increment, even for leading zeros: positional value matters + // (e.g., 0x0.004p0 = 4 * 2^-12, so all three fractional digits count). + fractionalDigitsConsumed++; + } + else + { + hasDiscardedNonZeroDigits |= digit != 0; + } + + index++; + } + hasFractionalPart = index > fractionalPartStart; + } + } + + if (!hasIntegerPart && !hasFractionalPart) + { + return false; + } + + // Parse binary exponent (p or P) + int binaryExponent = 0; + if (index < value.Length && ((TChar.CastToUInt32(value[index]) | 0x20) == 'p')) + { + index++; + + if (index >= value.Length) + { + return false; + } + + bool exponentIsNegative = false; + uint ch = TChar.CastToUInt32(value[index]); + if (ch == '-') + { + exponentIsNegative = true; + index++; + } + else if (ch == '+') + { + index++; + } + + if (index >= value.Length) + { + return false; + } + + int exponentStart = index; + while (index < value.Length) + { + uint ech = TChar.CastToUInt32(value[index]); + if (!IsDigit(ech)) + { + break; + } + + int digit = (int)(ech - '0'); + + binaryExponent = binaryExponent <= (int.MaxValue - digit) / 10 ? + binaryExponent * 10 + digit : + int.MaxValue; + + index++; + } + + if (index == exponentStart) + { + return false; + } + + if (exponentIsNegative) + { + binaryExponent = -binaryExponent; + } + } + else if (hasFractionalPart) + { + // Exponent is required when there's a fractional part + return false; + } + + // Skip trailing whitespace + if ((styles & NumberStyles.AllowTrailingWhite) != 0) + { + while (index < value.Length && IsWhite(TChar.CastToUInt32(value[index]))) + { + index++; + } + } + + if (index != value.Length) + { + return false; + } + + if (significand == 0) + { + result = isNegative ? TFloat.NegativeZero : TFloat.Zero; + return true; + } + + // Compute the effective binary exponent. + // value = significand * 2^(-4 * fractionalDigitsConsumed) * 2^(4 * overflowIntegerDigits) * 2^binaryExponent + long exp = (long)binaryExponent - 4L * fractionalDigitsConsumed + 4L * overflowIntegerDigits; + + // Normalize: shift significand so MSB is at bit 63 + int lz = BitOperations.LeadingZeroCount(significand); + significand <<= lz; + exp -= lz; + + // significand is now in [2^63, 2^64), so value = significand * 2^exp + // = (significand / 2^63) * 2^(exp + 63) = 1.xxx * 2^(exp + 63) + long actualExp = exp + 63; + + int mantissaBits = TFloat.DenormalMantissaBits; + + if (actualExp > TFloat.MaxBinaryExponent) + { + result = isNegative ? TFloat.NegativeInfinity : TFloat.PositiveInfinity; + return true; + } + + int shiftRight = 63 - mantissaBits; + long biasedExp = actualExp + TFloat.ExponentBias; + + if (biasedExp <= 0) + { + long denormalShift = 1L - biasedExp; + if (denormalShift > 64 - shiftRight) + { + // Value is too small to round to min subnormal + result = isNegative ? TFloat.NegativeZero : TFloat.Zero; + return true; + } + shiftRight += (int)denormalShift; + biasedExp = 0; + } + + // Round to nearest, ties to even + ulong mantissa = 0; + if (shiftRight > 0 && shiftRight < 64) + { + ulong roundBit = 1UL << (shiftRight - 1); + ulong stickyBits = (significand & (roundBit - 1)) | (hasDiscardedNonZeroDigits ? 1UL : 0UL); + mantissa = significand >> shiftRight; + + if ((significand & roundBit) != 0 && (stickyBits != 0 || (mantissa & 1) != 0)) + { + mantissa++; + + if (biasedExp == 0 && mantissa > TFloat.DenormalMantissaMask) + { + biasedExp = 1; + mantissa &= TFloat.DenormalMantissaMask; + } + else if (mantissa > ((1UL << (mantissaBits + 1)) - 1)) + { + mantissa >>= 1; + biasedExp++; + if (biasedExp >= TFloat.InfinityExponent) + { + result = isNegative ? TFloat.NegativeInfinity : TFloat.PositiveInfinity; + return true; + } + } + } + } + else if (shiftRight == 64) + { + // Significand is at bit 63. Round bit is bit 63, sticky bits are 62..0. + ulong roundBit = 1UL << 63; + ulong stickyBits = (significand & (roundBit - 1)) | (hasDiscardedNonZeroDigits ? 1UL : 0UL); + mantissa = 0; + + // mantissa is 0 (even), so ties-to-even rounds up only when sticky bits are nonzero. + if ((significand & roundBit) != 0 && stickyBits != 0) + { + mantissa = 1; + if (mantissa > TFloat.DenormalMantissaMask) + { + biasedExp = 1; + mantissa &= TFloat.DenormalMantissaMask; + } + } + } + else if (shiftRight == 0) + { + mantissa = significand; + } + + mantissa &= TFloat.DenormalMantissaMask; + + ulong bits = ((ulong)biasedExp << mantissaBits) | mantissa; + result = TFloat.BitsToFloat(bits); + if (isNegative) + { + result = -result; + } + + return true; + } + internal static bool TryParseFloat(ReadOnlySpan value, NumberStyles styles, NumberFormatInfo info, out TFloat result) where TChar : unmanaged, IUtfChar where TFloat : unmanaged, IBinaryFloatParseAndFormatInfo { + if ((styles & NumberStyles.AllowHexSpecifier) != 0) + { + return TryParseHexFloatingPoint(value, styles, info, out result); + } + NumberBuffer number = new NumberBuffer(NumberBufferKind.FloatingPoint, stackalloc byte[TFloat.NumberBufferLength]); if (!TryStringToNumber(value, styles, ref number, info)) diff --git a/src/libraries/System.Private.CoreLib/src/System/Numerics/BFloat16.cs b/src/libraries/System.Private.CoreLib/src/System/Numerics/BFloat16.cs index 545b9a62091637..356e3e19909150 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Numerics/BFloat16.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Numerics/BFloat16.cs @@ -2088,14 +2088,14 @@ public static (BFloat16 SinPi, BFloat16 CosPi) SinCosPi(BFloat16 x) /// public static BFloat16 Parse(ReadOnlySpan utf8Text, NumberStyles style = DefaultParseStyle, IFormatProvider? provider = null) { - NumberFormatInfo.ValidateParseStyleInteger(style); + NumberFormatInfo.ValidateParseStyleFloatingPoint(style); return Number.ParseFloat(utf8Text, style, NumberFormatInfo.GetInstance(provider)); } /// public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFormatProvider? provider, out BFloat16 result) { - NumberFormatInfo.ValidateParseStyleInteger(style); + NumberFormatInfo.ValidateParseStyleFloatingPoint(style); return Number.TryParseFloat(utf8Text, style, NumberFormatInfo.GetInstance(provider), out result); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Single.cs b/src/libraries/System.Private.CoreLib/src/System/Single.cs index a284cb69ed3a32..cfad70514b4dc9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Single.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Single.cs @@ -2180,14 +2180,14 @@ public static float TanPi(float x) /// public static float Parse(ReadOnlySpan utf8Text, NumberStyles style = NumberStyles.Float | NumberStyles.AllowThousands, IFormatProvider? provider = null) { - NumberFormatInfo.ValidateParseStyleInteger(style); + NumberFormatInfo.ValidateParseStyleFloatingPoint(style); return Number.ParseFloat(utf8Text, style, NumberFormatInfo.GetInstance(provider)); } /// public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFormatProvider? provider, out float result) { - NumberFormatInfo.ValidateParseStyleInteger(style); + NumberFormatInfo.ValidateParseStyleFloatingPoint(style); return Number.TryParseFloat(utf8Text, style, NumberFormatInfo.GetInstance(provider), out result); } diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index ccc217672ebad4..03ffbfee054378 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -9806,6 +9806,7 @@ public enum NumberStyles Any = 511, AllowHexSpecifier = 512, HexNumber = 515, + HexFloat = 551, AllowBinarySpecifier = 1024, BinaryNumber = 1027, } diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/NumberFormatInfo/NumberFormatInfoValidateParseStyle.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/NumberFormatInfo/NumberFormatInfoValidateParseStyle.cs index 8e130fbd2e532b..bb04917ea6f9a6 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/NumberFormatInfo/NumberFormatInfoValidateParseStyle.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/NumberFormatInfo/NumberFormatInfoValidateParseStyle.cs @@ -26,8 +26,11 @@ public void ValidateParseStyle_Integer(NumberStyles style, bool valid) [Theory] [InlineData(unchecked((NumberStyles)0xFFFFFC00), false)] - [InlineData(NumberStyles.HexNumber | NumberStyles.Integer, false)] - [InlineData(NumberStyles.AllowHexSpecifier, false)] + [InlineData(NumberStyles.HexNumber | NumberStyles.Integer, true)] + [InlineData(NumberStyles.AllowHexSpecifier, true)] + [InlineData(NumberStyles.HexFloat, true)] + [InlineData(NumberStyles.AllowBinarySpecifier, false)] + [InlineData(NumberStyles.AllowHexSpecifier | NumberStyles.AllowExponent, false)] [InlineData(NumberStyles.None, true)] public void ValidateParseStyle_Float(NumberStyles style, bool valid) { diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs index ac1bf4abf4e863..5281afe19f5816 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs @@ -331,6 +331,161 @@ public static IEnumerable Parse_Valid_TestData() yield return new object[] { "NaN", NumberStyles.Any, invariantFormat, double.NaN }; yield return new object[] { "Infinity", NumberStyles.Any, invariantFormat, double.PositiveInfinity }; yield return new object[] { "-Infinity", NumberStyles.Any, invariantFormat, double.NegativeInfinity }; + + // Hex float parsing tests (IEEE 754:2008 §5.12.3) + // Basic values + yield return new object[] { "0x1.0p0", NumberStyles.HexFloat, invariantFormat, 1.0 }; + yield return new object[] { "0x1.8p0", NumberStyles.HexFloat, invariantFormat, 1.5 }; + yield return new object[] { "0x1.0p1", NumberStyles.HexFloat, invariantFormat, 2.0 }; + yield return new object[] { "0x1.0p-1", NumberStyles.HexFloat, invariantFormat, 0.5 }; + yield return new object[] { "0x0.8p0", NumberStyles.HexFloat, invariantFormat, 0.5 }; + yield return new object[] { "0x1.4p3", NumberStyles.HexFloat, invariantFormat, 10.0 }; + yield return new object[] { "0x1.0p10", NumberStyles.HexFloat, invariantFormat, 1024.0 }; + yield return new object[] { "0x1.0p-10", NumberStyles.HexFloat, invariantFormat, 0.0009765625 }; + yield return new object[] { "0x1.0p+10", NumberStyles.HexFloat, invariantFormat, 1024.0 }; + yield return new object[] { "0x10p0", NumberStyles.HexFloat, invariantFormat, 16.0 }; + + // Sign handling + yield return new object[] { "-0x1.0p0", NumberStyles.HexFloat, invariantFormat, -1.0 }; + yield return new object[] { "+0x1.0p0", NumberStyles.HexFloat, invariantFormat, 1.0 }; + yield return new object[] { "-0x1.8p1", NumberStyles.HexFloat, invariantFormat, -3.0 }; + yield return new object[] { "+0xAp0", NumberStyles.HexFloat, invariantFormat, 10.0 }; + + // Case variations + yield return new object[] { "0X1.0P0", NumberStyles.HexFloat, invariantFormat, 1.0 }; + yield return new object[] { "0x1.Ap1", NumberStyles.HexFloat, invariantFormat, 3.25 }; + yield return new object[] { "0x1.ap1", NumberStyles.HexFloat, invariantFormat, 3.25 }; + yield return new object[] { "0X1.AP1", NumberStyles.HexFloat, invariantFormat, 3.25 }; + yield return new object[] { "0x1.AbCdEfP+4", NumberStyles.HexFloat, invariantFormat, 26.737776756286621 }; + yield return new object[] { "0xaBcDeF.0P0", NumberStyles.HexFloat, invariantFormat, 11259375.0 }; + + // Zero variants + yield return new object[] { "0x0p0", NumberStyles.HexFloat, invariantFormat, 0.0 }; + yield return new object[] { "-0x0p0", NumberStyles.HexFloat, invariantFormat, -0.0 }; + yield return new object[] { "0x0.0p0", NumberStyles.HexFloat, invariantFormat, 0.0 }; + yield return new object[] { "0x00p0", NumberStyles.HexFloat, invariantFormat, 0.0 }; + yield return new object[] { "0x000.000p0", NumberStyles.HexFloat, invariantFormat, 0.0 }; + yield return new object[] { "0x0", NumberStyles.HexFloat, invariantFormat, 0.0 }; + yield return new object[] { "0", NumberStyles.HexFloat, invariantFormat, 0.0 }; + yield return new object[] { "-0x0", NumberStyles.HexFloat, invariantFormat, -0.0 }; + + // Fractional-only significand (no integer part before decimal) + yield return new object[] { "0x.8p1", NumberStyles.HexFloat, invariantFormat, 1.0 }; + yield return new object[] { "0x.Cp2", NumberStyles.HexFloat, invariantFormat, 3.0 }; + yield return new object[] { "0x.4p2", NumberStyles.HexFloat, invariantFormat, 1.0 }; + yield return new object[] { "0x.001p12", NumberStyles.HexFloat, invariantFormat, 1.0 }; + yield return new object[] { "0x.1p4", NumberStyles.HexFloat, invariantFormat, 1.0 }; + yield return new object[] { "0x.1p04", NumberStyles.HexFloat, invariantFormat, 1.0 }; + + // Leading zeros in integer and fractional parts + yield return new object[] { "0x001p0", NumberStyles.HexFloat, invariantFormat, 1.0 }; + yield return new object[] { "0x0001.0p0", NumberStyles.HexFloat, invariantFormat, 1.0 }; + yield return new object[] { "0x1.00000p0", NumberStyles.HexFloat, invariantFormat, 1.0 }; + yield return new object[] { "0x00Ap0", NumberStyles.HexFloat, invariantFormat, 10.0 }; + + // Trailing decimal point (no fractional digits) + yield return new object[] { "0x1.p0", NumberStyles.HexFloat, invariantFormat, 1.0 }; + yield return new object[] { "0xA.p0", NumberStyles.HexFloat, invariantFormat, 10.0 }; + + // Edge case values + yield return new object[] { "0x1.0p-1074", NumberStyles.HexFloat, invariantFormat, double.Epsilon }; + yield return new object[] { "0x1.fffffffffffffp1023", NumberStyles.HexFloat, invariantFormat, double.MaxValue }; + yield return new object[] { "-0x1.fffffffffffffp1023", NumberStyles.HexFloat, invariantFormat, double.MinValue }; + yield return new object[] { "0x1.0p-1022", NumberStyles.HexFloat, invariantFormat, 2.2250738585072014E-308 }; // Min normal + yield return new object[] { "0x0.0000000000001p-1022", NumberStyles.HexFloat, invariantFormat, double.Epsilon }; // Min subnormal + yield return new object[] { "0x1.fffffffffffffp+1023", NumberStyles.HexFloat, invariantFormat, 1.7976931348623157E+308 }; // MAX + + // Well-known constants + yield return new object[] { "0x1.921fb54442d18p+1", NumberStyles.HexFloat, invariantFormat, Math.PI }; + yield return new object[] { "0x1.5bf0a8b145769p+1", NumberStyles.HexFloat, invariantFormat, Math.E }; + // Various representations of pi (same value, different significand/exponent splits) + yield return new object[] { "0x3.243f6a8885a3p0", NumberStyles.HexFloat, invariantFormat, Math.PI }; + yield return new object[] { "0x6.487ed5110b46p-1", NumberStyles.HexFloat, invariantFormat, Math.PI }; + yield return new object[] { "0xc.90fdaa22168cp-2", NumberStyles.HexFloat, invariantFormat, Math.PI }; + yield return new object[] { "0x19.21fb54442d18p-3", NumberStyles.HexFloat, invariantFormat, Math.PI }; + yield return new object[] { "0x0.c90fdaa22168cp2", NumberStyles.HexFloat, invariantFormat, Math.PI }; + // Various representations of 190 + yield return new object[] { "0xbep0", NumberStyles.HexFloat, invariantFormat, 190.0 }; + yield return new object[] { "0XBE0P-4", NumberStyles.HexFloat, invariantFormat, 190.0 }; + yield return new object[] { "0xB.Ep4", NumberStyles.HexFloat, invariantFormat, 190.0 }; + yield return new object[] { "0x.BEp8", NumberStyles.HexFloat, invariantFormat, 190.0 }; + yield return new object[] { "0x.0BEp12", NumberStyles.HexFloat, invariantFormat, 190.0 }; + + // Overflow to infinity + yield return new object[] { "0x1.0p1024", NumberStyles.HexFloat, invariantFormat, double.PositiveInfinity }; + yield return new object[] { "-0x1.0p1024", NumberStyles.HexFloat, invariantFormat, double.NegativeInfinity }; + yield return new object[] { "0x1.0p99999", NumberStyles.HexFloat, invariantFormat, double.PositiveInfinity }; + yield return new object[] { "0x1p+1025", NumberStyles.HexFloat, invariantFormat, double.PositiveInfinity }; + yield return new object[] { "0X1p1030", NumberStyles.HexFloat, invariantFormat, double.PositiveInfinity }; + yield return new object[] { "0x1p+1100", NumberStyles.HexFloat, invariantFormat, double.PositiveInfinity }; + yield return new object[] { "0X.8p+1025", NumberStyles.HexFloat, invariantFormat, double.PositiveInfinity }; + yield return new object[] { "0x0.8p1025", NumberStyles.HexFloat, invariantFormat, double.PositiveInfinity }; + yield return new object[] { "0x2p+1023", NumberStyles.HexFloat, invariantFormat, double.PositiveInfinity }; + yield return new object[] { "0x2.0p+1023", NumberStyles.HexFloat, invariantFormat, double.PositiveInfinity }; + yield return new object[] { "0X4p+1022", NumberStyles.HexFloat, invariantFormat, double.PositiveInfinity }; + yield return new object[] { "0x1.ffffffffffffffp+1023", NumberStyles.HexFloat, invariantFormat, double.PositiveInfinity }; + + // Underflow to zero + yield return new object[] { "0x1.0p-1075", NumberStyles.HexFloat, invariantFormat, 0.0 }; + yield return new object[] { "0x1.0p-9999", NumberStyles.HexFloat, invariantFormat, 0.0 }; + yield return new object[] { "0X1p-1075", NumberStyles.HexFloat, invariantFormat, 0.0 }; + yield return new object[] { "0x1.1p-1075", NumberStyles.HexFloat, invariantFormat, double.Epsilon }; + + // Round-half-even near zero + yield return new object[] { "0x.8p-1074", NumberStyles.HexFloat, invariantFormat, 0.0 }; + yield return new object[] { "0x.81p-1074", NumberStyles.HexFloat, invariantFormat, 5e-324 }; + yield return new object[] { "0x8p-1078", NumberStyles.HexFloat, invariantFormat, 0.0 }; + yield return new object[] { "0x8.1p-1078", NumberStyles.HexFloat, invariantFormat, 5e-324 }; + yield return new object[] { "0x80p-1082", NumberStyles.HexFloat, invariantFormat, 0.0 }; + yield return new object[] { "0x81p-1082", NumberStyles.HexFloat, invariantFormat, 5e-324 }; + yield return new object[] { "0x1p-1076", NumberStyles.HexFloat, invariantFormat, 0.0 }; // 0.5 * TINY, ties to even = 0 + yield return new object[] { "0X2p-1076", NumberStyles.HexFloat, invariantFormat, 0.0 }; + yield return new object[] { "0X3p-1076", NumberStyles.HexFloat, invariantFormat, 5e-324 }; // 1.5 * TINY rounds up + yield return new object[] { "0x4p-1076", NumberStyles.HexFloat, invariantFormat, 5e-324 }; + yield return new object[] { "0X5p-1076", NumberStyles.HexFloat, invariantFormat, 5e-324 }; + yield return new object[] { "0X6p-1076", NumberStyles.HexFloat, invariantFormat, 1e-323 }; + yield return new object[] { "0x7p-1076", NumberStyles.HexFloat, invariantFormat, 1e-323 }; + + // Round-half-even near 1.0 + yield return new object[] { "0x1.00000000000008p0", NumberStyles.HexFloat, invariantFormat, 1.0 }; // exactly halfway, ties to even + yield return new object[] { "0x1.00000000000018p0", NumberStyles.HexFloat, invariantFormat, 1.0000000000000004 }; // ties to even + + // Round-half-even near MIN normal boundary + yield return new object[] { "0x0.fffffffffffff8p-1022", NumberStyles.HexFloat, invariantFormat, 2.2250738585072014E-308 }; + yield return new object[] { "0x1.00000000000008p-1022", NumberStyles.HexFloat, invariantFormat, 2.2250738585072014E-308 }; + + // Whitespace handling + yield return new object[] { " 0x1.0p0 ", NumberStyles.HexFloat, invariantFormat, 1.0 }; + yield return new object[] { " 0x1.0p0 ", NumberStyles.HexFloat, invariantFormat, 1.0 }; + + // Without 0x prefix + yield return new object[] { "1.0p0", NumberStyles.HexFloat, invariantFormat, 1.0 }; + yield return new object[] { "1.8p0", NumberStyles.HexFloat, invariantFormat, 1.5 }; + yield return new object[] { "Ap0", NumberStyles.HexFloat, invariantFormat, 10.0 }; + yield return new object[] { "FF.8p0", NumberStyles.HexFloat, invariantFormat, 255.5 }; + + // Without exponent (integer-only form) + yield return new object[] { "0xA", NumberStyles.HexFloat, invariantFormat, 10.0 }; + yield return new object[] { "0xFF", NumberStyles.HexFloat, invariantFormat, 255.0 }; + yield return new object[] { "A", NumberStyles.HexFloat, invariantFormat, 10.0 }; + yield return new object[] { "1", NumberStyles.HexFloat, invariantFormat, 1.0 }; + yield return new object[] { "FF", NumberStyles.HexFloat, invariantFormat, 255.0 }; + yield return new object[] { "0x100", NumberStyles.HexFloat, invariantFormat, 256.0 }; + + // Large significand (many hex digits) + yield return new object[] { "0xFFFFFFFFFFFFFFp0", NumberStyles.HexFloat, invariantFormat, (double)0xFFFFFFFFFFFFFF }; + yield return new object[] { "0x1.0000000000000p0", NumberStyles.HexFloat, invariantFormat, 1.0 }; + + // Denormal values + yield return new object[] { "0x0.8p-1073", NumberStyles.HexFloat, invariantFormat, double.Epsilon }; + yield return new object[] { "0x0.4p-1072", NumberStyles.HexFloat, invariantFormat, double.Epsilon }; + + // Round-trip: format then parse + yield return new object[] { "0x1.999999999999ap-4", NumberStyles.HexFloat, invariantFormat, 0.1 }; + + // HexFloat without AllowDecimalPoint should parse integers only + yield return new object[] { "0xFF", NumberStyles.AllowHexSpecifier | NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite, invariantFormat, 255.0 }; + yield return new object[] { "A", NumberStyles.AllowHexSpecifier, invariantFormat, 10.0 }; } [Theory] @@ -457,6 +612,29 @@ public static IEnumerable Parse_Invalid_TestData() yield return new object[] { "ab", NumberStyles.None, null, typeof(FormatException) }; // Negative hex value yield return new object[] { " 123 ", NumberStyles.None, null, typeof(FormatException) }; // Trailing and leading whitespace + + // Invalid hex float inputs + yield return new object[] { "", NumberStyles.HexFloat, null, typeof(FormatException) }; // Empty + yield return new object[] { " ", NumberStyles.HexFloat, null, typeof(FormatException) }; // Whitespace only + yield return new object[] { "0x", NumberStyles.HexFloat, null, typeof(FormatException) }; // Prefix only + yield return new object[] { "0x.p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // No significand digits + yield return new object[] { "0x1.0e5", NumberStyles.HexFloat, null, typeof(FormatException) }; // 'e' exponent instead of 'p' + yield return new object[] { "0x1.0p", NumberStyles.HexFloat, null, typeof(FormatException) }; // Exponent marker without digits + yield return new object[] { "0x1.8", NumberStyles.HexFloat, null, typeof(FormatException) }; // Fractional part without exponent + yield return new object[] { "0x.8", NumberStyles.HexFloat, null, typeof(FormatException) }; // Fractional-only without exponent + yield return new object[] { "0xGp0", NumberStyles.HexFloat, null, typeof(FormatException) }; // Invalid hex char + yield return new object[] { "0x1.Gp0", NumberStyles.HexFloat, null, typeof(FormatException) }; // Invalid hex char in fraction + yield return new object[] { "0x1.0p0garbage", NumberStyles.HexFloat, null, typeof(FormatException) }; // Trailing garbage + yield return new object[] { "+-0x1.0p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // Double sign + yield return new object[] { "0x1.0p+-1", NumberStyles.HexFloat, null, typeof(FormatException) }; // Double exponent sign + yield return new object[] { "NaN", NumberStyles.HexFloat, null, typeof(FormatException) }; // NaN not valid for HexFloat + yield return new object[] { "Infinity", NumberStyles.HexFloat, null, typeof(FormatException) }; // Infinity not valid for HexFloat + yield return new object[] { "0xX1.0p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // double X + yield return new object[] { "x1.0p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // missing 0 before x + yield return new object[] { "0x1.0p 0", NumberStyles.HexFloat, null, typeof(FormatException) }; // internal whitespace in exponent + yield return new object[] { "0x1pa", NumberStyles.HexFloat, null, typeof(FormatException) }; // non-digit in exponent + yield return new object[] { "xyz", NumberStyles.HexFloat, null, typeof(FormatException) }; // no hex digits + yield return new object[] { "0x1.0.p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // double decimal point } [Theory] @@ -843,6 +1021,127 @@ public static void ToString_ValidLargeFormat() d.ToString("E00000999999999"); // Should not throw } + [Theory] + // Basic values + [InlineData(1.0, "x", "1p+0")] + [InlineData(1.5, "x", "1.8p+0")] + [InlineData(2.0, "x", "1p+1")] + [InlineData(0.5, "x", "1p-1")] + [InlineData(-1.0, "x", "-1p+0")] + [InlineData(10.0, "x", "1.4p+3")] + [InlineData(0.25, "x", "1p-2")] + [InlineData(0.75, "x", "1.8p-1")] + [InlineData(100.0, "x", "1.9p+6")] + [InlineData(0.1, "x", "1.999999999999ap-4")] + [InlineData(0.3, "x", "1.3333333333333p-2")] + [InlineData(Math.PI, "x", "1.921fb54442d18p+1")] + [InlineData(Math.E, "x", "1.5bf0a8b145769p+1")] + [InlineData(1234567890.123456, "x", "1.26580b487e6b4p+30")] + [InlineData(-2.2250738585072014E-308, "x", "-1p-1022")] + // Zero + [InlineData(0.0, "x", "0p+0")] + [InlineData(-0.0, "x", "-0p+0")] + // Special values + [InlineData(double.NaN, "x", "NaN")] + [InlineData(double.PositiveInfinity, "x", "Infinity")] + [InlineData(double.NegativeInfinity, "x", "-Infinity")] + // Edge case values + [InlineData(double.MaxValue, "x", "1.fffffffffffffp+1023")] + [InlineData(double.MinValue, "x", "-1.fffffffffffffp+1023")] + [InlineData(double.Epsilon, "x", "1p-1074")] + [InlineData(-double.Epsilon, "x", "-1p-1074")] + [InlineData(2.2250738585072009E-308, "x", "1.ffffffffffffep-1023")] // Max subnormal + [InlineData(1.48219693752374E-323, "x", "1.8p-1073")] // 3 * Epsilon + [InlineData(2.2250738585072014E-308, "x", "1p-1022")] // Min normal + // Uppercase + [InlineData(3.25, "X", "1.AP+1")] + [InlineData(10.0, "X", "1.4P+3")] + [InlineData(255.5, "X", "1.FFP+7")] + // Explicit precision + [InlineData(1.0, "x0", "1p+0")] + [InlineData(1.0, "x2", "1.00p+0")] + [InlineData(1.5, "x4", "1.8000p+0")] + [InlineData(1.5, "x13", "1.8000000000000p+0")] + [InlineData(1.5, "x1", "1.8p+0")] + [InlineData(0.1, "x1", "1.ap-4")] // 0.1 = 1.999999999999a, round at 1 digit: 0x19... > 0x10, round up + [InlineData(1.0, "x15", "1.000000000000000p+0")] // precision > default (13) + // Precision with uppercase + [InlineData(3.25, "X4", "1.A000P+1")] + [InlineData(1.5, "X0", "2P+0")] + // Precision rounding: tie-to-even + [InlineData(1.53125, "x1", "1.8p+0")] // 1.8800...p+0 x1: halfway, 8 is even => stays 8 + [InlineData(1.59375, "x1", "1.ap+0")] // 1.9800...p+0 x1: halfway, 9 is odd => rounds to a + [InlineData(1.5937499999999998, "x1", "1.9p+0")] // just below halfway => truncate + [InlineData(1.5937500000000002, "x1", "1.ap+0")] // just above halfway => round up + // Precision rounding: carry into leading digit + [InlineData(1.9999999999999998, "x0", "2p+0")] // 1.fffff...p+0 x0: rounds up to 2 + // Precision rounding: carry through all fractional nibbles bumps exponent + [InlineData(1.9999999999999998, "x1", "1.0p+1")] // 1.fffff...p+0 x1: round up overflows => exponent bumps + // Large precision (beyond mantissa bits) + [InlineData(1.0, "x20", "1.00000000000000000000p+0")] + [InlineData(double.Epsilon, "x20", "1.00000000000000000000p-1074")] + // Zero with precision + [InlineData(0.0, "x3", "0.000p+0")] + [InlineData(0.0, "x0", "0p+0")] + [InlineData(0.0, "x20", "0.00000000000000000000p+0")] + [InlineData(-0.0, "x0", "-0p+0")] + [InlineData(-0.0, "x3", "-0.000p+0")] + public static void ToStringHexFloat(double d, string format, string expected) + { + Assert.Equal(expected, d.ToString(format, NumberFormatInfo.InvariantInfo)); + NumberFormatTestHelper.TryFormatNumberTest(d, format, NumberFormatInfo.InvariantInfo, expected, formatCasingMatchesOutput: false); + + if (!double.IsNaN(d) && !double.IsInfinity(d) && format.Length == 1) // round-trip: default precision only + { + double parsed = double.Parse(expected, NumberStyles.HexFloat, NumberFormatInfo.InvariantInfo); + Assert.Equal(d, parsed); + } + } + + [Theory] + [InlineData("-0x0p0")] + [InlineData("-0x0.0p0")] + [InlineData("-0x0")] + public static void ParseHexFloat_NegativeZero(string input) + { + double result = double.Parse(input, NumberStyles.HexFloat, NumberFormatInfo.InvariantInfo); + Assert.True(double.IsNegative(result) && result == 0.0); + + result = double.Parse(input.AsSpan(), NumberStyles.HexFloat, NumberFormatInfo.InvariantInfo); + Assert.True(double.IsNegative(result) && result == 0.0); + + result = double.Parse(Encoding.UTF8.GetBytes(input), NumberStyles.HexFloat, NumberFormatInfo.InvariantInfo); + Assert.True(double.IsNegative(result) && result == 0.0); + + Assert.True(double.TryParse(input, NumberStyles.HexFloat, NumberFormatInfo.InvariantInfo, out result)); + Assert.True(double.IsNegative(result) && result == 0.0); + } + + [Fact] + public static void HexFloat_CustomNumberFormat() + { + var commaSep = new NumberFormatInfo { NumberDecimalSeparator = "," }; + Assert.Equal(1.5, double.Parse("0x1,8p0", NumberStyles.HexFloat, commaSep)); + Assert.False(double.TryParse("0x1.8p0", NumberStyles.HexFloat, commaSep, out _)); + Assert.Equal("1,8p+0", 1.5.ToString("x", commaSep)); + NumberFormatTestHelper.TryFormatNumberTest(1.5, "x", commaSep, "1,8p+0", formatCasingMatchesOutput: false); + + var tildeMinus = new NumberFormatInfo { NegativeSign = "~" }; + Assert.Equal("~1p+0", (-1.0).ToString("x", tildeMinus)); + NumberFormatTestHelper.TryFormatNumberTest(-1.0, "x", tildeMinus, "~1p+0", formatCasingMatchesOutput: false); + } + + [Theory] + [InlineData(NumberStyles.HexFloat | NumberStyles.AllowThousands)] + [InlineData(NumberStyles.HexFloat | NumberStyles.AllowCurrencySymbol)] + [InlineData(NumberStyles.HexFloat | NumberStyles.AllowExponent)] + [InlineData(NumberStyles.AllowBinarySpecifier)] + public static void HexFloat_StyleValidation(NumberStyles style) + { + Assert.Throws(() => double.Parse("0x1p0", style)); + Assert.Throws(() => double.TryParse("0x1p0", style, NumberFormatInfo.InvariantInfo, out _)); + } + [Theory] [InlineData(double.NegativeInfinity, false)] // Negative Infinity [InlineData(double.MinValue, true)] // Min Negative Normal diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/HalfTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/HalfTests.cs index 62a8d831e9fccd..ad5b67a3d85c98 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/HalfTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/HalfTests.cs @@ -731,6 +731,56 @@ public static IEnumerable Parse_Valid_TestData() yield return new object[] { "NaN", NumberStyles.Any, invariantFormat, float.NaN }; yield return new object[] { "Infinity", NumberStyles.Any, invariantFormat, float.PositiveInfinity }; yield return new object[] { "-Infinity", NumberStyles.Any, invariantFormat, float.NegativeInfinity }; + + // Hex float parsing tests + // Basic values + yield return new object[] { "0x1.0p0", NumberStyles.HexFloat, invariantFormat, 1.0f }; + yield return new object[] { "0x1.8p0", NumberStyles.HexFloat, invariantFormat, 1.5f }; + yield return new object[] { "0x1.0p1", NumberStyles.HexFloat, invariantFormat, 2.0f }; + yield return new object[] { "0x1.0p-1", NumberStyles.HexFloat, invariantFormat, 0.5f }; + yield return new object[] { "-0x1.0p0", NumberStyles.HexFloat, invariantFormat, -1.0f }; + yield return new object[] { "+0x1.0p0", NumberStyles.HexFloat, invariantFormat, 1.0f }; + yield return new object[] { "0X1.0P0", NumberStyles.HexFloat, invariantFormat, 1.0f }; + yield return new object[] { "0xAp0", NumberStyles.HexFloat, invariantFormat, 10.0f }; + + // Fractional-only significand + yield return new object[] { "0x.8p1", NumberStyles.HexFloat, invariantFormat, 1.0f }; + + // Leading zeros + yield return new object[] { "0x001p0", NumberStyles.HexFloat, invariantFormat, 1.0f }; + + // Edge case values + yield return new object[] { "0x1.0p-24", NumberStyles.HexFloat, invariantFormat, (float)Math.Pow(2, -24) }; + yield return new object[] { "0x1.ffcp15", NumberStyles.HexFloat, invariantFormat, 65504.0f }; // Half max + yield return new object[] { "0x1.0p-14", NumberStyles.HexFloat, invariantFormat, 6.1035156E-05f }; // Min normal for Half + + // Zero + yield return new object[] { "0x0p0", NumberStyles.HexFloat, invariantFormat, 0.0f }; + yield return new object[] { "-0x0p0", NumberStyles.HexFloat, invariantFormat, -0.0f }; + yield return new object[] { "0x0", NumberStyles.HexFloat, invariantFormat, 0.0f }; + + // Whitespace + yield return new object[] { " 0x1.0p0 ", NumberStyles.HexFloat, invariantFormat, 1.0f }; + + // Without prefix + yield return new object[] { "1.0p0", NumberStyles.HexFloat, invariantFormat, 1.0f }; + yield return new object[] { "A", NumberStyles.HexFloat, invariantFormat, 10.0f }; + + // Without exponent + yield return new object[] { "0xA", NumberStyles.HexFloat, invariantFormat, 10.0f }; + yield return new object[] { "0xFF", NumberStyles.HexFloat, invariantFormat, 255.0f }; + + // Overflow to infinity + yield return new object[] { "0x1.0p16", NumberStyles.HexFloat, invariantFormat, float.PositiveInfinity }; + yield return new object[] { "-0x1.0p16", NumberStyles.HexFloat, invariantFormat, float.NegativeInfinity }; + + // Round-half-even near zero + yield return new object[] { "0x1p-25", NumberStyles.HexFloat, invariantFormat, 0.0f }; // 0.5*Epsilon, ties to even -> 0 + yield return new object[] { "0x3p-25", NumberStyles.HexFloat, invariantFormat, 1.19209289550781E-07f }; // 1.5*Epsilon, ties to even -> 2*Epsilon + + // Various representations + yield return new object[] { "0xA.0p0", NumberStyles.HexFloat, invariantFormat, 10.0f }; + yield return new object[] { "0x.1p4", NumberStyles.HexFloat, invariantFormat, 1.0f }; } [Theory] @@ -1045,6 +1095,99 @@ private static void ToStringTest(Half f, string format, IFormatProvider provider Assert.Equal(expected.Replace('E', 'e'), f.ToString(format.ToLowerInvariant(), provider)); } + [Theory] + [InlineData(1.0f, "x", "1p+0")] + [InlineData(1.5f, "x", "1.8p+0")] + [InlineData(2.0f, "x", "1p+1")] + [InlineData(0.5f, "x", "1p-1")] + [InlineData(-1.0f, "x", "-1p+0")] + [InlineData(0.0f, "x", "0p+0")] + [InlineData(-0.0f, "x", "-0p+0")] + [InlineData(10.0f, "x", "1.4p+3")] + [InlineData(0.25f, "x", "1p-2")] + [InlineData(0.1f, "x", "1.998p-4")] + // Special values + [InlineData(float.NaN, "x", "NaN")] + [InlineData(float.PositiveInfinity, "x", "Infinity")] + [InlineData(float.NegativeInfinity, "x", "-Infinity")] + // Edge case values + [InlineData(65504.0f, "x", "1.ffcp+15")] // Half.MaxValue + [InlineData(-65504.0f, "x", "-1.ffcp+15")] // Half.MinValue + [InlineData(5.9604645E-08f, "x", "1p-24")] // Half.Epsilon + [InlineData(-5.9604645E-08f, "x", "-1p-24")] // -Half.Epsilon + [InlineData(6.1035156E-05f, "x", "1p-14")] // Min normal + // Uppercase + [InlineData(3.25f, "X", "1.AP+1")] + // Precision + [InlineData(1.0f, "x0", "1p+0")] + [InlineData(1.5f, "x2", "1.80p+0")] + // Precision with uppercase + [InlineData(3.25f, "X4", "1.A000P+1")] + // Precision rounding: carry into leading digit + [InlineData(1.998f, "x0", "2p+0")] // (Half)1.998f ~ 1.ffcp+0, rounds up to 2 + // Large precision + [InlineData(1.0f, "x5", "1.00000p+0")] + // Zero precision + [InlineData(0.0f, "x0", "0p+0")] + [InlineData(0.0f, "x3", "0.000p+0")] + [InlineData(-0.0f, "x0", "-0p+0")] + public static void ToStringHexFloat(float f, string format, string expected) + { + Half h = (Half)f; + Assert.Equal(expected, h.ToString(format, NumberFormatInfo.InvariantInfo)); + NumberFormatTestHelper.TryFormatNumberTest(h, format, NumberFormatInfo.InvariantInfo, expected, formatCasingMatchesOutput: false); + + if (!Half.IsNaN(h) && !Half.IsInfinity(h) && format.Length == 1) + { + Half parsed = Half.Parse(expected, NumberStyles.HexFloat, NumberFormatInfo.InvariantInfo); + Assert.Equal(h, parsed); + } + } + + [Theory] + [InlineData("-0x0p0")] + [InlineData("-0x0.0p0")] + [InlineData("-0x0")] + public static void ParseHexFloat_NegativeZero(string input) + { + Half result = Half.Parse(input, NumberStyles.HexFloat, NumberFormatInfo.InvariantInfo); + Assert.True(Half.IsNegative(result) && result == (Half)0.0f); + + result = Half.Parse(input.AsSpan(), NumberStyles.HexFloat, NumberFormatInfo.InvariantInfo); + Assert.True(Half.IsNegative(result) && result == (Half)0.0f); + + result = Half.Parse(Encoding.UTF8.GetBytes(input), NumberStyles.HexFloat, NumberFormatInfo.InvariantInfo); + Assert.True(Half.IsNegative(result) && result == (Half)0.0f); + + Assert.True(Half.TryParse(input, NumberStyles.HexFloat, NumberFormatInfo.InvariantInfo, out result)); + Assert.True(Half.IsNegative(result) && result == (Half)0.0f); + } + + [Fact] + public static void HexFloat_CustomNumberFormat() + { + var commaSep = new NumberFormatInfo { NumberDecimalSeparator = "," }; + Assert.Equal((Half)1.5f, Half.Parse("0x1,8p0", NumberStyles.HexFloat, commaSep)); + Assert.False(Half.TryParse("0x1.8p0", NumberStyles.HexFloat, commaSep, out _)); + Assert.Equal("1,8p+0", ((Half)1.5f).ToString("x", commaSep)); + NumberFormatTestHelper.TryFormatNumberTest((Half)1.5f, "x", commaSep, "1,8p+0", formatCasingMatchesOutput: false); + + var tildeMinus = new NumberFormatInfo { NegativeSign = "~" }; + Assert.Equal("~1p+0", ((Half)(-1.0f)).ToString("x", tildeMinus)); + NumberFormatTestHelper.TryFormatNumberTest((Half)(-1.0f), "x", tildeMinus, "~1p+0", formatCasingMatchesOutput: false); + } + + [Theory] + [InlineData(NumberStyles.HexFloat | NumberStyles.AllowThousands)] + [InlineData(NumberStyles.HexFloat | NumberStyles.AllowCurrencySymbol)] + [InlineData(NumberStyles.HexFloat | NumberStyles.AllowExponent)] + [InlineData(NumberStyles.AllowBinarySpecifier)] + public static void HexFloat_StyleValidation(NumberStyles style) + { + Assert.Throws(() => Half.Parse("0x1p0", style)); + Assert.Throws(() => Half.TryParse("0x1p0", style, NumberFormatInfo.InvariantInfo, out _)); + } + [Fact] public static void ToString_InvalidFormat_ThrowsFormatException() { diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Numerics/BFloat16Tests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Numerics/BFloat16Tests.cs index 08597497d80b3a..d85884c8839f4a 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Numerics/BFloat16Tests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Numerics/BFloat16Tests.cs @@ -766,6 +766,22 @@ public static IEnumerable Parse_Valid_TestData() yield return new object[] { "NaN", NumberStyles.Any, invariantFormat, float.NaN }; yield return new object[] { "Infinity", NumberStyles.Any, invariantFormat, float.PositiveInfinity }; yield return new object[] { "-Infinity", NumberStyles.Any, invariantFormat, float.NegativeInfinity }; + + // Hex float + yield return new object[] { "0x1p0", NumberStyles.HexFloat, invariantFormat, 1.0f }; + yield return new object[] { "0x1.8p0", NumberStyles.HexFloat, invariantFormat, 1.5f }; + yield return new object[] { "0x1p1", NumberStyles.HexFloat, invariantFormat, 2.0f }; + yield return new object[] { "0x1p-1", NumberStyles.HexFloat, invariantFormat, 0.5f }; + yield return new object[] { "-0x1p0", NumberStyles.HexFloat, invariantFormat, -1.0f }; + yield return new object[] { "0x0p0", NumberStyles.HexFloat, invariantFormat, 0.0f }; + yield return new object[] { "0xAp0", NumberStyles.HexFloat, invariantFormat, 10.0f }; + yield return new object[] { "0x1.4p3", NumberStyles.HexFloat, invariantFormat, 10.0f }; + yield return new object[] { "0xFF", NumberStyles.HexFloat, invariantFormat, 255.0f }; + yield return new object[] { " 0x1p0 ", NumberStyles.HexFloat, invariantFormat, 1.0f }; + yield return new object[] { "0x.1p4", NumberStyles.HexFloat, invariantFormat, 1.0f }; + // Overflow to infinity + yield return new object[] { "0x1p128", NumberStyles.HexFloat, invariantFormat, float.PositiveInfinity }; + yield return new object[] { "-0x1p128", NumberStyles.HexFloat, invariantFormat, float.NegativeInfinity }; } [Theory] @@ -830,6 +846,16 @@ public static IEnumerable Parse_Invalid_TestData() yield return new object[] { "ab", NumberStyles.None, null, typeof(FormatException) }; // Negative hex value yield return new object[] { " 123 ", NumberStyles.None, null, typeof(FormatException) }; // Trailing and leading whitespace + + // Invalid hex float inputs + yield return new object[] { "", NumberStyles.HexFloat, null, typeof(FormatException) }; + yield return new object[] { "0x", NumberStyles.HexFloat, null, typeof(FormatException) }; + yield return new object[] { "0x.p0", NumberStyles.HexFloat, null, typeof(FormatException) }; + yield return new object[] { "0x1.8", NumberStyles.HexFloat, null, typeof(FormatException) }; + yield return new object[] { "0x1.0p", NumberStyles.HexFloat, null, typeof(FormatException) }; + yield return new object[] { "0xGp0", NumberStyles.HexFloat, null, typeof(FormatException) }; + yield return new object[] { "NaN", NumberStyles.HexFloat, null, typeof(FormatException) }; + yield return new object[] { "Infinity", NumberStyles.HexFloat, null, typeof(FormatException) }; } [Theory] @@ -1079,6 +1105,85 @@ private static void ToStringTest(BFloat16 f, string format, IFormatProvider prov Assert.Equal(expected.Replace('E', 'e'), f.ToString(format.ToLowerInvariant(), provider)); } + [Theory] + [InlineData(1.0f, "x", "1p+0")] + [InlineData(1.5f, "x", "1.8p+0")] + [InlineData(2.0f, "x", "1p+1")] + [InlineData(0.5f, "x", "1p-1")] + [InlineData(-1.0f, "x", "-1p+0")] + [InlineData(0.0f, "x", "0p+0")] + [InlineData(-0.0f, "x", "-0p+0")] + [InlineData(10.0f, "x", "1.4p+3")] + [InlineData(0.25f, "x", "1p-2")] + // Special values + [InlineData(float.NaN, "x", "NaN")] + [InlineData(float.PositiveInfinity, "x", "Infinity")] + [InlineData(float.NegativeInfinity, "x", "-Infinity")] + // Uppercase + [InlineData(3.25f, "X", "1.AP+1")] + // Precision + [InlineData(1.0f, "x0", "1p+0")] + [InlineData(1.5f, "x2", "1.80p+0")] + [InlineData(0.0f, "x0", "0p+0")] + [InlineData(0.0f, "x3", "0.000p+0")] + [InlineData(-0.0f, "x0", "-0p+0")] + public static void ToStringHexFloat(float f, string format, string expected) + { + BFloat16 b = (BFloat16)f; + Assert.Equal(expected, b.ToString(format, NumberFormatInfo.InvariantInfo)); + NumberFormatTestHelper.TryFormatNumberTest(b, format, NumberFormatInfo.InvariantInfo, expected, formatCasingMatchesOutput: false); + + if (!BFloat16.IsNaN(b) && !BFloat16.IsInfinity(b) && format.Length == 1) + { + BFloat16 parsed = BFloat16.Parse(expected, NumberStyles.HexFloat, NumberFormatInfo.InvariantInfo); + Assert.Equal(b, parsed); + } + } + + [Theory] + [InlineData("-0x0p0")] + [InlineData("-0x0.0p0")] + [InlineData("-0x0")] + public static void ParseHexFloat_NegativeZero(string input) + { + BFloat16 result = BFloat16.Parse(input, NumberStyles.HexFloat, NumberFormatInfo.InvariantInfo); + Assert.True(BFloat16.IsNegative(result) && result == (BFloat16)0.0f); + + result = BFloat16.Parse(input.AsSpan(), NumberStyles.HexFloat, NumberFormatInfo.InvariantInfo); + Assert.True(BFloat16.IsNegative(result) && result == (BFloat16)0.0f); + + result = BFloat16.Parse(Encoding.UTF8.GetBytes(input), NumberStyles.HexFloat, NumberFormatInfo.InvariantInfo); + Assert.True(BFloat16.IsNegative(result) && result == (BFloat16)0.0f); + + Assert.True(BFloat16.TryParse(input, NumberStyles.HexFloat, NumberFormatInfo.InvariantInfo, out result)); + Assert.True(BFloat16.IsNegative(result) && result == (BFloat16)0.0f); + } + + [Fact] + public static void HexFloat_CustomNumberFormat() + { + var commaSep = new NumberFormatInfo { NumberDecimalSeparator = "," }; + Assert.Equal((BFloat16)1.5f, BFloat16.Parse("0x1,8p0", NumberStyles.HexFloat, commaSep)); + Assert.False(BFloat16.TryParse("0x1.8p0", NumberStyles.HexFloat, commaSep, out _)); + Assert.Equal("1,8p+0", ((BFloat16)1.5f).ToString("x", commaSep)); + NumberFormatTestHelper.TryFormatNumberTest((BFloat16)1.5f, "x", commaSep, "1,8p+0", formatCasingMatchesOutput: false); + + var tildeMinus = new NumberFormatInfo { NegativeSign = "~" }; + Assert.Equal("~1p+0", ((BFloat16)(-1.0f)).ToString("x", tildeMinus)); + NumberFormatTestHelper.TryFormatNumberTest((BFloat16)(-1.0f), "x", tildeMinus, "~1p+0", formatCasingMatchesOutput: false); + } + + [Theory] + [InlineData(NumberStyles.HexFloat | NumberStyles.AllowThousands)] + [InlineData(NumberStyles.HexFloat | NumberStyles.AllowCurrencySymbol)] + [InlineData(NumberStyles.HexFloat | NumberStyles.AllowExponent)] + [InlineData(NumberStyles.AllowBinarySpecifier)] + public static void HexFloat_StyleValidation(NumberStyles style) + { + Assert.Throws(() => BFloat16.Parse("0x1p0", style)); + Assert.Throws(() => BFloat16.TryParse("0x1p0", style, NumberFormatInfo.InvariantInfo, out _)); + } + [Fact] public static void ToString_InvalidFormat_ThrowsFormatException() { diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/SingleTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/SingleTests.cs index fc8f547f15ea45..1f2661817aa8ad 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/SingleTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/SingleTests.cs @@ -330,6 +330,78 @@ public static IEnumerable Parse_Valid_TestData() yield return new object[] { "NaN", NumberStyles.Any, invariantFormat, float.NaN }; yield return new object[] { "Infinity", NumberStyles.Any, invariantFormat, float.PositiveInfinity }; yield return new object[] { "-Infinity", NumberStyles.Any, invariantFormat, float.NegativeInfinity }; + + // Hex float parsing tests + // Basic values + yield return new object[] { "0x1.0p0", NumberStyles.HexFloat, invariantFormat, 1.0f }; + yield return new object[] { "0x1.8p0", NumberStyles.HexFloat, invariantFormat, 1.5f }; + yield return new object[] { "0x1.0p1", NumberStyles.HexFloat, invariantFormat, 2.0f }; + yield return new object[] { "0x1.0p-1", NumberStyles.HexFloat, invariantFormat, 0.5f }; + yield return new object[] { "-0x1.0p0", NumberStyles.HexFloat, invariantFormat, -1.0f }; + yield return new object[] { "+0x1.0p0", NumberStyles.HexFloat, invariantFormat, 1.0f }; + yield return new object[] { "0X1.0P0", NumberStyles.HexFloat, invariantFormat, 1.0f }; + yield return new object[] { "0xAp0", NumberStyles.HexFloat, invariantFormat, 10.0f }; + yield return new object[] { "0x10p0", NumberStyles.HexFloat, invariantFormat, 16.0f }; + + // Fractional-only significand + yield return new object[] { "0x.8p1", NumberStyles.HexFloat, invariantFormat, 1.0f }; + yield return new object[] { "0x.Cp2", NumberStyles.HexFloat, invariantFormat, 3.0f }; + + // Leading zeros + yield return new object[] { "0x001p0", NumberStyles.HexFloat, invariantFormat, 1.0f }; + yield return new object[] { "0x1.00000p0", NumberStyles.HexFloat, invariantFormat, 1.0f }; + + // Edge case values + yield return new object[] { "0x1.0p-149", NumberStyles.HexFloat, invariantFormat, float.Epsilon }; + yield return new object[] { "0x1.fffffep127", NumberStyles.HexFloat, invariantFormat, float.MaxValue }; + yield return new object[] { "-0x1.fffffep127", NumberStyles.HexFloat, invariantFormat, float.MinValue }; + yield return new object[] { "0x1.0p-126", NumberStyles.HexFloat, invariantFormat, 1.17549435E-38f }; // Min normal + yield return new object[] { "0x1.fffffep+127", NumberStyles.HexFloat, invariantFormat, float.MaxValue }; + yield return new object[] { "0x1.000000p-126", NumberStyles.HexFloat, invariantFormat, 1.17549435E-38f }; + + // Zero variants + yield return new object[] { "0x0p0", NumberStyles.HexFloat, invariantFormat, 0.0f }; + yield return new object[] { "-0x0p0", NumberStyles.HexFloat, invariantFormat, -0.0f }; + yield return new object[] { "0x0", NumberStyles.HexFloat, invariantFormat, 0.0f }; + + // Overflow to infinity + yield return new object[] { "0x1.0p128", NumberStyles.HexFloat, invariantFormat, float.PositiveInfinity }; + yield return new object[] { "-0x1.0p128", NumberStyles.HexFloat, invariantFormat, float.NegativeInfinity }; + yield return new object[] { "0x2p+127", NumberStyles.HexFloat, invariantFormat, float.PositiveInfinity }; + yield return new object[] { "0x1.ffffffp+127", NumberStyles.HexFloat, invariantFormat, float.PositiveInfinity }; + + // Underflow to zero + yield return new object[] { "0x1.0p-150", NumberStyles.HexFloat, invariantFormat, 0.0f }; + + // Round-half-even near zero + yield return new object[] { "0x1p-150", NumberStyles.HexFloat, invariantFormat, 0.0f }; // 0.5*Epsilon, ties to even -> 0 + yield return new object[] { "0x3p-150", NumberStyles.HexFloat, invariantFormat, 2.802596928649634e-45f }; // 1.5*Epsilon, ties to even -> 2*Epsilon + yield return new object[] { "0x4p-150", NumberStyles.HexFloat, invariantFormat, 2.802596928649634e-45f }; + yield return new object[] { "0x5p-150", NumberStyles.HexFloat, invariantFormat, 2.802596928649634e-45f }; + yield return new object[] { "0x6p-150", NumberStyles.HexFloat, invariantFormat, 4.203895392974451e-45f }; + + // Pi (various representations) + yield return new object[] { "0x1.921fb6p1", NumberStyles.HexFloat, invariantFormat, 3.14159274f }; + yield return new object[] { "0x3.243f6cp0", NumberStyles.HexFloat, invariantFormat, 3.14159274f }; + yield return new object[] { "0xc.90fdaa22168cp-2", NumberStyles.HexFloat, invariantFormat, 3.14159274f }; + yield return new object[] { "0x.1p4", NumberStyles.HexFloat, invariantFormat, 1.0f }; + + // Whitespace + yield return new object[] { " 0x1.0p0 ", NumberStyles.HexFloat, invariantFormat, 1.0f }; + + // Without prefix + yield return new object[] { "1.0p0", NumberStyles.HexFloat, invariantFormat, 1.0f }; + yield return new object[] { "A", NumberStyles.HexFloat, invariantFormat, 10.0f }; + + // Without exponent (integer-only) + yield return new object[] { "0xA", NumberStyles.HexFloat, invariantFormat, 10.0f }; + yield return new object[] { "0xFF", NumberStyles.HexFloat, invariantFormat, 255.0f }; + + // Denormal + yield return new object[] { "0x0.000002p-126", NumberStyles.HexFloat, invariantFormat, float.Epsilon }; + + // Half max (65504 = 0x1.ffcp15 in half precision, but parsing as float) + yield return new object[] { "0x1.ffcp15", NumberStyles.HexFloat, invariantFormat, 65504.0f }; } [Theory] @@ -753,6 +825,111 @@ private static void ToStringTest(float f, string format, IFormatProvider provide Assert.Equal(expected.Replace('E', 'e'), f.ToString(format.ToLowerInvariant(), provider)); } + [Theory] + // Basic values + [InlineData(1.0f, "x", "1p+0")] + [InlineData(1.5f, "x", "1.8p+0")] + [InlineData(2.0f, "x", "1p+1")] + [InlineData(0.5f, "x", "1p-1")] + [InlineData(-1.0f, "x", "-1p+0")] + [InlineData(10.0f, "x", "1.4p+3")] + [InlineData(0.25f, "x", "1p-2")] + [InlineData(0.1f, "x", "1.99999ap-4")] + [InlineData(MathF.PI, "x", "1.921fb6p+1")] + [InlineData(MathF.E, "x", "1.5bf0a8p+1")] + [InlineData(12345.6789f, "x", "1.81cd6ep+13")] + [InlineData(-1.17549435E-38f, "x", "-1p-126")] + // Zero + [InlineData(0.0f, "x", "0p+0")] + [InlineData(-0.0f, "x", "-0p+0")] + // Special values + [InlineData(float.NaN, "x", "NaN")] + [InlineData(float.PositiveInfinity, "x", "Infinity")] + [InlineData(float.NegativeInfinity, "x", "-Infinity")] + // Edge case values + [InlineData(float.MaxValue, "x", "1.fffffep+127")] + [InlineData(float.MinValue, "x", "-1.fffffep+127")] + [InlineData(float.Epsilon, "x", "1p-149")] + [InlineData(-float.Epsilon, "x", "-1p-149")] + [InlineData(1.17549435E-38f, "x", "1p-126")] // Min normal + // Uppercase + [InlineData(3.25f, "X", "1.AP+1")] + [InlineData(10.0f, "X", "1.4P+3")] + // Precision + [InlineData(1.0f, "x0", "1p+0")] + [InlineData(1.5f, "x1", "1.8p+0")] + [InlineData(1.5f, "x6", "1.800000p+0")] + [InlineData(0.0f, "x3", "0.000p+0")] + // Precision with uppercase + [InlineData(3.25f, "X4", "1.A000P+1")] + // Precision rounding: tie-to-even + [InlineData(1.53125f, "x1", "1.8p+0")] // 1.88p+0 x1: halfway, 8 is even => stays + [InlineData(1.59375f, "x1", "1.ap+0")] // 1.98p+0 x1: halfway, 9 is odd => rounds up + // Precision rounding: carry into leading digit + [InlineData(1.99999988f, "x0", "2p+0")] // ~1.fffffep+0 x0: rounds up to 2 + // Precision rounding: carry through fractional nibbles bumps exponent + [InlineData(1.99999988f, "x1", "1.0p+1")] // x1: round up overflows + // Large precision + [InlineData(1.0f, "x10", "1.0000000000p+0")] + // Zero precision variations + [InlineData(0.0f, "x0", "0p+0")] + [InlineData(-0.0f, "x3", "-0.000p+0")] + public static void ToStringHexFloat(float f, string format, string expected) + { + Assert.Equal(expected, f.ToString(format, NumberFormatInfo.InvariantInfo)); + NumberFormatTestHelper.TryFormatNumberTest(f, format, NumberFormatInfo.InvariantInfo, expected, formatCasingMatchesOutput: false); + + if (!float.IsNaN(f) && !float.IsInfinity(f) && format.Length == 1) + { + float parsed = float.Parse(expected, NumberStyles.HexFloat, NumberFormatInfo.InvariantInfo); + Assert.Equal(f, parsed); + } + } + + [Theory] + [InlineData("-0x0p0")] + [InlineData("-0x0.0p0")] + [InlineData("-0x0")] + public static void ParseHexFloat_NegativeZero(string input) + { + float result = float.Parse(input, NumberStyles.HexFloat, NumberFormatInfo.InvariantInfo); + Assert.True(float.IsNegative(result) && result == 0.0f); + + result = float.Parse(input.AsSpan(), NumberStyles.HexFloat, NumberFormatInfo.InvariantInfo); + Assert.True(float.IsNegative(result) && result == 0.0f); + + result = float.Parse(Encoding.UTF8.GetBytes(input), NumberStyles.HexFloat, NumberFormatInfo.InvariantInfo); + Assert.True(float.IsNegative(result) && result == 0.0f); + + Assert.True(float.TryParse(input, NumberStyles.HexFloat, NumberFormatInfo.InvariantInfo, out result)); + Assert.True(float.IsNegative(result) && result == 0.0f); + } + + [Fact] + public static void HexFloat_CustomNumberFormat() + { + var commaSep = new NumberFormatInfo { NumberDecimalSeparator = "," }; + Assert.Equal(1.5f, float.Parse("0x1,8p0", NumberStyles.HexFloat, commaSep)); + Assert.False(float.TryParse("0x1.8p0", NumberStyles.HexFloat, commaSep, out _)); + Assert.Equal("1,8p+0", 1.5f.ToString("x", commaSep)); + NumberFormatTestHelper.TryFormatNumberTest(1.5f, "x", commaSep, "1,8p+0", formatCasingMatchesOutput: false); + + var tildeMinus = new NumberFormatInfo { NegativeSign = "~" }; + Assert.Equal("~1p+0", (-1.0f).ToString("x", tildeMinus)); + NumberFormatTestHelper.TryFormatNumberTest(-1.0f, "x", tildeMinus, "~1p+0", formatCasingMatchesOutput: false); + } + + [Theory] + [InlineData(NumberStyles.HexFloat | NumberStyles.AllowThousands)] + [InlineData(NumberStyles.HexFloat | NumberStyles.AllowCurrencySymbol)] + [InlineData(NumberStyles.HexFloat | NumberStyles.AllowExponent)] + [InlineData(NumberStyles.AllowBinarySpecifier)] + public static void HexFloat_StyleValidation(NumberStyles style) + { + Assert.Throws(() => float.Parse("0x1p0", style)); + Assert.Throws(() => float.TryParse("0x1p0", style, NumberFormatInfo.InvariantInfo, out _)); + } + [Fact] public static void ToString_InvalidFormat_ThrowsFormatException() { From 5e9249719729c74ba25927fcfecfa2ea2b86b057 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 12 Mar 2026 11:23:59 -0400 Subject: [PATCH 02/11] Address PR review feedback for hex float formatting/parsing - Update HexFloat XML doc to describe the actual accepted syntax including optional 0x prefix and optional p exponent - Fix misleading comment about 0x prefix being consistent with integer hex parsing (integer parsing does not accept 0x) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/Globalization/NumberStyles.cs | 5 ++++- .../System.Private.CoreLib/src/System/Number.Parsing.cs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberStyles.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberStyles.cs index a7bc8000b23545..4319ab1766df6f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberStyles.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberStyles.cs @@ -77,7 +77,10 @@ public enum NumberStyles /// Indicates that the , , /// , , and /// styles are used. This is a composite number style used for parsing hexadecimal floating-point values - /// as defined in IEEE 754:2008 §5.12.3. + /// based on the syntax defined in IEEE 754:2008 §5.12.3. The parsed string can include an optional "0x" + /// or "0X" prefix, a hexadecimal significand with an optional decimal point, and an optional binary + /// exponent introduced by 'p' or 'P'. For compatibility, forms without the "0x"/"0X" prefix are also + /// accepted, and integer-only hexadecimal values may omit the 'p'/'P' exponent. /// HexFloat = AllowLeadingWhite | AllowTrailingWhite | AllowLeadingSign | AllowHexSpecifier | AllowDecimalPoint, 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 c81559f304f820..510d45b71f32d6 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs @@ -1031,7 +1031,7 @@ private static bool TryParseHexFloatingPoint(ReadOnlySpan return false; } - // Skip optional "0x" or "0X" prefix (consistent with integer hex parsing) + // Skip optional "0x" or "0X" prefix to accept both prefixed and non-prefixed forms for round-tripping if (TChar.CastToUInt32(value[index]) == '0' && index + 1 < value.Length && (TChar.CastToUInt32(value[index + 1]) | 0x20) == 'x') From 42697bf363fc94ef861aae78e9775c61f4b6864a Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Fri, 13 Mar 2026 23:29:59 -0600 Subject: [PATCH 03/11] Add additional hex float edge case tests - Zero with extreme exponents (0x0p99999, 0x0p-99999) - Trailing decimal point with no fractional digits (0x1.p0) - Embedded whitespace after 0x prefix (parse rejection) - Decimal point rejected when AllowDecimalPoint not in style - Precision rounding at MaxValue boundary (x0 and x1) - Multi-character decimal separator (::) - Style validation for AllowTrailingSign and AllowParentheses Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../System/DoubleTests.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs index 5281afe19f5816..e3dc778dccd944 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs @@ -486,6 +486,13 @@ public static IEnumerable Parse_Valid_TestData() // HexFloat without AllowDecimalPoint should parse integers only yield return new object[] { "0xFF", NumberStyles.AllowHexSpecifier | NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite, invariantFormat, 255.0 }; yield return new object[] { "A", NumberStyles.AllowHexSpecifier, invariantFormat, 10.0 }; + + // Zero with absurd exponent (still zero) + yield return new object[] { "0x0p99999", NumberStyles.HexFloat, invariantFormat, 0.0 }; + yield return new object[] { "0x0p-99999", NumberStyles.HexFloat, invariantFormat, 0.0 }; + + // Trailing decimal point with no fractional digits + yield return new object[] { "0x1.p0", NumberStyles.HexFloat, invariantFormat, 1.0 }; } [Theory] @@ -635,6 +642,8 @@ public static IEnumerable Parse_Invalid_TestData() yield return new object[] { "0x1pa", NumberStyles.HexFloat, null, typeof(FormatException) }; // non-digit in exponent yield return new object[] { "xyz", NumberStyles.HexFloat, null, typeof(FormatException) }; // no hex digits yield return new object[] { "0x1.0.p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // double decimal point + yield return new object[] { "0x 1p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // embedded whitespace after prefix + yield return new object[] { "0x1.8p0", NumberStyles.AllowHexSpecifier, null, typeof(FormatException) }; // decimal point not allowed without AllowDecimalPoint } [Theory] @@ -1086,6 +1095,9 @@ public static void ToString_ValidLargeFormat() [InlineData(0.0, "x20", "0.00000000000000000000p+0")] [InlineData(-0.0, "x0", "-0p+0")] [InlineData(-0.0, "x3", "-0.000p+0")] + // Precision rounding at MaxValue boundary (non-round-trippable: result exceeds double range) + [InlineData(double.MaxValue, "x0", "2p+1023")] + [InlineData(double.MaxValue, "x1", "1.0p+1024")] public static void ToStringHexFloat(double d, string format, string expected) { Assert.Equal(expected, d.ToString(format, NumberFormatInfo.InvariantInfo)); @@ -1129,12 +1141,21 @@ public static void HexFloat_CustomNumberFormat() var tildeMinus = new NumberFormatInfo { NegativeSign = "~" }; Assert.Equal("~1p+0", (-1.0).ToString("x", tildeMinus)); NumberFormatTestHelper.TryFormatNumberTest(-1.0, "x", tildeMinus, "~1p+0", formatCasingMatchesOutput: false); + + // Multi-character decimal separator + var multiSep = new NumberFormatInfo { NumberDecimalSeparator = "::" }; + Assert.Equal(1.5, double.Parse("0x1::8p0", NumberStyles.HexFloat, multiSep)); + Assert.False(double.TryParse("0x1.8p0", NumberStyles.HexFloat, multiSep, out _)); + Assert.Equal("1::8p+0", 1.5.ToString("x", multiSep)); + NumberFormatTestHelper.TryFormatNumberTest(1.5, "x", multiSep, "1::8p+0", formatCasingMatchesOutput: false); } [Theory] [InlineData(NumberStyles.HexFloat | NumberStyles.AllowThousands)] [InlineData(NumberStyles.HexFloat | NumberStyles.AllowCurrencySymbol)] [InlineData(NumberStyles.HexFloat | NumberStyles.AllowExponent)] + [InlineData(NumberStyles.HexFloat | NumberStyles.AllowTrailingSign)] + [InlineData(NumberStyles.HexFloat | NumberStyles.AllowParentheses)] [InlineData(NumberStyles.AllowBinarySpecifier)] public static void HexFloat_StyleValidation(NumberStyles style) { From acc5e03208263c2aed64e2d002792ad3a1b29b94 Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Fri, 13 Mar 2026 23:58:28 -0600 Subject: [PATCH 04/11] Clean up hex float formatting: remove orphaned string, add assertions and doc comments - Remove orphaned Arg_HexBinaryStylesNotSupported resource string (replaced by Arg_BinaryStyleNotSupported and Arg_InvalidHexFloatStyle, no longer referenced) - Add Debug.Assert(TNumber.IsFinite(value)) at FormatFloatingPointAsHex entry to guard against future callers passing NaN/Infinity - Add remarks on NumberStyles.HexFloat documenting the 0x prefix asymmetry with HexNumber for integer types - Add comment clarifying that the exponent sign in p+/-N is always ASCII per IEEE 754, independent of NumberFormatInfo Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../System.Private.CoreLib/src/Resources/Strings.resx | 3 --- .../src/System/Globalization/NumberStyles.cs | 5 +++++ .../System.Private.CoreLib/src/System/Number.Formatting.cs | 3 +++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index 2a5ce88d6022dc..ea04918cf9e532 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -403,9 +403,6 @@ Byte array for Guid must be exactly {0} bytes long. - - The number styles AllowHexSpecifier and AllowBinarySpecifier are not supported on floating point data types. - The number style AllowBinarySpecifier is not supported on floating point data types. diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberStyles.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberStyles.cs index 4319ab1766df6f..94ea67a990abd6 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberStyles.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberStyles.cs @@ -82,6 +82,11 @@ public enum NumberStyles /// exponent introduced by 'p' or 'P'. For compatibility, forms without the "0x"/"0X" prefix are also /// accepted, and integer-only hexadecimal values may omit the 'p'/'P' exponent. /// + /// + /// Note that unlike for integer types (which rejects a "0x"/"0X" prefix), + /// accepts and ignores the prefix. This difference exists because the + /// IEEE 754 hex float grammar (e.g., 0x1.921fb54442d18p+1) naturally includes the prefix. + /// HexFloat = AllowLeadingWhite | AllowTrailingWhite | AllowLeadingSign | AllowHexSpecifier | AllowDecimalPoint, Currency = AllowLeadingWhite | AllowTrailingWhite | AllowLeadingSign | AllowTrailingSign | 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 dd8c1130109410..1f185d70cccf94 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs @@ -545,6 +545,7 @@ private static unsafe void FormatFloatingPointAsHex(ref ValueLis where TChar : unmanaged, IUtfChar { Debug.Assert((fmt | 0x20) == 'x'); + Debug.Assert(TNumber.IsFinite(value)); bool isNegative = TNumber.IsNegative(value); @@ -713,6 +714,8 @@ private static unsafe void FormatFloatingPointAsHex(ref ValueLis } // Emit exponent: p+NNN or p-NNN + // The exponent sign is always ASCII '+'/'-' per IEEE 754 §5.12.3, + // independent of NumberFormatInfo (which only governs the leading value sign). vlb.Append(TChar.CastFrom(fmt == 'X' ? 'P' : 'p')); if (actualExponent >= 0) From f23c2692c912eb323dfaadb4295f983e1577ef82 Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Fri, 13 Mar 2026 23:58:59 -0600 Subject: [PATCH 05/11] Remove unreachable shiftRight == 0 branch in hex float parsing shiftRight = 63 - mantissaBits, where mantissaBits is the denormal mantissa bit count for each IEEE float type: double (52), float (23), Half (10), BFloat16 (7). The minimum value is 63 - 52 = 11 for double. Even with denormal adjustment, the early return at line 1247 prevents shiftRight from exceeding 64. The shiftRight == 0 branch was therefore dead code for all supported float types. Added a Debug.Assert documenting the invariant. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../System.Private.CoreLib/src/System/Number.Parsing.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 510d45b71f32d6..1eb16d30d4033e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs @@ -1236,6 +1236,7 @@ private static bool TryParseHexFloatingPoint(ReadOnlySpan } int shiftRight = 63 - mantissaBits; + Debug.Assert(shiftRight >= 11, "shiftRight is always >= 11 for all IEEE float types (double: 11, float: 40, Half: 53, BFloat16: 56)"); long biasedExp = actualExp + TFloat.ExponentBias; if (biasedExp <= 0) @@ -1298,10 +1299,8 @@ private static bool TryParseHexFloatingPoint(ReadOnlySpan } } } - else if (shiftRight == 0) - { - mantissa = significand; - } + // shiftRight > 64 is impossible: max is 63 - 7 + denormalShift, capped by the + // early return when denormalShift > 64 - shiftRight. mantissa &= TFloat.DenormalMantissaMask; From d794f24f60257b09cefeec8444c07895cfb9e612 Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Sat, 14 Mar 2026 00:19:17 -0600 Subject: [PATCH 06/11] Add Debug.Assert for bitsToDiscard range in hex float formatter The bitsToDiscard value (mantissaBits - precision * 4) is always in the range (0, mantissaBits) at this point in the code because: - precision >= 1 (we are in the 'precision > 0' branch) - precision < defaultHexDigits (checked on the preceding line) - mantissaBits <= 52 for all IEEE float types, so bitsToDiscard < 64 The existing 'if (bitsToDiscard > 0 && bitsToDiscard < 64)' guard and its else branch are therefore always true / unreachable respectively. Adding an assertion documents this invariant; the runtime guard is retained for defense in depth. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../System.Private.CoreLib/src/System/Number.Formatting.cs | 5 +++++ 1 file changed, 5 insertions(+) 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 1f185d70cccf94..2cbf371f7fcba5 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs @@ -635,6 +635,11 @@ private static unsafe void FormatFloatingPointAsHex(ref ValueLis int bitsToKeep = precision * 4; int bitsToDiscard = mantissaBits - bitsToKeep; + // bitsToDiscard is always in (0, mantissaBits) here because precision >= 1 + // (we're in the precision > 0 branch) and precision < defaultHexDigits + // (checked above), so bitsToKeep < mantissaBits and bitsToDiscard > 0. + // For all IEEE types mantissaBits <= 52, so bitsToDiscard < 64. + Debug.Assert(bitsToDiscard > 0 && bitsToDiscard < 64); if (bitsToDiscard > 0 && bitsToDiscard < 64) { ulong roundBit = 1UL << (bitsToDiscard - 1); From 4d8aa3422eedb58f253c5f058c69ec62344d17ae Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Sat, 14 Mar 2026 00:20:01 -0600 Subject: [PATCH 07/11] Clarify significand accumulation guards in hex float parser Add comments explaining two subtle aspects of the significand accumulation loops in TryParseHexFloatingPoint: 1. The '|| significand == 0' clause in the guard condition is defensive: significandDigits only increments when a nonzero digit is accumulated (or significand is already nonzero), so significandDigits >= 16 implies significand != 0. The guard is retained for safety. 2. Discarded fractional digits intentionally do NOT increment fractionalDigitsConsumed. Those digits are beyond the 64-bit significand precision and only contribute sticky bits for IEEE 754 rounding. Counting them would incorrectly shift the binary exponent by 4 per discarded digit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../System.Private.CoreLib/src/System/Number.Parsing.cs | 6 ++++++ 1 file changed, 6 insertions(+) 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 1eb16d30d4033e..30ebc34c98f8a7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs @@ -1065,6 +1065,9 @@ private static bool TryParseHexFloatingPoint(ReadOnlySpan break; } + // Accumulate up to 16 significant hex digits. The '|| significand == 0' is + // a defensive check: significandDigits only increments when a nonzero digit is + // accumulated, so significandDigits >= 16 implies significand != 0 in practice. if (significandDigits < 16 || significand == 0) { if (significand != 0 || digit != 0) @@ -1104,6 +1107,9 @@ private static bool TryParseHexFloatingPoint(ReadOnlySpan break; } + // Accumulate significant digits (see integer loop comment for '|| significand == 0'). + // Discarded fractional digits intentionally do NOT increment fractionalDigitsConsumed: + // they are beyond significand precision and only contribute sticky bits for rounding. if (significandDigits < 16 || significand == 0) { if (significand != 0 || digit != 0) From 9de6fd12b443c7c1e80975e0eaa421515ebd3e7d Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Sat, 14 Mar 2026 00:44:28 -0600 Subject: [PATCH 08/11] Assert shiftRight range after rounding logic in hex float parser Documents the invariant that shiftRight is always in [11, 64] at the point after the rounding branches: the if/else-if chain covers (0, 64) and == 64 exactly, and shiftRight can never be 0 (min 11 for double) or > 64 (capped by the early return when denormalShift exceeds 64 - shiftRight). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../System.Private.CoreLib/src/System/Number.Parsing.cs | 2 ++ 1 file changed, 2 insertions(+) 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 30ebc34c98f8a7..dc82673363d99f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs @@ -1307,6 +1307,8 @@ private static bool TryParseHexFloatingPoint(ReadOnlySpan } // shiftRight > 64 is impossible: max is 63 - 7 + denormalShift, capped by the // early return when denormalShift > 64 - shiftRight. + // shiftRight == 0 is impossible: minimum is 63 - 52 = 11 (for double), see assert above. + Debug.Assert(shiftRight > 0 && shiftRight <= 64); mantissa &= TFloat.DenormalMantissaMask; From 4ce422a3f2bdf2b7f33201887b0dbfaf9f27e629 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Sat, 14 Mar 2026 19:46:57 -0400 Subject: [PATCH 09/11] Emit 0x prefix --- .../src/System/Number.Formatting.cs | 3 + .../System/DoubleTests.cs | 114 +++++++++--------- .../System.Runtime.Tests/System/HalfTests.cs | 56 ++++----- .../System/Numerics/BFloat16Tests.cs | 38 +++--- .../System/SingleTests.cs | 74 ++++++------ 5 files changed, 144 insertions(+), 141 deletions(-) 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 2cbf371f7fcba5..50757186a274ed 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs @@ -554,6 +554,9 @@ private static unsafe void FormatFloatingPointAsHex(ref ValueLis vlb.Append(info.NegativeSignTChar()); } + vlb.Append(TChar.CastFrom('0')); + vlb.Append(TChar.CastFrom(fmt)); + ulong fraction = ExtractFractionAndBiasedExponent(value, out int exponent); if (fraction == 0) diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs index e3dc778dccd944..801b0baf028b92 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs @@ -1032,72 +1032,72 @@ public static void ToString_ValidLargeFormat() [Theory] // Basic values - [InlineData(1.0, "x", "1p+0")] - [InlineData(1.5, "x", "1.8p+0")] - [InlineData(2.0, "x", "1p+1")] - [InlineData(0.5, "x", "1p-1")] - [InlineData(-1.0, "x", "-1p+0")] - [InlineData(10.0, "x", "1.4p+3")] - [InlineData(0.25, "x", "1p-2")] - [InlineData(0.75, "x", "1.8p-1")] - [InlineData(100.0, "x", "1.9p+6")] - [InlineData(0.1, "x", "1.999999999999ap-4")] - [InlineData(0.3, "x", "1.3333333333333p-2")] - [InlineData(Math.PI, "x", "1.921fb54442d18p+1")] - [InlineData(Math.E, "x", "1.5bf0a8b145769p+1")] - [InlineData(1234567890.123456, "x", "1.26580b487e6b4p+30")] - [InlineData(-2.2250738585072014E-308, "x", "-1p-1022")] + [InlineData(1.0, "x", "0x1p+0")] + [InlineData(1.5, "x", "0x1.8p+0")] + [InlineData(2.0, "x", "0x1p+1")] + [InlineData(0.5, "x", "0x1p-1")] + [InlineData(-1.0, "x", "-0x1p+0")] + [InlineData(10.0, "x", "0x1.4p+3")] + [InlineData(0.25, "x", "0x1p-2")] + [InlineData(0.75, "x", "0x1.8p-1")] + [InlineData(100.0, "x", "0x1.9p+6")] + [InlineData(0.1, "x", "0x1.999999999999ap-4")] + [InlineData(0.3, "x", "0x1.3333333333333p-2")] + [InlineData(Math.PI, "x", "0x1.921fb54442d18p+1")] + [InlineData(Math.E, "x", "0x1.5bf0a8b145769p+1")] + [InlineData(1234567890.123456, "x", "0x1.26580b487e6b4p+30")] + [InlineData(-2.2250738585072014E-308, "x", "-0x1p-1022")] // Zero - [InlineData(0.0, "x", "0p+0")] - [InlineData(-0.0, "x", "-0p+0")] + [InlineData(0.0, "x", "0x0p+0")] + [InlineData(-0.0, "x", "-0x0p+0")] // Special values [InlineData(double.NaN, "x", "NaN")] [InlineData(double.PositiveInfinity, "x", "Infinity")] [InlineData(double.NegativeInfinity, "x", "-Infinity")] // Edge case values - [InlineData(double.MaxValue, "x", "1.fffffffffffffp+1023")] - [InlineData(double.MinValue, "x", "-1.fffffffffffffp+1023")] - [InlineData(double.Epsilon, "x", "1p-1074")] - [InlineData(-double.Epsilon, "x", "-1p-1074")] - [InlineData(2.2250738585072009E-308, "x", "1.ffffffffffffep-1023")] // Max subnormal - [InlineData(1.48219693752374E-323, "x", "1.8p-1073")] // 3 * Epsilon - [InlineData(2.2250738585072014E-308, "x", "1p-1022")] // Min normal + [InlineData(double.MaxValue, "x", "0x1.fffffffffffffp+1023")] + [InlineData(double.MinValue, "x", "-0x1.fffffffffffffp+1023")] + [InlineData(double.Epsilon, "x", "0x1p-1074")] + [InlineData(-double.Epsilon, "x", "-0x1p-1074")] + [InlineData(2.2250738585072009E-308, "x", "0x1.ffffffffffffep-1023")] // Max subnormal + [InlineData(1.48219693752374E-323, "x", "0x1.8p-1073")] // 3 * Epsilon + [InlineData(2.2250738585072014E-308, "x", "0x1p-1022")] // Min normal // Uppercase - [InlineData(3.25, "X", "1.AP+1")] - [InlineData(10.0, "X", "1.4P+3")] - [InlineData(255.5, "X", "1.FFP+7")] + [InlineData(3.25, "X", "0X1.AP+1")] + [InlineData(10.0, "X", "0X1.4P+3")] + [InlineData(255.5, "X", "0X1.FFP+7")] // Explicit precision - [InlineData(1.0, "x0", "1p+0")] - [InlineData(1.0, "x2", "1.00p+0")] - [InlineData(1.5, "x4", "1.8000p+0")] - [InlineData(1.5, "x13", "1.8000000000000p+0")] - [InlineData(1.5, "x1", "1.8p+0")] - [InlineData(0.1, "x1", "1.ap-4")] // 0.1 = 1.999999999999a, round at 1 digit: 0x19... > 0x10, round up - [InlineData(1.0, "x15", "1.000000000000000p+0")] // precision > default (13) + [InlineData(1.0, "x0", "0x1p+0")] + [InlineData(1.0, "x2", "0x1.00p+0")] + [InlineData(1.5, "x4", "0x1.8000p+0")] + [InlineData(1.5, "x13", "0x1.8000000000000p+0")] + [InlineData(1.5, "x1", "0x1.8p+0")] + [InlineData(0.1, "x1", "0x1.ap-4")] // 0.1 = 1.999999999999a, round at 1 digit: 0x19... > 0x10, round up + [InlineData(1.0, "x15", "0x1.000000000000000p+0")] // precision > default (13) // Precision with uppercase - [InlineData(3.25, "X4", "1.A000P+1")] - [InlineData(1.5, "X0", "2P+0")] + [InlineData(3.25, "X4", "0X1.A000P+1")] + [InlineData(1.5, "X0", "0X2P+0")] // Precision rounding: tie-to-even - [InlineData(1.53125, "x1", "1.8p+0")] // 1.8800...p+0 x1: halfway, 8 is even => stays 8 - [InlineData(1.59375, "x1", "1.ap+0")] // 1.9800...p+0 x1: halfway, 9 is odd => rounds to a - [InlineData(1.5937499999999998, "x1", "1.9p+0")] // just below halfway => truncate - [InlineData(1.5937500000000002, "x1", "1.ap+0")] // just above halfway => round up + [InlineData(1.53125, "x1", "0x1.8p+0")] // 1.8800...p+0 x1: halfway, 8 is even => stays 8 + [InlineData(1.59375, "x1", "0x1.ap+0")] // 1.9800...p+0 x1: halfway, 9 is odd => rounds to a + [InlineData(1.5937499999999998, "x1", "0x1.9p+0")] // just below halfway => truncate + [InlineData(1.5937500000000002, "x1", "0x1.ap+0")] // just above halfway => round up // Precision rounding: carry into leading digit - [InlineData(1.9999999999999998, "x0", "2p+0")] // 1.fffff...p+0 x0: rounds up to 2 + [InlineData(1.9999999999999998, "x0", "0x2p+0")] // 1.fffff...p+0 x0: rounds up to 2 // Precision rounding: carry through all fractional nibbles bumps exponent - [InlineData(1.9999999999999998, "x1", "1.0p+1")] // 1.fffff...p+0 x1: round up overflows => exponent bumps + [InlineData(1.9999999999999998, "x1", "0x1.0p+1")] // 1.fffff...p+0 x1: round up overflows => exponent bumps // Large precision (beyond mantissa bits) - [InlineData(1.0, "x20", "1.00000000000000000000p+0")] - [InlineData(double.Epsilon, "x20", "1.00000000000000000000p-1074")] + [InlineData(1.0, "x20", "0x1.00000000000000000000p+0")] + [InlineData(double.Epsilon, "x20", "0x1.00000000000000000000p-1074")] // Zero with precision - [InlineData(0.0, "x3", "0.000p+0")] - [InlineData(0.0, "x0", "0p+0")] - [InlineData(0.0, "x20", "0.00000000000000000000p+0")] - [InlineData(-0.0, "x0", "-0p+0")] - [InlineData(-0.0, "x3", "-0.000p+0")] + [InlineData(0.0, "x3", "0x0.000p+0")] + [InlineData(0.0, "x0", "0x0p+0")] + [InlineData(0.0, "x20", "0x0.00000000000000000000p+0")] + [InlineData(-0.0, "x0", "-0x0p+0")] + [InlineData(-0.0, "x3", "-0x0.000p+0")] // Precision rounding at MaxValue boundary (non-round-trippable: result exceeds double range) - [InlineData(double.MaxValue, "x0", "2p+1023")] - [InlineData(double.MaxValue, "x1", "1.0p+1024")] + [InlineData(double.MaxValue, "x0", "0x2p+1023")] + [InlineData(double.MaxValue, "x1", "0x1.0p+1024")] public static void ToStringHexFloat(double d, string format, string expected) { Assert.Equal(expected, d.ToString(format, NumberFormatInfo.InvariantInfo)); @@ -1135,19 +1135,19 @@ public static void HexFloat_CustomNumberFormat() var commaSep = new NumberFormatInfo { NumberDecimalSeparator = "," }; Assert.Equal(1.5, double.Parse("0x1,8p0", NumberStyles.HexFloat, commaSep)); Assert.False(double.TryParse("0x1.8p0", NumberStyles.HexFloat, commaSep, out _)); - Assert.Equal("1,8p+0", 1.5.ToString("x", commaSep)); - NumberFormatTestHelper.TryFormatNumberTest(1.5, "x", commaSep, "1,8p+0", formatCasingMatchesOutput: false); + Assert.Equal("0x1,8p+0", 1.5.ToString("x", commaSep)); + NumberFormatTestHelper.TryFormatNumberTest(1.5, "x", commaSep, "0x1,8p+0", formatCasingMatchesOutput: false); var tildeMinus = new NumberFormatInfo { NegativeSign = "~" }; - Assert.Equal("~1p+0", (-1.0).ToString("x", tildeMinus)); - NumberFormatTestHelper.TryFormatNumberTest(-1.0, "x", tildeMinus, "~1p+0", formatCasingMatchesOutput: false); + Assert.Equal("~0x1p+0", (-1.0).ToString("x", tildeMinus)); + NumberFormatTestHelper.TryFormatNumberTest(-1.0, "x", tildeMinus, "~0x1p+0", formatCasingMatchesOutput: false); // Multi-character decimal separator var multiSep = new NumberFormatInfo { NumberDecimalSeparator = "::" }; Assert.Equal(1.5, double.Parse("0x1::8p0", NumberStyles.HexFloat, multiSep)); Assert.False(double.TryParse("0x1.8p0", NumberStyles.HexFloat, multiSep, out _)); - Assert.Equal("1::8p+0", 1.5.ToString("x", multiSep)); - NumberFormatTestHelper.TryFormatNumberTest(1.5, "x", multiSep, "1::8p+0", formatCasingMatchesOutput: false); + Assert.Equal("0x1::8p+0", 1.5.ToString("x", multiSep)); + NumberFormatTestHelper.TryFormatNumberTest(1.5, "x", multiSep, "0x1::8p+0", formatCasingMatchesOutput: false); } [Theory] diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/HalfTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/HalfTests.cs index ad5b67a3d85c98..3f3a935da94365 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/HalfTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/HalfTests.cs @@ -1096,41 +1096,41 @@ private static void ToStringTest(Half f, string format, IFormatProvider provider } [Theory] - [InlineData(1.0f, "x", "1p+0")] - [InlineData(1.5f, "x", "1.8p+0")] - [InlineData(2.0f, "x", "1p+1")] - [InlineData(0.5f, "x", "1p-1")] - [InlineData(-1.0f, "x", "-1p+0")] - [InlineData(0.0f, "x", "0p+0")] - [InlineData(-0.0f, "x", "-0p+0")] - [InlineData(10.0f, "x", "1.4p+3")] - [InlineData(0.25f, "x", "1p-2")] - [InlineData(0.1f, "x", "1.998p-4")] + [InlineData(1.0f, "x", "0x1p+0")] + [InlineData(1.5f, "x", "0x1.8p+0")] + [InlineData(2.0f, "x", "0x1p+1")] + [InlineData(0.5f, "x", "0x1p-1")] + [InlineData(-1.0f, "x", "-0x1p+0")] + [InlineData(0.0f, "x", "0x0p+0")] + [InlineData(-0.0f, "x", "-0x0p+0")] + [InlineData(10.0f, "x", "0x1.4p+3")] + [InlineData(0.25f, "x", "0x1p-2")] + [InlineData(0.1f, "x", "0x1.998p-4")] // Special values [InlineData(float.NaN, "x", "NaN")] [InlineData(float.PositiveInfinity, "x", "Infinity")] [InlineData(float.NegativeInfinity, "x", "-Infinity")] // Edge case values - [InlineData(65504.0f, "x", "1.ffcp+15")] // Half.MaxValue - [InlineData(-65504.0f, "x", "-1.ffcp+15")] // Half.MinValue - [InlineData(5.9604645E-08f, "x", "1p-24")] // Half.Epsilon - [InlineData(-5.9604645E-08f, "x", "-1p-24")] // -Half.Epsilon - [InlineData(6.1035156E-05f, "x", "1p-14")] // Min normal + [InlineData(65504.0f, "x", "0x1.ffcp+15")] // Half.MaxValue + [InlineData(-65504.0f, "x", "-0x1.ffcp+15")] // Half.MinValue + [InlineData(5.9604645E-08f, "x", "0x1p-24")] // Half.Epsilon + [InlineData(-5.9604645E-08f, "x", "-0x1p-24")] // -Half.Epsilon + [InlineData(6.1035156E-05f, "x", "0x1p-14")] // Min normal // Uppercase - [InlineData(3.25f, "X", "1.AP+1")] + [InlineData(3.25f, "X", "0X1.AP+1")] // Precision - [InlineData(1.0f, "x0", "1p+0")] - [InlineData(1.5f, "x2", "1.80p+0")] + [InlineData(1.0f, "x0", "0x1p+0")] + [InlineData(1.5f, "x2", "0x1.80p+0")] // Precision with uppercase - [InlineData(3.25f, "X4", "1.A000P+1")] + [InlineData(3.25f, "X4", "0X1.A000P+1")] // Precision rounding: carry into leading digit - [InlineData(1.998f, "x0", "2p+0")] // (Half)1.998f ~ 1.ffcp+0, rounds up to 2 + [InlineData(1.998f, "x0", "0x2p+0")] // (Half)1.998f ~ 1.ffcp+0, rounds up to 2 // Large precision - [InlineData(1.0f, "x5", "1.00000p+0")] + [InlineData(1.0f, "x5", "0x1.00000p+0")] // Zero precision - [InlineData(0.0f, "x0", "0p+0")] - [InlineData(0.0f, "x3", "0.000p+0")] - [InlineData(-0.0f, "x0", "-0p+0")] + [InlineData(0.0f, "x0", "0x0p+0")] + [InlineData(0.0f, "x3", "0x0.000p+0")] + [InlineData(-0.0f, "x0", "-0x0p+0")] public static void ToStringHexFloat(float f, string format, string expected) { Half h = (Half)f; @@ -1169,12 +1169,12 @@ public static void HexFloat_CustomNumberFormat() var commaSep = new NumberFormatInfo { NumberDecimalSeparator = "," }; Assert.Equal((Half)1.5f, Half.Parse("0x1,8p0", NumberStyles.HexFloat, commaSep)); Assert.False(Half.TryParse("0x1.8p0", NumberStyles.HexFloat, commaSep, out _)); - Assert.Equal("1,8p+0", ((Half)1.5f).ToString("x", commaSep)); - NumberFormatTestHelper.TryFormatNumberTest((Half)1.5f, "x", commaSep, "1,8p+0", formatCasingMatchesOutput: false); + Assert.Equal("0x1,8p+0", ((Half)1.5f).ToString("x", commaSep)); + NumberFormatTestHelper.TryFormatNumberTest((Half)1.5f, "x", commaSep, "0x1,8p+0", formatCasingMatchesOutput: false); var tildeMinus = new NumberFormatInfo { NegativeSign = "~" }; - Assert.Equal("~1p+0", ((Half)(-1.0f)).ToString("x", tildeMinus)); - NumberFormatTestHelper.TryFormatNumberTest((Half)(-1.0f), "x", tildeMinus, "~1p+0", formatCasingMatchesOutput: false); + Assert.Equal("~0x1p+0", ((Half)(-1.0f)).ToString("x", tildeMinus)); + NumberFormatTestHelper.TryFormatNumberTest((Half)(-1.0f), "x", tildeMinus, "~0x1p+0", formatCasingMatchesOutput: false); } [Theory] diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Numerics/BFloat16Tests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Numerics/BFloat16Tests.cs index d85884c8839f4a..7e035b86e4ab7c 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Numerics/BFloat16Tests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Numerics/BFloat16Tests.cs @@ -1106,27 +1106,27 @@ private static void ToStringTest(BFloat16 f, string format, IFormatProvider prov } [Theory] - [InlineData(1.0f, "x", "1p+0")] - [InlineData(1.5f, "x", "1.8p+0")] - [InlineData(2.0f, "x", "1p+1")] - [InlineData(0.5f, "x", "1p-1")] - [InlineData(-1.0f, "x", "-1p+0")] - [InlineData(0.0f, "x", "0p+0")] - [InlineData(-0.0f, "x", "-0p+0")] - [InlineData(10.0f, "x", "1.4p+3")] - [InlineData(0.25f, "x", "1p-2")] + [InlineData(1.0f, "x", "0x1p+0")] + [InlineData(1.5f, "x", "0x1.8p+0")] + [InlineData(2.0f, "x", "0x1p+1")] + [InlineData(0.5f, "x", "0x1p-1")] + [InlineData(-1.0f, "x", "-0x1p+0")] + [InlineData(0.0f, "x", "0x0p+0")] + [InlineData(-0.0f, "x", "-0x0p+0")] + [InlineData(10.0f, "x", "0x1.4p+3")] + [InlineData(0.25f, "x", "0x1p-2")] // Special values [InlineData(float.NaN, "x", "NaN")] [InlineData(float.PositiveInfinity, "x", "Infinity")] [InlineData(float.NegativeInfinity, "x", "-Infinity")] // Uppercase - [InlineData(3.25f, "X", "1.AP+1")] + [InlineData(3.25f, "X", "0X1.AP+1")] // Precision - [InlineData(1.0f, "x0", "1p+0")] - [InlineData(1.5f, "x2", "1.80p+0")] - [InlineData(0.0f, "x0", "0p+0")] - [InlineData(0.0f, "x3", "0.000p+0")] - [InlineData(-0.0f, "x0", "-0p+0")] + [InlineData(1.0f, "x0", "0x1p+0")] + [InlineData(1.5f, "x2", "0x1.80p+0")] + [InlineData(0.0f, "x0", "0x0p+0")] + [InlineData(0.0f, "x3", "0x0.000p+0")] + [InlineData(-0.0f, "x0", "-0x0p+0")] public static void ToStringHexFloat(float f, string format, string expected) { BFloat16 b = (BFloat16)f; @@ -1165,12 +1165,12 @@ public static void HexFloat_CustomNumberFormat() var commaSep = new NumberFormatInfo { NumberDecimalSeparator = "," }; Assert.Equal((BFloat16)1.5f, BFloat16.Parse("0x1,8p0", NumberStyles.HexFloat, commaSep)); Assert.False(BFloat16.TryParse("0x1.8p0", NumberStyles.HexFloat, commaSep, out _)); - Assert.Equal("1,8p+0", ((BFloat16)1.5f).ToString("x", commaSep)); - NumberFormatTestHelper.TryFormatNumberTest((BFloat16)1.5f, "x", commaSep, "1,8p+0", formatCasingMatchesOutput: false); + Assert.Equal("0x1,8p+0", ((BFloat16)1.5f).ToString("x", commaSep)); + NumberFormatTestHelper.TryFormatNumberTest((BFloat16)1.5f, "x", commaSep, "0x1,8p+0", formatCasingMatchesOutput: false); var tildeMinus = new NumberFormatInfo { NegativeSign = "~" }; - Assert.Equal("~1p+0", ((BFloat16)(-1.0f)).ToString("x", tildeMinus)); - NumberFormatTestHelper.TryFormatNumberTest((BFloat16)(-1.0f), "x", tildeMinus, "~1p+0", formatCasingMatchesOutput: false); + Assert.Equal("~0x1p+0", ((BFloat16)(-1.0f)).ToString("x", tildeMinus)); + NumberFormatTestHelper.TryFormatNumberTest((BFloat16)(-1.0f), "x", tildeMinus, "~0x1p+0", formatCasingMatchesOutput: false); } [Theory] diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/SingleTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/SingleTests.cs index 1f2661817aa8ad..6ab752b6a94006 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/SingleTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/SingleTests.cs @@ -827,53 +827,53 @@ private static void ToStringTest(float f, string format, IFormatProvider provide [Theory] // Basic values - [InlineData(1.0f, "x", "1p+0")] - [InlineData(1.5f, "x", "1.8p+0")] - [InlineData(2.0f, "x", "1p+1")] - [InlineData(0.5f, "x", "1p-1")] - [InlineData(-1.0f, "x", "-1p+0")] - [InlineData(10.0f, "x", "1.4p+3")] - [InlineData(0.25f, "x", "1p-2")] - [InlineData(0.1f, "x", "1.99999ap-4")] - [InlineData(MathF.PI, "x", "1.921fb6p+1")] - [InlineData(MathF.E, "x", "1.5bf0a8p+1")] - [InlineData(12345.6789f, "x", "1.81cd6ep+13")] - [InlineData(-1.17549435E-38f, "x", "-1p-126")] + [InlineData(1.0f, "x", "0x1p+0")] + [InlineData(1.5f, "x", "0x1.8p+0")] + [InlineData(2.0f, "x", "0x1p+1")] + [InlineData(0.5f, "x", "0x1p-1")] + [InlineData(-1.0f, "x", "-0x1p+0")] + [InlineData(10.0f, "x", "0x1.4p+3")] + [InlineData(0.25f, "x", "0x1p-2")] + [InlineData(0.1f, "x", "0x1.99999ap-4")] + [InlineData(MathF.PI, "x", "0x1.921fb6p+1")] + [InlineData(MathF.E, "x", "0x1.5bf0a8p+1")] + [InlineData(12345.6789f, "x", "0x1.81cd6ep+13")] + [InlineData(-1.17549435E-38f, "x", "-0x1p-126")] // Zero - [InlineData(0.0f, "x", "0p+0")] - [InlineData(-0.0f, "x", "-0p+0")] + [InlineData(0.0f, "x", "0x0p+0")] + [InlineData(-0.0f, "x", "-0x0p+0")] // Special values [InlineData(float.NaN, "x", "NaN")] [InlineData(float.PositiveInfinity, "x", "Infinity")] [InlineData(float.NegativeInfinity, "x", "-Infinity")] // Edge case values - [InlineData(float.MaxValue, "x", "1.fffffep+127")] - [InlineData(float.MinValue, "x", "-1.fffffep+127")] - [InlineData(float.Epsilon, "x", "1p-149")] - [InlineData(-float.Epsilon, "x", "-1p-149")] - [InlineData(1.17549435E-38f, "x", "1p-126")] // Min normal + [InlineData(float.MaxValue, "x", "0x1.fffffep+127")] + [InlineData(float.MinValue, "x", "-0x1.fffffep+127")] + [InlineData(float.Epsilon, "x", "0x1p-149")] + [InlineData(-float.Epsilon, "x", "-0x1p-149")] + [InlineData(1.17549435E-38f, "x", "0x1p-126")] // Min normal // Uppercase - [InlineData(3.25f, "X", "1.AP+1")] - [InlineData(10.0f, "X", "1.4P+3")] + [InlineData(3.25f, "X", "0X1.AP+1")] + [InlineData(10.0f, "X", "0X1.4P+3")] // Precision - [InlineData(1.0f, "x0", "1p+0")] - [InlineData(1.5f, "x1", "1.8p+0")] - [InlineData(1.5f, "x6", "1.800000p+0")] - [InlineData(0.0f, "x3", "0.000p+0")] + [InlineData(1.0f, "x0", "0x1p+0")] + [InlineData(1.5f, "x1", "0x1.8p+0")] + [InlineData(1.5f, "x6", "0x1.800000p+0")] + [InlineData(0.0f, "x3", "0x0.000p+0")] // Precision with uppercase - [InlineData(3.25f, "X4", "1.A000P+1")] + [InlineData(3.25f, "X4", "0X1.A000P+1")] // Precision rounding: tie-to-even - [InlineData(1.53125f, "x1", "1.8p+0")] // 1.88p+0 x1: halfway, 8 is even => stays - [InlineData(1.59375f, "x1", "1.ap+0")] // 1.98p+0 x1: halfway, 9 is odd => rounds up + [InlineData(1.53125f, "x1", "0x1.8p+0")] // 1.88p+0 x1: halfway, 8 is even => stays + [InlineData(1.59375f, "x1", "0x1.ap+0")] // 1.98p+0 x1: halfway, 9 is odd => rounds up // Precision rounding: carry into leading digit - [InlineData(1.99999988f, "x0", "2p+0")] // ~1.fffffep+0 x0: rounds up to 2 + [InlineData(1.99999988f, "x0", "0x2p+0")] // ~1.fffffep+0 x0: rounds up to 2 // Precision rounding: carry through fractional nibbles bumps exponent - [InlineData(1.99999988f, "x1", "1.0p+1")] // x1: round up overflows + [InlineData(1.99999988f, "x1", "0x1.0p+1")] // x1: round up overflows // Large precision - [InlineData(1.0f, "x10", "1.0000000000p+0")] + [InlineData(1.0f, "x10", "0x1.0000000000p+0")] // Zero precision variations - [InlineData(0.0f, "x0", "0p+0")] - [InlineData(-0.0f, "x3", "-0.000p+0")] + [InlineData(0.0f, "x0", "0x0p+0")] + [InlineData(-0.0f, "x3", "-0x0.000p+0")] public static void ToStringHexFloat(float f, string format, string expected) { Assert.Equal(expected, f.ToString(format, NumberFormatInfo.InvariantInfo)); @@ -911,12 +911,12 @@ public static void HexFloat_CustomNumberFormat() var commaSep = new NumberFormatInfo { NumberDecimalSeparator = "," }; Assert.Equal(1.5f, float.Parse("0x1,8p0", NumberStyles.HexFloat, commaSep)); Assert.False(float.TryParse("0x1.8p0", NumberStyles.HexFloat, commaSep, out _)); - Assert.Equal("1,8p+0", 1.5f.ToString("x", commaSep)); - NumberFormatTestHelper.TryFormatNumberTest(1.5f, "x", commaSep, "1,8p+0", formatCasingMatchesOutput: false); + Assert.Equal("0x1,8p+0", 1.5f.ToString("x", commaSep)); + NumberFormatTestHelper.TryFormatNumberTest(1.5f, "x", commaSep, "0x1,8p+0", formatCasingMatchesOutput: false); var tildeMinus = new NumberFormatInfo { NegativeSign = "~" }; - Assert.Equal("~1p+0", (-1.0f).ToString("x", tildeMinus)); - NumberFormatTestHelper.TryFormatNumberTest(-1.0f, "x", tildeMinus, "~1p+0", formatCasingMatchesOutput: false); + Assert.Equal("~0x1p+0", (-1.0f).ToString("x", tildeMinus)); + NumberFormatTestHelper.TryFormatNumberTest(-1.0f, "x", tildeMinus, "~0x1p+0", formatCasingMatchesOutput: false); } [Theory] From a72b437c693339ec3af1616f44714fc22a370cc2 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Sat, 14 Mar 2026 20:22:18 -0400 Subject: [PATCH 10/11] Require 0x prefix when parsing --- .../src/System/Number.Parsing.cs | 11 ++++--- .../System/DoubleTests.cs | 3 +- .../System.Runtime.Tests/System/HalfTests.cs | 27 +++++++++++++++ .../System/Numerics/BFloat16Tests.cs | 33 ++++++++++++++----- .../System/SingleTests.cs | 27 +++++++++++++++ 5 files changed, 87 insertions(+), 14 deletions(-) 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 dc82673363d99f..6214ededdcbd1f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs @@ -1031,13 +1031,14 @@ private static bool TryParseHexFloatingPoint(ReadOnlySpan return false; } - // Skip optional "0x" or "0X" prefix to accept both prefixed and non-prefixed forms for round-tripping - if (TChar.CastToUInt32(value[index]) == '0' && - index + 1 < value.Length && - (TChar.CastToUInt32(value[index + 1]) | 0x20) == 'x') + // Require "0x" or "0X" prefix (consistent with IEEE 754 conventions) + if (TChar.CastToUInt32(value[index]) != '0' || + index + 1 >= value.Length || + (TChar.CastToUInt32(value[index + 1]) | 0x20) != 'x') { - index += 2; + return false; } + index += 2; if (index >= value.Length) { diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs index 801b0baf028b92..aa104e6442fcf1 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs @@ -366,7 +366,6 @@ public static IEnumerable Parse_Valid_TestData() yield return new object[] { "0x00p0", NumberStyles.HexFloat, invariantFormat, 0.0 }; yield return new object[] { "0x000.000p0", NumberStyles.HexFloat, invariantFormat, 0.0 }; yield return new object[] { "0x0", NumberStyles.HexFloat, invariantFormat, 0.0 }; - yield return new object[] { "0", NumberStyles.HexFloat, invariantFormat, 0.0 }; yield return new object[] { "-0x0", NumberStyles.HexFloat, invariantFormat, -0.0 }; // Fractional-only significand (no integer part before decimal) @@ -638,6 +637,8 @@ public static IEnumerable Parse_Invalid_TestData() yield return new object[] { "Infinity", NumberStyles.HexFloat, null, typeof(FormatException) }; // Infinity not valid for HexFloat yield return new object[] { "0xX1.0p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // double X yield return new object[] { "x1.0p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // missing 0 before x + yield return new object[] { "0", NumberStyles.HexFloat, null, typeof(FormatException) }; // missing 0x prefix + yield return new object[] { "1p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // missing 0x prefix yield return new object[] { "0x1.0p 0", NumberStyles.HexFloat, null, typeof(FormatException) }; // internal whitespace in exponent yield return new object[] { "0x1pa", NumberStyles.HexFloat, null, typeof(FormatException) }; // non-digit in exponent yield return new object[] { "xyz", NumberStyles.HexFloat, null, typeof(FormatException) }; // no hex digits diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/HalfTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/HalfTests.cs index 3f3a935da94365..f29bb1ae1f7782 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/HalfTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/HalfTests.cs @@ -845,6 +845,33 @@ public static IEnumerable Parse_Invalid_TestData() yield return new object[] { "ab", NumberStyles.None, null, typeof(FormatException) }; // Negative hex value yield return new object[] { " 123 ", NumberStyles.None, null, typeof(FormatException) }; // Trailing and leading whitespace + + // Invalid hex float inputs + yield return new object[] { "", NumberStyles.HexFloat, null, typeof(FormatException) }; // Empty + yield return new object[] { " ", NumberStyles.HexFloat, null, typeof(FormatException) }; // Whitespace only + yield return new object[] { "0x", NumberStyles.HexFloat, null, typeof(FormatException) }; // Prefix only + yield return new object[] { "0x.p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // No significand digits + yield return new object[] { "0x1.0e5", NumberStyles.HexFloat, null, typeof(FormatException) }; // 'e' exponent instead of 'p' + yield return new object[] { "0x1.0p", NumberStyles.HexFloat, null, typeof(FormatException) }; // Exponent marker without digits + yield return new object[] { "0x1.8", NumberStyles.HexFloat, null, typeof(FormatException) }; // Fractional part without exponent + yield return new object[] { "0x.8", NumberStyles.HexFloat, null, typeof(FormatException) }; // Fractional-only without exponent + yield return new object[] { "0xGp0", NumberStyles.HexFloat, null, typeof(FormatException) }; // Invalid hex char + yield return new object[] { "0x1.Gp0", NumberStyles.HexFloat, null, typeof(FormatException) }; // Invalid hex char in fraction + yield return new object[] { "0x1.0p0garbage", NumberStyles.HexFloat, null, typeof(FormatException) }; // Trailing garbage + yield return new object[] { "+-0x1.0p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // Double sign + yield return new object[] { "0x1.0p+-1", NumberStyles.HexFloat, null, typeof(FormatException) }; // Double exponent sign + yield return new object[] { "NaN", NumberStyles.HexFloat, null, typeof(FormatException) }; // NaN not valid for HexFloat + yield return new object[] { "Infinity", NumberStyles.HexFloat, null, typeof(FormatException) }; // Infinity not valid for HexFloat + yield return new object[] { "0xX1.0p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // double X + yield return new object[] { "x1.0p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // missing 0 before x + yield return new object[] { "0", NumberStyles.HexFloat, null, typeof(FormatException) }; // missing 0x prefix + yield return new object[] { "1p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // missing 0x prefix + yield return new object[] { "0x1.0p 0", NumberStyles.HexFloat, null, typeof(FormatException) }; // internal whitespace in exponent + yield return new object[] { "0x1pa", NumberStyles.HexFloat, null, typeof(FormatException) }; // non-digit in exponent + yield return new object[] { "xyz", NumberStyles.HexFloat, null, typeof(FormatException) }; // no hex digits + yield return new object[] { "0x1.0.p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // double decimal point + yield return new object[] { "0x 1p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // embedded whitespace after prefix + yield return new object[] { "0x1.8p0", NumberStyles.AllowHexSpecifier, null, typeof(FormatException) }; // decimal point not allowed without AllowDecimalPoint } [Theory] diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Numerics/BFloat16Tests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Numerics/BFloat16Tests.cs index 7e035b86e4ab7c..6917883f6f4d07 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Numerics/BFloat16Tests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Numerics/BFloat16Tests.cs @@ -848,14 +848,31 @@ public static IEnumerable Parse_Invalid_TestData() yield return new object[] { " 123 ", NumberStyles.None, null, typeof(FormatException) }; // Trailing and leading whitespace // Invalid hex float inputs - yield return new object[] { "", NumberStyles.HexFloat, null, typeof(FormatException) }; - yield return new object[] { "0x", NumberStyles.HexFloat, null, typeof(FormatException) }; - yield return new object[] { "0x.p0", NumberStyles.HexFloat, null, typeof(FormatException) }; - yield return new object[] { "0x1.8", NumberStyles.HexFloat, null, typeof(FormatException) }; - yield return new object[] { "0x1.0p", NumberStyles.HexFloat, null, typeof(FormatException) }; - yield return new object[] { "0xGp0", NumberStyles.HexFloat, null, typeof(FormatException) }; - yield return new object[] { "NaN", NumberStyles.HexFloat, null, typeof(FormatException) }; - yield return new object[] { "Infinity", NumberStyles.HexFloat, null, typeof(FormatException) }; + yield return new object[] { "", NumberStyles.HexFloat, null, typeof(FormatException) }; // Empty + yield return new object[] { " ", NumberStyles.HexFloat, null, typeof(FormatException) }; // Whitespace only + yield return new object[] { "0x", NumberStyles.HexFloat, null, typeof(FormatException) }; // Prefix only + yield return new object[] { "0x.p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // No significand digits + yield return new object[] { "0x1.0e5", NumberStyles.HexFloat, null, typeof(FormatException) }; // 'e' exponent instead of 'p' + yield return new object[] { "0x1.0p", NumberStyles.HexFloat, null, typeof(FormatException) }; // Exponent marker without digits + yield return new object[] { "0x1.8", NumberStyles.HexFloat, null, typeof(FormatException) }; // Fractional part without exponent + yield return new object[] { "0x.8", NumberStyles.HexFloat, null, typeof(FormatException) }; // Fractional-only without exponent + yield return new object[] { "0xGp0", NumberStyles.HexFloat, null, typeof(FormatException) }; // Invalid hex char + yield return new object[] { "0x1.Gp0", NumberStyles.HexFloat, null, typeof(FormatException) }; // Invalid hex char in fraction + yield return new object[] { "0x1.0p0garbage", NumberStyles.HexFloat, null, typeof(FormatException) }; // Trailing garbage + yield return new object[] { "+-0x1.0p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // Double sign + yield return new object[] { "0x1.0p+-1", NumberStyles.HexFloat, null, typeof(FormatException) }; // Double exponent sign + yield return new object[] { "NaN", NumberStyles.HexFloat, null, typeof(FormatException) }; // NaN not valid for HexFloat + yield return new object[] { "Infinity", NumberStyles.HexFloat, null, typeof(FormatException) }; // Infinity not valid for HexFloat + yield return new object[] { "0xX1.0p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // double X + yield return new object[] { "x1.0p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // missing 0 before x + yield return new object[] { "0", NumberStyles.HexFloat, null, typeof(FormatException) }; // missing 0x prefix + yield return new object[] { "1p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // missing 0x prefix + yield return new object[] { "0x1.0p 0", NumberStyles.HexFloat, null, typeof(FormatException) }; // internal whitespace in exponent + yield return new object[] { "0x1pa", NumberStyles.HexFloat, null, typeof(FormatException) }; // non-digit in exponent + yield return new object[] { "xyz", NumberStyles.HexFloat, null, typeof(FormatException) }; // no hex digits + yield return new object[] { "0x1.0.p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // double decimal point + yield return new object[] { "0x 1p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // embedded whitespace after prefix + yield return new object[] { "0x1.8p0", NumberStyles.AllowHexSpecifier, null, typeof(FormatException) }; // decimal point not allowed without AllowDecimalPoint } [Theory] diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/SingleTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/SingleTests.cs index 6ab752b6a94006..f766173ae8b501 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/SingleTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/SingleTests.cs @@ -465,6 +465,33 @@ public static IEnumerable Parse_Invalid_TestData() yield return new object[] { "ab", NumberStyles.None, null, typeof(FormatException) }; // Negative hex value yield return new object[] { " 123 ", NumberStyles.None, null, typeof(FormatException) }; // Trailing and leading whitespace + + // Invalid hex float inputs + yield return new object[] { "", NumberStyles.HexFloat, null, typeof(FormatException) }; // Empty + yield return new object[] { " ", NumberStyles.HexFloat, null, typeof(FormatException) }; // Whitespace only + yield return new object[] { "0x", NumberStyles.HexFloat, null, typeof(FormatException) }; // Prefix only + yield return new object[] { "0x.p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // No significand digits + yield return new object[] { "0x1.0e5", NumberStyles.HexFloat, null, typeof(FormatException) }; // 'e' exponent instead of 'p' + yield return new object[] { "0x1.0p", NumberStyles.HexFloat, null, typeof(FormatException) }; // Exponent marker without digits + yield return new object[] { "0x1.8", NumberStyles.HexFloat, null, typeof(FormatException) }; // Fractional part without exponent + yield return new object[] { "0x.8", NumberStyles.HexFloat, null, typeof(FormatException) }; // Fractional-only without exponent + yield return new object[] { "0xGp0", NumberStyles.HexFloat, null, typeof(FormatException) }; // Invalid hex char + yield return new object[] { "0x1.Gp0", NumberStyles.HexFloat, null, typeof(FormatException) }; // Invalid hex char in fraction + yield return new object[] { "0x1.0p0garbage", NumberStyles.HexFloat, null, typeof(FormatException) }; // Trailing garbage + yield return new object[] { "+-0x1.0p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // Double sign + yield return new object[] { "0x1.0p+-1", NumberStyles.HexFloat, null, typeof(FormatException) }; // Double exponent sign + yield return new object[] { "NaN", NumberStyles.HexFloat, null, typeof(FormatException) }; // NaN not valid for HexFloat + yield return new object[] { "Infinity", NumberStyles.HexFloat, null, typeof(FormatException) }; // Infinity not valid for HexFloat + yield return new object[] { "0xX1.0p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // double X + yield return new object[] { "x1.0p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // missing 0 before x + yield return new object[] { "0", NumberStyles.HexFloat, null, typeof(FormatException) }; // missing 0x prefix + yield return new object[] { "1p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // missing 0x prefix + yield return new object[] { "0x1.0p 0", NumberStyles.HexFloat, null, typeof(FormatException) }; // internal whitespace in exponent + yield return new object[] { "0x1pa", NumberStyles.HexFloat, null, typeof(FormatException) }; // non-digit in exponent + yield return new object[] { "xyz", NumberStyles.HexFloat, null, typeof(FormatException) }; // no hex digits + yield return new object[] { "0x1.0.p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // double decimal point + yield return new object[] { "0x 1p0", NumberStyles.HexFloat, null, typeof(FormatException) }; // embedded whitespace after prefix + yield return new object[] { "0x1.8p0", NumberStyles.AllowHexSpecifier, null, typeof(FormatException) }; // decimal point not allowed without AllowDecimalPoint } [Theory] From 2607848ea10c357175c2c166ec1f63605db5b98b Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Sat, 14 Mar 2026 20:42:30 -0400 Subject: [PATCH 11/11] Fix doc comment --- .../src/System/Globalization/NumberStyles.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberStyles.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberStyles.cs index 94ea67a990abd6..0aa4fa97453696 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberStyles.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberStyles.cs @@ -77,14 +77,13 @@ public enum NumberStyles /// Indicates that the , , /// , , and /// styles are used. This is a composite number style used for parsing hexadecimal floating-point values - /// based on the syntax defined in IEEE 754:2008 §5.12.3. The parsed string can include an optional "0x" - /// or "0X" prefix, a hexadecimal significand with an optional decimal point, and an optional binary - /// exponent introduced by 'p' or 'P'. For compatibility, forms without the "0x"/"0X" prefix are also - /// accepted, and integer-only hexadecimal values may omit the 'p'/'P' exponent. + /// based on the syntax defined in IEEE 754:2008 §5.12.3. The parsed string must include a "0x" or "0X" + /// prefix, followed by a hexadecimal significand with an optional decimal point, and an optional binary + /// exponent introduced by 'p' or 'P'. Integer-only hexadecimal values may omit the 'p'/'P' exponent. /// /// /// Note that unlike for integer types (which rejects a "0x"/"0X" prefix), - /// accepts and ignores the prefix. This difference exists because the + /// requires the prefix. This difference exists because the /// IEEE 754 hex float grammar (e.g., 0x1.921fb54442d18p+1) naturally includes the prefix. /// HexFloat = AllowLeadingWhite | AllowTrailingWhite | AllowLeadingSign | AllowHexSpecifier | AllowDecimalPoint,