Skip to content

Add support for floating-point hex formatting and parsing#124139

Open
stephentoub wants to merge 1 commit intodotnet:mainfrom
stephentoub:floathex
Open

Add support for floating-point hex formatting and parsing#124139
stephentoub wants to merge 1 commit intodotnet:mainfrom
stephentoub:floathex

Conversation

@stephentoub
Copy link
Member

Closes #1630

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds IEEE-754-style hexadecimal floating-point parsing/formatting support across double, float, Half, and BFloat16, exposing it via a new NumberStyles.HexFloat composite and enabling "x"/"X" format specifiers for floating-point ToString/TryFormat.

Changes:

  • Introduces NumberStyles.HexFloat and updates floating-point parse-style validation to permit hex-float parsing while rejecting incompatible flag combinations.
  • Implements hex-float parsing (p/P binary exponent) and hex-float formatting for "x"/"X" (with optional precision).
  • Adds extensive unit test coverage for parsing/formatting behavior and updates shared formatter tests to no longer treat "X" as invalid for floating-point.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/SingleTests.cs Adds float hex-float parse/format tests, including rounding/edge cases and custom NumberFormatInfo.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Numerics/BFloat16Tests.cs Adds BFloat16 hex-float parse/format tests and invalid-input coverage.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/HalfTests.cs Adds Half hex-float parse/format tests and style-validation checks.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs Adds extensive double hex-float parse/format tests including boundary/rounding behaviors.
src/libraries/System.Runtime/ref/System.Runtime.cs Exposes NumberStyles.HexFloat in the reference assembly surface.
src/libraries/System.Private.CoreLib/src/System/Single.cs Updates UTF-8 span parse entrypoints to use floating-point style validation (enabling hex-float styles).
src/libraries/System.Private.CoreLib/src/System/Numerics/BFloat16.cs Same validation update for BFloat16 UTF-8 span parse entrypoints.
src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs Adds the hex-float parser and routes AllowHexSpecifier float parsing to it.
src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs Adds "x"/"X" hex-float formatting implementation for floating-point types.
src/libraries/System.Private.CoreLib/src/System/Half.cs Updates UTF-8 span parse entrypoints to use floating-point style validation.
src/libraries/System.Private.CoreLib/src/System/Globalization/NumberStyles.cs Adds the HexFloat composite style with XML documentation.
src/libraries/System.Private.CoreLib/src/System/Globalization/NumberFormatInfo.cs Adjusts float style validation and introduces new error messages for invalid hex-float/binary combinations.
src/libraries/System.Private.CoreLib/src/Resources/Strings.resx Adds new resource strings for updated validation exceptions.
src/libraries/Common/tests/System/RealFormatterTestsBase.cs Updates shared invalid-format test data to stop treating "X" formats as invalid for floating-point.

Copilot AI review requested due to automatic review settings February 8, 2026 21:04
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

/// Indicates that the <see cref="AllowLeadingWhite"/>, <see cref="AllowTrailingWhite"/>,
/// <see cref="AllowLeadingSign"/>, <see cref="AllowHexSpecifier"/>, and <see cref="AllowDecimalPoint"/>
/// 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.
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

The XML doc for NumberStyles.HexFloat states it parses values “as defined in IEEE 754:2008 §5.12.3”, but the implementation/tests also accept forms without a 0x prefix (to support round-tripping ToString("x")) and allow integer-only forms without a p exponent. Please update this documentation to describe the supported syntax (including the optional 0x prefix and when the p exponent is required) or tighten the parser to match the IEEE grammar if strict conformance is intended.

Suggested change
/// 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.

Copilot uses AI. Check for mistakes.
Comment on lines +1034 to +1038
// 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')
{
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

The comment says skipping an optional 0x prefix is “consistent with integer hex parsing”, but integer parsing with AllowHexSpecifier does not accept a 0x/0X prefix (callers typically strip it themselves). Please reword this comment to reflect the actual intent (e.g., accepting both prefixed and non-prefixed forms for compatibility/round-tripping).

Copilot uses AI. Check for mistakes.
@stephentoub
Copy link
Member Author

🤖 Copilot Code Review — PR #124139

Holistic Assessment

Motivation: Strongly justified — this closes a long-standing api-approved issue (#1630) to add IEEE 754:2008 §5.12.3 hex float formatting/parsing support. This is a standard feature in C/C++ (%a/strtod) and a notable gap in .NET's numeric formatting.

Approach: Clean and well-scoped. The implementation adds a dedicated FormatFloatingPointAsHex method for formatting and TryParseHexFloatingPoint for parsing, both integrated into the existing Number class infrastructure. The generic TNumber/TFloat pattern reuses the existing IBinaryFloatParseAndFormatInfo<T> interface, ensuring all four float types (double, float, Half, BFloat16) are covered uniformly.

Net positive: This is a clear net positive — it fills a real API gap with a correct, well-tested implementation that follows established patterns.


Detailed Findings

(Multi-model review: Claude Sonnet 4.5 (primary) + Gemini 3 Pro (secondary). GPT-5.1 timed out and did not contribute. Both contributing models agree on all major points.)

✅ Formatting correctness — verified

Traced through several representative cases:

  • Normal values (e.g., Math.PI): correctly extracts mantissa bits, computes actualExponent, and emits MSB-first nibbles.
  • Denormals (e.g., double.Epsilon, max subnormal 2.2250738585072009E-308): correctly shifts the leading 1 to the implicit bit position, adjusts the exponent, and emits the right number of significant nibbles. Verified double.Epsilon1p-1074 and max subnormal → 1.ffffffffffffep-1023.
  • Zero (+0 and -0): handled as a special case before normalization, correctly emits sign + 0p+0 with optional precision padding.
  • Precision rounding: rounds-to-even correctly. Carry propagation through fractional nibbles into the exponent (e.g., 1.fffff...p+0 at x1 → 1.0p+1) works.
  • Precision 0: leading digit can become 2 (e.g., 2p+0 instead of 1p+1), which is valid per C99's %a behavior at %.0a. Values are mathematically correct.

NaN/Infinity are handled by the existing pre-check in the shared formatting helper (TNumber.IsFinite(value)) before the hex format branch is reached. Both the ToString and TryFormat paths go through the same private FormatFloat<TNumber, TChar> helper, so both are covered.

✅ Parsing correctness — verified

  • Normal values, denormals, overflow/underflow: correctly computes the effective binary exponent (binaryExponent - 4*fractionalDigitsConsumed + 4*overflowIntegerDigits), normalizes the significand to bit 63, and constructs the IEEE 754 bit pattern via BitsToFloat.
  • Round-to-nearest-even with sticky bit: correctly tracks hasDiscardedNonZeroDigits for discarded hex digits beyond the significand's 16-digit capacity. This is incorporated into the rounding decision at the final step.
  • fractionalDigitsConsumed tracking: Only incremented for digits actually incorporated into the significand (not for discarded overflow digits). This is correct — the exponent formula should only account for digits that contributed to the significand's positional value. Discarded digits are captured only via the sticky bit. Verified with traced examples including 0x0.00000000000000001Ap0 (17 fractional digits).
  • Exponent overflow: saturated at int.MaxValue, then correctly resolved to ±Infinity or ±0 depending on direction.
  • shiftRight == 64 boundary: correctly handled as a separate case for the deepest subnormal rounding scenario.

✅ Validation logic — correct

The updated ValidateParseStyleFloatingPoint correctly:

  • Allows AllowHexSpecifier combined with any subset of {AllowLeadingWhite, AllowTrailingWhite, AllowLeadingSign, AllowDecimalPoint}.
  • Rejects AllowHexSpecifier combined with AllowExponent, AllowThousands, AllowCurrencySymbol, etc.
  • Rejects AllowBinarySpecifier unconditionally for floats (with a new, more specific error message).

✅ Pre-existing bug fix — ValidateParseStyleInteger → ValidateParseStyleFloatingPoint

The UTF-8 Parse(ReadOnlySpan<byte>, ...) and TryParse(ReadOnlySpan<byte>, ...) overloads in Double, Single, Half, and BFloat16 were incorrectly calling ValidateParseStyleInteger instead of ValidateParseStyleFloatingPoint. This is a pre-existing bug (likely a copy-paste error from the integer overloads) that this PR correctly fixes. Good catch bundling this.

✅ Test coverage — thorough

Extensive tests across all four float types covering: basic values, sign handling, case variations, zero variants, fractional-only significand, leading zeros, trailing decimal point, edge cases (min/max normal, min subnormal, Epsilon), well-known constants (π, e), multiple representations of the same value, overflow to infinity, underflow to zero, round-half-even at multiple boundaries, whitespace handling, optional prefix, integer-only form, custom NumberFormatInfo (comma separator, tilde negative sign), and style validation.

Round-trip tests (format then parse) are included for default precision, which is the most important case.

💡 Documentation precision — the existing Copilot review comments about XML doc wording are reasonable

The NumberStyles.HexFloat XML doc references IEEE 754:2008 §5.12.3, but the implementation intentionally accepts a wider syntax (optional 0x prefix, integer-only forms without p exponent). This is clearly intentional for usability and round-trip compatibility, but a brief doc note about the accepted extensions would help users who read the XML docs and expect strict IEEE grammar.

Similarly, the comment "consistent with integer hex parsing" at the 0x prefix skip could be reworded since AllowHexSpecifier for integers does NOT accept 0x. Something like "for convenience and compatibility with C-style hex float literals" would be more accurate.

These are minor doc polish items, not blocking.


Summary

LGTM. This is a well-implemented feature addition that fills a real API gap. The formatting and parsing logic is correct across all traced edge cases including denormals, rounding boundaries, overflow, and underflow. The code integrates cleanly into the existing Number class infrastructure, the validation logic is sound, the pre-existing ValidateParseStyleInteger bug is properly fixed, and test coverage is thorough. The only minor items are the two already-noted documentation wording suggestions.

1 similar comment
@stephentoub
Copy link
Member Author

🤖 Copilot Code Review — PR #124139

Holistic Assessment

Motivation: Strongly justified — this closes a long-standing api-approved issue (#1630) to add IEEE 754:2008 §5.12.3 hex float formatting/parsing support. This is a standard feature in C/C++ (%a/strtod) and a notable gap in .NET's numeric formatting.

Approach: Clean and well-scoped. The implementation adds a dedicated FormatFloatingPointAsHex method for formatting and TryParseHexFloatingPoint for parsing, both integrated into the existing Number class infrastructure. The generic TNumber/TFloat pattern reuses the existing IBinaryFloatParseAndFormatInfo<T> interface, ensuring all four float types (double, float, Half, BFloat16) are covered uniformly.

Net positive: This is a clear net positive — it fills a real API gap with a correct, well-tested implementation that follows established patterns.


Detailed Findings

(Multi-model review: Claude Sonnet 4.5 (primary) + Gemini 3 Pro (secondary). GPT-5.1 timed out and did not contribute. Both contributing models agree on all major points.)

✅ Formatting correctness — verified

Traced through several representative cases:

  • Normal values (e.g., Math.PI): correctly extracts mantissa bits, computes actualExponent, and emits MSB-first nibbles.
  • Denormals (e.g., double.Epsilon, max subnormal 2.2250738585072009E-308): correctly shifts the leading 1 to the implicit bit position, adjusts the exponent, and emits the right number of significant nibbles. Verified double.Epsilon1p-1074 and max subnormal → 1.ffffffffffffep-1023.
  • Zero (+0 and -0): handled as a special case before normalization, correctly emits sign + 0p+0 with optional precision padding.
  • Precision rounding: rounds-to-even correctly. Carry propagation through fractional nibbles into the exponent (e.g., 1.fffff...p+0 at x1 → 1.0p+1) works.
  • Precision 0: leading digit can become 2 (e.g., 2p+0 instead of 1p+1), which is valid per C99's %a behavior at %.0a. Values are mathematically correct.

NaN/Infinity are handled by the existing pre-check in the shared formatting helper (TNumber.IsFinite(value)) before the hex format branch is reached. Both the ToString and TryFormat paths go through the same private FormatFloat<TNumber, TChar> helper, so both are covered.

✅ Parsing correctness — verified

  • Normal values, denormals, overflow/underflow: correctly computes the effective binary exponent (binaryExponent - 4*fractionalDigitsConsumed + 4*overflowIntegerDigits), normalizes the significand to bit 63, and constructs the IEEE 754 bit pattern via BitsToFloat.
  • Round-to-nearest-even with sticky bit: correctly tracks hasDiscardedNonZeroDigits for discarded hex digits beyond the significand's 16-digit capacity. This is incorporated into the rounding decision at the final step.
  • fractionalDigitsConsumed tracking: Only incremented for digits actually incorporated into the significand (not for discarded overflow digits). This is correct — the exponent formula should only account for digits that contributed to the significand's positional value. Discarded digits are captured only via the sticky bit. Verified with traced examples including 0x0.00000000000000001Ap0 (17 fractional digits).
  • Exponent overflow: saturated at int.MaxValue, then correctly resolved to ±Infinity or ±0 depending on direction.
  • shiftRight == 64 boundary: correctly handled as a separate case for the deepest subnormal rounding scenario.

✅ Validation logic — correct

The updated ValidateParseStyleFloatingPoint correctly:

  • Allows AllowHexSpecifier combined with any subset of {AllowLeadingWhite, AllowTrailingWhite, AllowLeadingSign, AllowDecimalPoint}.
  • Rejects AllowHexSpecifier combined with AllowExponent, AllowThousands, AllowCurrencySymbol, etc.
  • Rejects AllowBinarySpecifier unconditionally for floats (with a new, more specific error message).

✅ Pre-existing bug fix — ValidateParseStyleInteger → ValidateParseStyleFloatingPoint

The UTF-8 Parse(ReadOnlySpan<byte>, ...) and TryParse(ReadOnlySpan<byte>, ...) overloads in Double, Single, Half, and BFloat16 were incorrectly calling ValidateParseStyleInteger instead of ValidateParseStyleFloatingPoint. This is a pre-existing bug (likely a copy-paste error from the integer overloads) that this PR correctly fixes. Good catch bundling this.

✅ Test coverage — thorough

Extensive tests across all four float types covering: basic values, sign handling, case variations, zero variants, fractional-only significand, leading zeros, trailing decimal point, edge cases (min/max normal, min subnormal, Epsilon), well-known constants (π, e), multiple representations of the same value, overflow to infinity, underflow to zero, round-half-even at multiple boundaries, whitespace handling, optional prefix, integer-only form, custom NumberFormatInfo (comma separator, tilde negative sign), and style validation.

Round-trip tests (format then parse) are included for default precision, which is the most important case.

💡 Documentation precision — the existing Copilot review comments about XML doc wording are reasonable

The NumberStyles.HexFloat XML doc references IEEE 754:2008 §5.12.3, but the implementation intentionally accepts a wider syntax (optional 0x prefix, integer-only forms without p exponent). This is clearly intentional for usability and round-trip compatibility, but a brief doc note about the accepted extensions would help users who read the XML docs and expect strict IEEE grammar.

Similarly, the comment "consistent with integer hex parsing" at the 0x prefix skip could be reworded since AllowHexSpecifier for integers does NOT accept 0x. Something like "for convenience and compatibility with C-style hex float literals" would be more accurate.

These are minor doc polish items, not blocking.


Summary

LGTM. This is a well-implemented feature addition that fills a real API gap. The formatting and parsing logic is correct across all traced edge cases including denormals, rounding boundaries, overflow, and underflow. The code integrates cleanly into the existing Number class infrastructure, the validation logic is sound, the pre-existing ValidateParseStyleInteger bug is properly fixed, and test coverage is thorough. The only minor items are the two already-noted documentation wording suggestions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add the ability to parse/format a float/double from/to a hexadecimal literal

1 participant