Skip to content

Fix FbDecFloat explicit conversions from decimal, double and float - #1306

Open
fdcastel wants to merge 3 commits into
FirebirdSQL:masterfrom
fdcastel:fix-fbdecfloat-conversions
Open

fdcastel wants to merge 3 commits into
FirebirdSQL:masterfrom
fdcastel:fix-fbdecfloat-conversions

Conversation

@fdcastel

@fdcastel fdcastel commented Nov 7, 2026

Copy link
Copy Markdown
Member

Fixes #1299. Based on #1304: a converted double can have 17 digits, and must throw instead of being corrupted when sent to DECFLOAT(16).

Summary

Fixes the explicit conversions from decimal, double and float to FbDecFloat. They threw for every value without a fractional part, and they lost digits.

Changes

  • decimal: converted exactly from its 96-bit integer and scale, so the scale is kept (1.10m becomes 110E-2).
    • This includes the sign of a negative zero: (FbDecFloat)decimal.Negate(0m) is FbDecFloat.NegativeZero.
  • double and float: converted to the shortest decimal that converts back to the same binary value (the "R" format).
Value Master This PR
1m ArgumentOutOfRangeException 1E0
1.10m 11E-1 110E-2
2.0 ArgumentOutOfRangeException 2E0
1e-20 ArgumentOutOfRangeException 1E-20
6e-16 1E-15 6E-16
0.1 + 0.2 3E-1 30000000000000004E-17
-0.0 ArgumentOutOfRangeException negative zero

Why a 17-digit double throws for DECFLOAT(16)

0.1 + 0.2 converts to 30000000000000004E-17 (17 digits), which does not fit DECFLOAT(16). Sending it to a DECFLOAT(16) parameter therefore throws OverflowException (#1304).

Firebird's own CAST(0.1e0 + 0.2e0 AS DECFLOAT(16)) rounds to 0.3000000000000000; DECFLOAT(34) gives 0.30000000000000004, the same digits as this conversion. The provider deliberately does not round: an explicit conversion of the value would lose information silently. Callers who want the server's rounding can round first, or let the server do it with CAST.

Tests

  • FbDecFloatTypeTests (NoServer):
    • exact coefficient and exponent for decimal, double and float edge values: min, max, epsilon, negative zero, NaN and infinities,
    • a seeded property test: 10,000 random bit patterns per type (all exponents, both signs; the test asserts that more than 1,000 negative values of each type were checked) each convert back to the identical value.
  • FbDecFloat16SupportTests and FbDecFloat34SupportTests: converted values round trip through the server, and 0.1 + 0.2 and decimal.MaxValue sent to DECFLOAT(16) throw OverflowException.
  • Without the fix, 29 tests fail. With it, the embedded + NoServer suite passes: 706 passed, 0 failed.

…rmat

DecimalCodec.EncodeDecimal wrote coefficients with more digits than the
format allows, and exponents outside its range, without any check (only a
Debug.Assert). In Release builds the excess bits overflowed into the
combination/exponent fields and the server stored a different value, e.g.:

  12345678901234567890123456789012345 -> DECFLOAT(34) stored 8345678901234567890123456789012345
  30000000000000004E-17               -> DECFLOAT(16) stored 0.08000000000000004
  1E400                               -> DECFLOAT(16) stored 8.000000000000001E-353

The encoder now brings the value into the format's ranges without changing
its numeric value (dropping trailing zeros of the coefficient, folding the
exponent into the coefficient, clamping the exponent of zero), and throws
OverflowException when the value cannot be represented exactly (too many
significant digits, exponent overflow or underflow). Values that previously
got corrupted although representable (e.g. 1E384 in DECFLOAT(16)) now work.
The conversions formatted the value with a fixed custom format and then
removed the decimal point, assuming one was present. This threw
ArgumentOutOfRangeException for every value without a fractional part,
e.g. (FbDecFloat)1m, (FbDecFloat)100m, (FbDecFloat)2.0, and for values
smaller than the format's decimal places, e.g. (FbDecFloat)1e-20. It also
dropped the scale of decimals (1.10m became 1.1) and digits beyond 15
decimal places of doubles (7 for floats).

- decimal: converted exactly from its 96-bit integer and scale, so the
  value and its scale are preserved (1.10m -> 110E-2), including -0.
- double/float: converted to the shortest decimal that round-trips to the
  same binary value ("R" format), e.g. 0.1 -> 1E-1, 0.1 + 0.2 ->
  30000000000000004E-17, 1e-20 -> 1E-20; -0.0 -> negative zero.

A converted double may have up to 17 significant digits; sending it to a
DECFLOAT(16) throws OverflowException instead of being silently rounded
(this relies on the encoder range check of the previous commit).
Random.NextInt64() never returns a negative value, so the random doubles
were all positive. Use the full long range and assert both signs are covered.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FbDecFloat explicit conversions throw for integral decimal/double values

1 participant