Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/decfloat.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@

`DECFLOAT(16)` and `DECFLOAT(34)` from Firebird 4 is handled by `FbDecFloat` type. `FbDecFloat` can be used as parameter value for `FbParameter`.

### Values are never rounded silently

`DECFLOAT(16)` holds 16 significant digits and `DECFLOAT(34)` holds 34. A value that cannot be represented exactly in the precision or exponent range of the target (for example a 17 digit value passed to a `DECFLOAT(16)` parameter) throws `OverflowException`. Values that only need trailing zeros removed or the exponent adjusted (e.g. `1E384` in `DECFLOAT(16)`) are accepted.

### Other .NET types

Parameters: besides `FbDecFloat`, a `DECFLOAT` parameter accepts `decimal`, `double`, `float`, the integral types and `BigInteger`:

* `decimal` is converted exactly, including its scale (`1.10m` stays `1.10`).
* `double` and `float` are converted to the shortest decimal that converts back to the same binary value (`0.1` is `0.1`, `0.1 + 0.2` is `0.30000000000000004`), which may have up to 17 significant digits.

Reading a `DECFLOAT` column:

* `GetDecimal` is exact; it throws `OverflowException` when the value cannot be represented exactly as `decimal` (too large, too many digits, NaN or infinity).
* `GetDouble` and `GetFloat` return the nearest value (NaN and infinities are mapped).
* `GetInt16`, `GetInt32`, `GetInt64` and `GetByte` round to the nearest integer, ties to even, and throw `OverflowException` when out of range.

`FbDecFloat` has explicit conversions from and to `decimal`, `double` and `float`, with the same rules.

### Examples

Examples can be found in [`FbDecFloat16SupportTests`](../src/FirebirdSql.Data.FirebirdClient.Tests/FbDecFloat16SupportTests.cs) and [`FbDecFloat34SupportTests`](../src/FirebirdSql.Data.FirebirdClient.Tests/FbDecFloat34SupportTests.cs).
106 changes: 106 additions & 0 deletions src/FirebirdSql.Data.FirebirdClient.Tests/DecimalCodecTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* The contents of this file are subject to the Initial
* Developer's Public License Version 1.0 (the "License");
* you may not use this file except in compliance with the
* License. You may obtain a copy of the License at
* https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt.
*
* Software distributed under the License is distributed on
* an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either
* express or implied. See the License for the specific
* language governing rights and limitations under the License.
*
* All Rights Reserved.
*/

//$Authors = F.D.Castel

using System;
using System.Numerics;
using FirebirdSql.Data.Common;
using FirebirdSql.Data.TestsBase;
using FirebirdSql.Data.Types;
using NUnit.Framework;

namespace FirebirdSql.Data.FirebirdClient.Tests;

[NoServerCategory]
public class DecimalCodecTests
{
static readonly object[] RepresentableDecFloat16Values = new object[]
{
new FbDecFloat(BigInteger.Parse("9999999999999999"), 369),
new FbDecFloat(BigInteger.Parse("-9999999999999999"), 369),
new FbDecFloat(1, -398),
new FbDecFloat(-1, -398),
// representable only after normalization
new FbDecFloat(1, 384),
new FbDecFloat(-1, 384),
new FbDecFloat(BigInteger.Parse("100000000000000000000"), -20),
new FbDecFloat(BigInteger.Parse("12345678901234560"), 0),
new FbDecFloat(100, -400),
new FbDecFloat(0, 1000),
new FbDecFloat(0, -1000),
};
[TestCaseSource(nameof(RepresentableDecFloat16Values))]
public void DecFloat16EncodesRepresentableValueExactly(FbDecFloat value)
{
var result = DecimalCodec.DecFloat16.ParseBytes(DecimalCodec.DecFloat16.EncodeDecimal(value));
Assert.AreEqual(value, result);
}

static readonly object[] RepresentableDecFloat34Values = new object[]
{
new FbDecFloat(BigInteger.Parse("9999999999999999999999999999999999"), 6111),
new FbDecFloat(1, -6176),
// representable only after normalization
new FbDecFloat(1, 6144),
new FbDecFloat(BigInteger.Parse("12345678901234567890123456789012340"), 0),
new FbDecFloat(BigInteger.Parse("-12345678901234567890123456789012340000"), -40),
new FbDecFloat(1000, -6179),
new FbDecFloat(0, int.MaxValue),
new FbDecFloat(0, int.MinValue),
};
[TestCaseSource(nameof(RepresentableDecFloat34Values))]
public void DecFloat34EncodesRepresentableValueExactly(FbDecFloat value)
{
var result = DecimalCodec.DecFloat34.ParseBytes(DecimalCodec.DecFloat34.EncodeDecimal(value));
Assert.AreEqual(value, result);
}

[Test]
public void ZeroExponentIsClamped()
{
var result = DecimalCodec.DecFloat16.ParseBytes(DecimalCodec.DecFloat16.EncodeDecimal(new FbDecFloat(0, 1000)));
Assert.AreEqual(369, result.Exponent);
}

static readonly object[] UnrepresentableDecFloat16Values = new object[]
{
new FbDecFloat(BigInteger.Parse("30000000000000004"), -17),
new FbDecFloat(BigInteger.Parse("-12345678901234567"), 0),
new FbDecFloat(1, 385),
new FbDecFloat(BigInteger.Parse("9999999999999999"), 370),
new FbDecFloat(1, -399),
new FbDecFloat(15, -400),
new FbDecFloat(1, int.MaxValue),
new FbDecFloat(1, int.MinValue),
};
[TestCaseSource(nameof(UnrepresentableDecFloat16Values))]
public void DecFloat16ThrowsInsteadOfRounding(FbDecFloat value)
{
Assert.Throws<OverflowException>(() => DecimalCodec.DecFloat16.EncodeDecimal(value));
}

static readonly object[] UnrepresentableDecFloat34Values = new object[]
{
new FbDecFloat(BigInteger.Parse("12345678901234567890123456789012345"), 0),
new FbDecFloat(1, 6145),
new FbDecFloat(1, -6177),
};
[TestCaseSource(nameof(UnrepresentableDecFloat34Values))]
public void DecFloat34ThrowsInsteadOfRounding(FbDecFloat value)
{
Assert.Throws<OverflowException>(() => DecimalCodec.DecFloat34.EncodeDecimal(value));
}
}
180 changes: 180 additions & 0 deletions src/FirebirdSql.Data.FirebirdClient.Tests/FbDecFloat16SupportTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
//$Authors = Jiri Cincura (jiri@cincura.net)

using System;
using System.Globalization;
using System.Numerics;
using System.Threading.Tasks;
using FirebirdSql.Data.TestsBase;
Expand Down Expand Up @@ -63,8 +64,25 @@ public override async Task SetUp()
new object[] { new FbDecFloat(-10, 34), "-100000000000000000000000000000000000" },
new object[] { new FbDecFloat(BigInteger.Parse("123000000001"), -10), "123.000000001E-1" },
new object[] { new FbDecFloat(BigInteger.Parse("-123000000001"), -10), "-123.000000001E-1" },
new object[] { new FbDecFloat(1, 384), "1E384" },
new object[] { new FbDecFloat(BigInteger.Parse("100000000000000000000"), -20), "1" },
new object[] { new FbDecFloat(100, -400), "1E-398" },
new object[] { (FbDecFloat)2.0, "2" },
new object[] { (FbDecFloat)1e-20, "1E-20" },
new object[] { (FbDecFloat)2.5f, "2.5" },
new object[] { (FbDecFloat)100m, "100" },
new object[] { (FbDecFloat)1.10m, "1.10" },
};

static readonly object[] UnrepresentableValues = new object[]
{
new FbDecFloat(BigInteger.Parse("30000000000000004"), -17),
new FbDecFloat(1, 385),
new FbDecFloat(1, -399),
(FbDecFloat)(0.1 + 0.2),
(FbDecFloat)decimal.MaxValue,
};

[TestCaseSource(nameof(TestValues))]
public async Task ReadsValueCorrectly(FbDecFloat value, string castValue)
{
Expand All @@ -88,6 +106,22 @@ public async Task PassesValueCorrectly(FbDecFloat value, string dummy)
}
}

[TestCaseSource(nameof(UnrepresentableValues))]
public async Task PassesUnrepresentableValueThrows(FbDecFloat value)
{
await using (var cmd = Connection.CreateCommand())
{
cmd.CommandText = "select cast(@value as decfloat(16)) from rdb$database";
cmd.Parameters.AddWithValue("value", value);
Assert.ThrowsAsync<OverflowException>(() => cmd.ExecuteScalarAsync());
}
await using (var cmd = Connection.CreateCommand())
{
cmd.CommandText = "select cast(1 as decfloat(16)) from rdb$database";
Assert.AreEqual((FbDecFloat)1, (FbDecFloat)await cmd.ExecuteScalarAsync(), "connection must remain usable");
}
}

[Test]
public async Task ReadsValueNullCorrectly()
{
Expand All @@ -111,6 +145,152 @@ public async Task PassesValueNullCorrectly()
}
}

static readonly object[] ClrValues = new[]
{
new object[] { 2.5m, "25", -1 },
new object[] { 1.10m, "110", -2 },
new object[] { -100m, "-100", 0 },
new object[] { 0.1, "1", -1 },
new object[] { -6.6, "-66", -1 },
new object[] { 2.5f, "25", -1 },
new object[] { 42, "42", 0 },
new object[] { -42L, "-42", 0 },
new object[] { (short)7, "7", 0 },
new object[] { (byte)7, "7", 0 },
new object[] { 1234567890123456UL, "1234567890123456", 0 },
new object[] { new BigInteger(123456789), "123456789", 0 },
};

[TestCaseSource(nameof(ClrValues))]
public async Task PassesClrValueCorrectly(object value, string coefficient, int exponent)
{
await using (var cmd = Connection.CreateCommand())
{
cmd.CommandText = "select cast(@value as decfloat(16)) from rdb$database";
cmd.Parameters.AddWithValue("value", value);
var result = (FbDecFloat)await cmd.ExecuteScalarAsync();
Assert.AreEqual(new FbDecFloat(BigInteger.Parse(coefficient, CultureInfo.InvariantCulture), exponent), result);
}
}

static readonly object[] UnrepresentableClrValues = new object[]
{
ulong.MaxValue,
decimal.MaxValue,
0.1 + 0.2,
1.0 / 3.0 * 1e-300,
};

[TestCaseSource(nameof(UnrepresentableClrValues))]
public async Task PassesUnrepresentableClrValueThrows(object value)
{
await using (var cmd = Connection.CreateCommand())
{
cmd.CommandText = "select cast(@value as decfloat(16)) from rdb$database";
cmd.Parameters.AddWithValue("value", value);
Assert.ThrowsAsync<OverflowException>(() => cmd.ExecuteScalarAsync());
}
}

[TestCase("1.10", "1.10")]
[TestCase("-123.456", "-123.456")]
[TestCase("1E+20", "100000000000000000000")]
[TestCase("0", "0")]
[TestCase("0.0000000000009999999999999999", "0.0000000000009999999999999999")]
public async Task ReadsAsDecimalExactly(string castValue, string expected)
{
await using (var cmd = Connection.CreateCommand())
{
cmd.CommandText = $"select cast('{castValue}' as decfloat(16)) from rdb$database";
await using (var reader = await cmd.ExecuteReaderAsync())
{
await reader.ReadAsync();
Assert.AreEqual(expected, reader.GetDecimal(0).ToString(CultureInfo.InvariantCulture));
Assert.AreEqual(expected, reader.GetFieldValue<decimal>(0).ToString(CultureInfo.InvariantCulture));
}
}
}

[TestCase("1E+29")]
[TestCase("-1E+29")]
[TestCase("1E-29")]
[TestCase("nan")]
[TestCase("inf")]
[TestCase("1E+300")]
public async Task ReadsAsDecimalThrowsWhenNotExact(string castValue)
{
await using (var cmd = Connection.CreateCommand())
{
cmd.CommandText = $"select cast('{castValue}' as decfloat(16)) from rdb$database";
await using (var reader = await cmd.ExecuteReaderAsync())
{
await reader.ReadAsync();
Assert.Throws<OverflowException>(() => reader.GetDecimal(0));
}
}
}

[TestCase("1.10", 1.1)]
[TestCase("-6.6", -6.6)]
[TestCase("0.1", 0.1)]
[TestCase("nan", double.NaN)]
[TestCase("inf", double.PositiveInfinity)]
[TestCase("-inf", double.NegativeInfinity)]
[TestCase("1E-398", 0d)]
[TestCase("1E+300", 1E+300)]
public async Task ReadsAsDoubleNearest(string castValue, double expected)
{
await using (var cmd = Connection.CreateCommand())
{
cmd.CommandText = $"select cast('{castValue}' as decfloat(16)) from rdb$database";
await using (var reader = await cmd.ExecuteReaderAsync())
{
await reader.ReadAsync();
Assert.AreEqual(expected, reader.GetDouble(0));
Assert.AreEqual((float)expected, reader.GetFloat(0));
}
}
}

[TestCase("123", 123)]
[TestCase("1E+3", 1000)]
[TestCase("2.5", 2)]
[TestCase("3.5", 4)]
[TestCase("-2.5", -2)]
[TestCase("-3.5", -4)]
[TestCase("2.51", 3)]
[TestCase("1E-300", 0)]
public async Task ReadsAsIntegerRoundingToEven(string castValue, long expected)
{
await using (var cmd = Connection.CreateCommand())
{
cmd.CommandText = $"select cast('{castValue}' as decfloat(16)) from rdb$database";
await using (var reader = await cmd.ExecuteReaderAsync())
{
await reader.ReadAsync();
Assert.AreEqual(expected, reader.GetInt64(0));
Assert.AreEqual((int)expected, reader.GetInt32(0));
Assert.AreEqual((short)expected, reader.GetInt16(0));
}
}
}

[TestCase("1E+19")]
[TestCase("nan")]
[TestCase("-inf")]
public async Task ReadsAsIntegerThrowsOnOverflow(string castValue)
{
await using (var cmd = Connection.CreateCommand())
{
cmd.CommandText = $"select cast('{castValue}' as decfloat(16)) from rdb$database";
await using (var reader = await cmd.ExecuteReaderAsync())
{
await reader.ReadAsync();
Assert.Throws<OverflowException>(() => reader.GetInt64(0));
}
}
}

[Test]
public async Task SelectEmptyResultSet()
{
Expand Down
Loading