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..ea04918cf9e532 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -403,8 +403,11 @@ 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. + + + 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..0aa4fa97453696 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,21 @@ 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 + /// 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), + /// 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, + 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..50757186a274ed 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,210 @@ 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'); + Debug.Assert(TNumber.IsFinite(value)); + + bool isNegative = TNumber.IsNegative(value); + + if (isNegative) + { + vlb.Append(info.NegativeSignTChar()); + } + + vlb.Append(TChar.CastFrom('0')); + vlb.Append(TChar.CastFrom(fmt)); + + 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; + + // 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); + 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 + // 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) + { + 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 +809,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; + } + + // 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') + { + return false; + } + 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; + } + + // 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) + { + 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; + } + + // 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) + { + 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; + 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) + { + 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; + } + } + } + // 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; + + 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..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 @@ -331,6 +331,167 @@ 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[] { "-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 }; + + // 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] @@ -457,6 +618,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] @@ -843,6 +1031,139 @@ public static void ToString_ValidLargeFormat() d.ToString("E00000999999999"); // Should not throw } + [Theory] + // Basic values + [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", "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", "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", "0X1.AP+1")] + [InlineData(10.0, "X", "0X1.4P+3")] + [InlineData(255.5, "X", "0X1.FFP+7")] + // Explicit precision + [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", "0X1.A000P+1")] + [InlineData(1.5, "X0", "0X2P+0")] + // Precision rounding: tie-to-even + [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", "0x2p+0")] // 1.fffff...p+0 x0: rounds up to 2 + // Precision rounding: carry through all fractional nibbles bumps exponent + [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", "0x1.00000000000000000000p+0")] + [InlineData(double.Epsilon, "x20", "0x1.00000000000000000000p-1074")] + // Zero with precision + [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", "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)); + 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("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("~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("0x1::8p+0", 1.5.ToString("x", multiSep)); + NumberFormatTestHelper.TryFormatNumberTest(1.5, "x", multiSep, "0x1::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) + { + 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..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 @@ -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] @@ -795,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] @@ -1045,6 +1122,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", "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", "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", "0X1.AP+1")] + // Precision + [InlineData(1.0f, "x0", "0x1p+0")] + [InlineData(1.5f, "x2", "0x1.80p+0")] + // Precision with uppercase + [InlineData(3.25f, "X4", "0X1.A000P+1")] + // Precision rounding: carry into leading digit + [InlineData(1.998f, "x0", "0x2p+0")] // (Half)1.998f ~ 1.ffcp+0, rounds up to 2 + // Large precision + [InlineData(1.0f, "x5", "0x1.00000p+0")] + // Zero precision + [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; + 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("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("~0x1p+0", ((Half)(-1.0f)).ToString("x", tildeMinus)); + NumberFormatTestHelper.TryFormatNumberTest((Half)(-1.0f), "x", tildeMinus, "~0x1p+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..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 @@ -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,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] @@ -1079,6 +1122,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", "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", "0X1.AP+1")] + // Precision + [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; + 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("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("~0x1p+0", ((BFloat16)(-1.0f)).ToString("x", tildeMinus)); + NumberFormatTestHelper.TryFormatNumberTest((BFloat16)(-1.0f), "x", tildeMinus, "~0x1p+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..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 @@ -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] @@ -393,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] @@ -753,6 +852,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", "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", "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", "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", "0X1.AP+1")] + [InlineData(10.0f, "X", "0X1.4P+3")] + // Precision + [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", "0X1.A000P+1")] + // Precision rounding: tie-to-even + [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", "0x2p+0")] // ~1.fffffep+0 x0: rounds up to 2 + // Precision rounding: carry through fractional nibbles bumps exponent + [InlineData(1.99999988f, "x1", "0x1.0p+1")] // x1: round up overflows + // Large precision + [InlineData(1.0f, "x10", "0x1.0000000000p+0")] + // Zero precision variations + [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)); + 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("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("~0x1p+0", (-1.0f).ToString("x", tildeMinus)); + NumberFormatTestHelper.TryFormatNumberTest(-1.0f, "x", tildeMinus, "~0x1p+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() {