Skip to content

Commit 1bb32cc

Browse files
committed
Fix FbDecFloat explicit conversions from decimal, double and float
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).
1 parent c1cf2d2 commit 1bb32cc

4 files changed

Lines changed: 151 additions & 7 deletions

File tree

‎src/FirebirdSql.Data.FirebirdClient.Tests/FbDecFloat16SupportTests.cs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,20 @@ public override async Task SetUp()
6666
new object[] { new FbDecFloat(1, 384), "1E384" },
6767
new object[] { new FbDecFloat(BigInteger.Parse("100000000000000000000"), -20), "1" },
6868
new object[] { new FbDecFloat(100, -400), "1E-398" },
69+
new object[] { (FbDecFloat)2.0, "2" },
70+
new object[] { (FbDecFloat)1e-20, "1E-20" },
71+
new object[] { (FbDecFloat)2.5f, "2.5" },
72+
new object[] { (FbDecFloat)100m, "100" },
73+
new object[] { (FbDecFloat)1.10m, "1.10" },
6974
};
7075

7176
static readonly object[] UnrepresentableValues = new object[]
7277
{
7378
new FbDecFloat(BigInteger.Parse("30000000000000004"), -17),
7479
new FbDecFloat(1, 385),
7580
new FbDecFloat(1, -399),
81+
(FbDecFloat)(0.1 + 0.2),
82+
(FbDecFloat)decimal.MaxValue,
7683
};
7784

7885
[TestCaseSource(nameof(TestValues))]

‎src/FirebirdSql.Data.FirebirdClient.Tests/FbDecFloat34SupportTests.cs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ public override async Task SetUp()
6666
new object[] { new FbDecFloat(1, 6144), "1E6144" },
6767
new object[] { new FbDecFloat(BigInteger.Parse("12345678901234567890123456789012340"), 0), "12345678901234567890123456789012340" },
6868
new object[] { new FbDecFloat(1000, -6179), "1E-6176" },
69+
new object[] { (FbDecFloat)2.0, "2" },
70+
new object[] { (FbDecFloat)1e-20, "1E-20" },
71+
new object[] { (FbDecFloat)2.5f, "2.5" },
72+
new object[] { (FbDecFloat)(0.1 + 0.2), "0.30000000000000004" },
73+
new object[] { (FbDecFloat)100m, "100" },
74+
new object[] { (FbDecFloat)1.10m, "1.10" },
75+
new object[] { (FbDecFloat)decimal.MaxValue, "79228162514264337593543950335" },
6976
};
7077

7178
static readonly object[] UnrepresentableValues = new object[]

‎src/FirebirdSql.Data.FirebirdClient.Tests/FbDecFloatTypeTests.cs‎

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
//$Authors = Jiri Cincura (jiri@cincura.net)
1717

18+
using System;
19+
using System.Globalization;
1820
using System.Numerics;
1921
using FirebirdSql.Data.TestsBase;
2022
using FirebirdSql.Data.Types;
@@ -72,4 +74,108 @@ public void EqualityFalse(FbDecFloat expected, FbDecFloat actual)
7274
{
7375
Assert.AreNotEqual(expected, actual);
7476
}
77+
78+
static readonly object[] DecimalConversionSource = new object[]
79+
{
80+
new object[] { 0m, "0", 0 },
81+
new object[] { 1m, "1", 0 },
82+
new object[] { 100m, "100", 0 },
83+
new object[] { -5m, "-5", 0 },
84+
new object[] { 1.5m, "15", -1 },
85+
new object[] { 1.10m, "110", -2 },
86+
new object[] { -0.001m, "-1", -3 },
87+
new object[] { decimal.MaxValue, "79228162514264337593543950335", 0 },
88+
new object[] { decimal.MinValue, "-79228162514264337593543950335", 0 },
89+
new object[] { 0.0000000000000000000000000001m, "1", -28 },
90+
new object[] { -7.9228162514264337593543950335m, "-79228162514264337593543950335", -28 },
91+
};
92+
[TestCaseSource(nameof(DecimalConversionSource))]
93+
public void ConvertsDecimalExactly(decimal value, string coefficient, int exponent)
94+
{
95+
var result = (FbDecFloat)value;
96+
Assert.AreEqual(BigInteger.Parse(coefficient), result.Coefficient);
97+
Assert.AreEqual(exponent, result.Exponent);
98+
}
99+
100+
static readonly object[] DoubleConversionSource = new object[]
101+
{
102+
new object[] { 0d, "0", 0 },
103+
new object[] { 2d, "2", 0 },
104+
new object[] { 100d, "100", 0 },
105+
new object[] { 0.1, "1", -1 },
106+
new object[] { -6.6, "-66", -1 },
107+
new object[] { 1e-20, "1", -20 },
108+
new object[] { 1e300, "1", 300 },
109+
new object[] { 0.1 + 0.2, "30000000000000004", -17 },
110+
new object[] { 123456789.123456789, "12345678912345679", -8 },
111+
new object[] { double.MaxValue, "17976931348623157", 292 },
112+
new object[] { double.MinValue, "-17976931348623157", 292 },
113+
new object[] { double.Epsilon, "5", -324 },
114+
};
115+
[TestCaseSource(nameof(DoubleConversionSource))]
116+
public void ConvertsDoubleToShortestRoundTripValue(double value, string coefficient, int exponent)
117+
{
118+
var result = (FbDecFloat)value;
119+
Assert.AreEqual(BigInteger.Parse(coefficient), result.Coefficient);
120+
Assert.AreEqual(exponent, result.Exponent);
121+
}
122+
123+
static readonly object[] SingleConversionSource = new object[]
124+
{
125+
new object[] { 2f, "2", 0 },
126+
new object[] { 0.1f, "1", -1 },
127+
new object[] { -6.6f, "-66", -1 },
128+
new object[] { 1e-20f, "1", -20 },
129+
new object[] { float.MaxValue, "34028235", 31 },
130+
new object[] { float.Epsilon, "1", -45 },
131+
};
132+
[TestCaseSource(nameof(SingleConversionSource))]
133+
public void ConvertsSingleToShortestRoundTripValue(float value, string coefficient, int exponent)
134+
{
135+
var result = (FbDecFloat)value;
136+
Assert.AreEqual(BigInteger.Parse(coefficient), result.Coefficient);
137+
Assert.AreEqual(exponent, result.Exponent);
138+
}
139+
140+
[Test]
141+
public void ConvertsNegativeZero()
142+
{
143+
Assert.AreEqual(FbDecFloat.NegativeZero, (FbDecFloat)(-0d));
144+
Assert.AreEqual(FbDecFloat.NegativeZero, (FbDecFloat)(-0f));
145+
Assert.AreEqual(FbDecFloat.NegativeZero, (FbDecFloat)decimal.Negate(0m));
146+
Assert.AreNotEqual(FbDecFloat.NegativeZero, (FbDecFloat)0d);
147+
Assert.AreNotEqual(FbDecFloat.NegativeZero, (FbDecFloat)0m);
148+
}
149+
150+
[Test]
151+
public void ConvertsSpecialValues()
152+
{
153+
Assert.AreEqual(FbDecFloat.PositiveNaN, (FbDecFloat)double.NaN);
154+
Assert.AreEqual(FbDecFloat.PositiveInfinity, (FbDecFloat)double.PositiveInfinity);
155+
Assert.AreEqual(FbDecFloat.NegativeInfinity, (FbDecFloat)double.NegativeInfinity);
156+
Assert.AreEqual(FbDecFloat.PositiveNaN, (FbDecFloat)float.NaN);
157+
Assert.AreEqual(FbDecFloat.PositiveInfinity, (FbDecFloat)float.PositiveInfinity);
158+
Assert.AreEqual(FbDecFloat.NegativeInfinity, (FbDecFloat)float.NegativeInfinity);
159+
}
160+
161+
[Test]
162+
public void DoubleAndSingleConversionsRoundTrip()
163+
{
164+
var random = new Random(20261107);
165+
for (var i = 0; i < 10_000; i++)
166+
{
167+
var d = BitConverter.Int64BitsToDouble(random.NextInt64());
168+
if (double.IsFinite(d))
169+
{
170+
var result = (FbDecFloat)d;
171+
Assert.AreEqual(d, double.Parse(result.ToString(), CultureInfo.InvariantCulture), $"{d:R} -> {result}");
172+
}
173+
var f = BitConverter.Int32BitsToSingle(random.Next(int.MinValue, int.MaxValue));
174+
if (float.IsFinite(f))
175+
{
176+
var result = (FbDecFloat)f;
177+
Assert.AreEqual(f, float.Parse(result.ToString(), CultureInfo.InvariantCulture), $"{f:R} -> {result}");
178+
}
179+
}
180+
}
75181
}

‎src/FirebirdSql.Data.FirebirdClient/Types/FbDecFloat.cs‎

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,27 @@ public FbDecFloat(BigInteger coefficient, int exponent = 0)
6464
float.NaN => PositiveNaN,
6565
float.PositiveInfinity => PositiveInfinity,
6666
float.NegativeInfinity => NegativeInfinity,
67-
_ => ParseNumber(value, "0.#######"),
67+
// shortest representation that round-trips to the same float
68+
_ => ParseRoundTrip(value.ToString("R", CultureInfo.InvariantCulture)),
6869
};
6970
public static explicit operator FbDecFloat(double value) => value switch
7071
{
7172
double.NaN => PositiveNaN,
7273
double.PositiveInfinity => PositiveInfinity,
7374
double.NegativeInfinity => NegativeInfinity,
74-
_ => ParseNumber(value, "0.###############"),
75+
// shortest representation that round-trips to the same double
76+
_ => ParseRoundTrip(value.ToString("R", CultureInfo.InvariantCulture)),
7577
};
76-
public static explicit operator FbDecFloat(decimal value) => ParseNumber(value, "0.############################");
78+
public static explicit operator FbDecFloat(decimal value)
79+
{
80+
// exact: the decimal's 96-bit integer and scale are the coefficient and (negated) exponent
81+
Span<int> bits = stackalloc int[4];
82+
decimal.GetBits(value, bits);
83+
BigInteger coefficient = ((UInt128)(uint)bits[2] << 64) | ((UInt128)(uint)bits[1] << 32) | (uint)bits[0];
84+
var negative = bits[3] < 0;
85+
var scale = (bits[3] >> 16) & 0xFF;
86+
return new FbDecFloat(DecimalType.Finite, negative, negative ? -coefficient : coefficient, -scale);
87+
}
7788

7889
public override string ToString()
7990
{
@@ -155,10 +166,23 @@ public bool Equals(FbDecFloat other)
155166
return !lhs.Equals(rhs);
156167
}
157168

158-
static FbDecFloat ParseNumber(IFormattable formattable, string format)
169+
// Parses the invariant "R" format of float/double, e.g. "2", "-0", "6.6", "1E-20", "1.7976931348623157E+308".
170+
static FbDecFloat ParseRoundTrip(string s)
159171
{
160-
var s = formattable.ToString(format, CultureInfo.InvariantCulture);
161-
var pos = s.IndexOf('.');
162-
return new FbDecFloat(BigInteger.Parse(s.Remove(pos, 1), CultureInfo.InvariantCulture), -(s.Length - pos - 1));
172+
var exponent = 0;
173+
var exponentPos = s.IndexOf('E');
174+
if (exponentPos >= 0)
175+
{
176+
exponent = int.Parse(s.AsSpan(exponentPos + 1), NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture);
177+
s = s.Substring(0, exponentPos);
178+
}
179+
var pointPos = s.IndexOf('.');
180+
if (pointPos >= 0)
181+
{
182+
exponent -= s.Length - pointPos - 1;
183+
s = s.Remove(pointPos, 1);
184+
}
185+
var coefficient = BigInteger.Parse(s, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture);
186+
return new FbDecFloat(DecimalType.Finite, s[0] == '-', coefficient, exponent);
163187
}
164188
}

0 commit comments

Comments
 (0)