Skip to content

Commit fdd5b06

Browse files
committed
Support .NET 11 Decimal128/Decimal64/Decimal32 for DECFLOAT (net11.0)
.NET 11 introduces the IEEE 754 decimal floating-point types Decimal32, Decimal64 and Decimal128. Their decimal interchange encoding (EncodeDecimal/DecodeDecimal, densely packed decimal) is the encoding of Firebird's DECFLOAT(16)/DECFLOAT(34): the provider's DECFLOAT codec and .NET agree bit for bit (asserted by a seeded test over random values and all special values), so the conversions are exact, including the exponent (1.10 stays 1.10), negative zero, infinities and NaNs, with no text round trip. On net11.0 only: - FbDecFloat: explicit conversions from Decimal32/Decimal64/Decimal128 (exact) and to Decimal64/Decimal128 (exact, OverflowException when the value does not fit). - Parameters: Decimal128 values infer FbDbType.Dec34, Decimal64 and Decimal32 infer FbDbType.Dec16. They can be passed to any numeric parameter; a value that does not fit the target exactly (e.g. a 34-digit Decimal128 into DECFLOAT(16)) throws OverflowException instead of being rounded. - Reader: GetFieldValue<Decimal128>() and GetFieldValue<Decimal64>() for DECFLOAT and the other numeric types (exact, OverflowException when the value does not fit). GetValue()/GetFieldType() still return FbDecFloat for DECFLOAT columns.
1 parent 24faf82 commit fdd5b06

6 files changed

Lines changed: 527 additions & 3 deletions

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
* The contents of this file are subject to the Initial
3+
* Developer's Public License Version 1.0 (the "License");
4+
* you may not use this file except in compliance with the
5+
* License. You may obtain a copy of the License at
6+
* https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt.
7+
*
8+
* Software distributed under the License is distributed on
9+
* an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either
10+
* express or implied. See the License for the specific
11+
* language governing rights and limitations under the License.
12+
*
13+
* All Rights Reserved.
14+
*/
15+
16+
//$Authors = F.D.Castel
17+
18+
#if NET11_0_OR_GREATER
19+
20+
using System;
21+
using System.Buffers.Binary;
22+
using System.Globalization;
23+
using System.Numerics;
24+
using FirebirdSql.Data.Common;
25+
using FirebirdSql.Data.TestsBase;
26+
using FirebirdSql.Data.Types;
27+
using NUnit.Framework;
28+
29+
namespace FirebirdSql.Data.FirebirdClient.Tests;
30+
31+
[NoServerCategory]
32+
public class FbDecFloatIeee754Tests
33+
{
34+
static BigInteger RandomCoefficient(Random random, int maxDigits)
35+
{
36+
var digits = random.Next(0, maxDigits + 1);
37+
if (digits == 0)
38+
return BigInteger.Zero;
39+
var chars = new char[digits];
40+
for (var i = 0; i < digits; i++)
41+
chars[i] = (char)('0' + random.Next(10));
42+
var result = BigInteger.Parse(new string(chars), CultureInfo.InvariantCulture);
43+
return random.Next(2) == 0 ? result : -result;
44+
}
45+
46+
// The provider's DECFLOAT codec and .NET's IEEE 754 decimal interchange encoding (EncodeDecimal/DecodeDecimal)
47+
// must agree bit for bit; the Decimal64/Decimal128 support relies on it.
48+
[Test]
49+
public void DecFloat34CodecMatchesDecimal128Encoding()
50+
{
51+
var random = new Random(20261107);
52+
for (var i = 0; i < 20_000; i++)
53+
{
54+
var value = new FbDecFloat(RandomCoefficient(random, 34), random.Next(-6176, 6111 + 1));
55+
var bits = BinaryPrimitives.ReadUInt128LittleEndian(DecimalCodec.DecFloat34.EncodeDecimal(value));
56+
var d = (Decimal128)value;
57+
Assert.AreEqual(bits, Decimal128.EncodeDecimal(d), value.ToString());
58+
var expected = Decimal128.Parse(value.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture);
59+
Assert.IsTrue(d.Equals(expected) && Decimal128.HaveSameQuantum(d, expected), value.ToString());
60+
var back = (FbDecFloat)d;
61+
Assert.AreEqual(value.Coefficient, back.Coefficient, value.ToString());
62+
Assert.AreEqual(value.Exponent, back.Exponent, value.ToString());
63+
}
64+
}
65+
66+
[Test]
67+
public void DecFloat16CodecMatchesDecimal64Encoding()
68+
{
69+
var random = new Random(20261107);
70+
for (var i = 0; i < 20_000; i++)
71+
{
72+
var value = new FbDecFloat(RandomCoefficient(random, 16), random.Next(-398, 369 + 1));
73+
var bits = BinaryPrimitives.ReadUInt64LittleEndian(DecimalCodec.DecFloat16.EncodeDecimal(value));
74+
var d = (Decimal64)value;
75+
Assert.AreEqual(bits, Decimal64.EncodeDecimal(d), value.ToString());
76+
var expected = Decimal64.Parse(value.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture);
77+
Assert.IsTrue(d.Equals(expected) && Decimal64.HaveSameQuantum(d, expected), value.ToString());
78+
var back = (FbDecFloat)d;
79+
Assert.AreEqual(value.Coefficient, back.Coefficient, value.ToString());
80+
Assert.AreEqual(value.Exponent, back.Exponent, value.ToString());
81+
}
82+
}
83+
84+
static readonly object[] SpecialValues = new object[]
85+
{
86+
FbDecFloat.NegativeZero,
87+
FbDecFloat.PositiveInfinity,
88+
FbDecFloat.NegativeInfinity,
89+
FbDecFloat.PositiveNaN,
90+
FbDecFloat.NegativeNaN,
91+
FbDecFloat.PositiveSignalingNaN,
92+
FbDecFloat.NegativeSignalingNaN,
93+
};
94+
[TestCaseSource(nameof(SpecialValues))]
95+
public void ConvertsSpecialValuesBitExactly(FbDecFloat value)
96+
{
97+
var bits128 = BinaryPrimitives.ReadUInt128LittleEndian(DecimalCodec.DecFloat34.EncodeDecimal(value));
98+
Assert.AreEqual(bits128, Decimal128.EncodeDecimal((Decimal128)value));
99+
Assert.AreEqual(value, (FbDecFloat)(Decimal128)value);
100+
var bits64 = BinaryPrimitives.ReadUInt64LittleEndian(DecimalCodec.DecFloat16.EncodeDecimal(value));
101+
Assert.AreEqual(bits64, Decimal64.EncodeDecimal((Decimal64)value));
102+
Assert.AreEqual(value, (FbDecFloat)(Decimal64)value);
103+
}
104+
105+
[Test]
106+
public void ConvertsSpecialDecimal128Values()
107+
{
108+
Assert.AreEqual(FbDecFloat.NegativeZero, (FbDecFloat)Decimal128.NegativeZero);
109+
Assert.AreEqual(FbDecFloat.PositiveInfinity, (FbDecFloat)Decimal128.PositiveInfinity);
110+
Assert.AreEqual(FbDecFloat.NegativeInfinity, (FbDecFloat)Decimal128.NegativeInfinity);
111+
Assert.IsTrue(Decimal128.IsNaN((Decimal128)FbDecFloat.PositiveNaN));
112+
Assert.IsTrue(Decimal128.IsNegative((Decimal128)FbDecFloat.NegativeZero));
113+
}
114+
115+
[Test]
116+
public void ConvertsDecimal32ExactlyThroughDecimal64()
117+
{
118+
var value = Decimal32.Parse("1.10", CultureInfo.InvariantCulture);
119+
var result = (FbDecFloat)value;
120+
Assert.AreEqual(new BigInteger(110), result.Coefficient);
121+
Assert.AreEqual(-2, result.Exponent);
122+
}
123+
124+
static readonly object[] NotRepresentableAsDecimal64 = new object[]
125+
{
126+
new FbDecFloat(BigInteger.Parse("12345678901234567"), 0),
127+
new FbDecFloat(1, 385),
128+
new FbDecFloat(1, -399),
129+
};
130+
[TestCaseSource(nameof(NotRepresentableAsDecimal64))]
131+
public void ConvertingToDecimal64ThrowsInsteadOfRounding(FbDecFloat value)
132+
{
133+
Assert.Throws<OverflowException>(() => _ = (Decimal64)value);
134+
}
135+
136+
static readonly object[] NotRepresentableAsDecimal128 = new object[]
137+
{
138+
new FbDecFloat(BigInteger.Parse("12345678901234567890123456789012345"), 0),
139+
new FbDecFloat(1, 6145),
140+
new FbDecFloat(1, -6177),
141+
};
142+
[TestCaseSource(nameof(NotRepresentableAsDecimal128))]
143+
public void ConvertingToDecimal128ThrowsInsteadOfRounding(FbDecFloat value)
144+
{
145+
Assert.Throws<OverflowException>(() => _ = (Decimal128)value);
146+
}
147+
148+
[Test]
149+
public void ConvertsRepresentableValueToDecimal64AfterNormalization()
150+
{
151+
// 17 digits, but the last one is a trailing zero
152+
var value = new FbDecFloat(BigInteger.Parse("12345678901234560"), 0);
153+
Assert.AreEqual(Decimal64.Parse("12345678901234560", CultureInfo.InvariantCulture), (Decimal64)value);
154+
}
155+
}
156+
157+
#endif

0 commit comments

Comments
 (0)