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
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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,18 @@ 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" },
};

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

[TestCaseSource(nameof(TestValues))]
public async Task ReadsValueCorrectly(FbDecFloat value, string castValue)
{
Expand All @@ -88,6 +98,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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,18 @@ 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, 6144), "1E6144" },
new object[] { new FbDecFloat(BigInteger.Parse("12345678901234567890123456789012340"), 0), "12345678901234567890123456789012340" },
new object[] { new FbDecFloat(1000, -6179), "1E-6176" },
};

static readonly object[] UnrepresentableValues = new object[]
{
new FbDecFloat(BigInteger.Parse("12345678901234567890123456789012345"), 0),
new FbDecFloat(1, 6145),
new FbDecFloat(1, -6177),
};

[TestCaseSource(nameof(TestValues))]
public async Task ReadsValueCorrectly(FbDecFloat value, string castValue)
{
Expand All @@ -88,6 +98,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(34)) 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(34)) from rdb$database";
Assert.AreEqual((FbDecFloat)1, (FbDecFloat)await cmd.ExecuteScalarAsync(), "connection must remain usable");
}
}

[Test]
public async Task ReadsValueNullCorrectly()
{
Expand Down
58 changes: 56 additions & 2 deletions src/FirebirdSql.Data.FirebirdClient/Common/DecimalCodec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

using System;
using System.Diagnostics;
using System.Numerics;
using FirebirdSql.Data.Types;

namespace FirebirdSql.Data.Common;
Expand All @@ -41,6 +42,9 @@ public DecimalFormat(int formatBitLength, int coefficientDigits)
ELimit = 3 * (1 << ExponentContinuationBits) - 1;
EMin = -ELimit / 2;
ExponentBias = -EMin + CoefficientDigits - 1;
MaxCoefficient = BigInteger.Pow(10, CoefficientDigits) - 1;
MinExponent = UnbiasedExponent(0);
MaxExponent = UnbiasedExponent(ELimit);
}

public int FormatBitLength { get; }
Expand All @@ -51,6 +55,10 @@ public DecimalFormat(int formatBitLength, int coefficientDigits)
public int ELimit { get; }
public int EMin { get; }
public int ExponentBias { get; }
public BigInteger MaxCoefficient { get; }
// Range of the exponent applied to the integral coefficient (value = coefficient * 10^exponent).
public int MinExponent { get; }
public int MaxExponent { get; }

public void ValidateByteLength(byte[] decBytes)
{
Expand Down Expand Up @@ -171,8 +179,8 @@ public byte[] EncodeDecimal(FbDecFloat @decimal)

void EncodeFinite(FbDecFloat @decimal, byte[] decBytes)
{
var biasedExponent = _decimalFormat.BiasedExponent(@decimal.Exponent);
var coefficient = @decimal.Coefficient;
var (coefficient, exponent) = FitToFormat(@decimal);
var biasedExponent = _decimalFormat.BiasedExponent(exponent);
var mostSignificantDigit = _coefficientCoder.EncodeValue(coefficient, decBytes);
var expMSB = biasedExponent >>> _decimalFormat.ExponentContinuationBits;
var expTwoBitCont = (biasedExponent >>> _decimalFormat.ExponentContinuationBits - 2) & 0b011;
Expand All @@ -192,6 +200,52 @@ void EncodeFinite(FbDecFloat @decimal, byte[] decBytes)
EncodeExponentContinuation(decBytes, biasedExponent, _decimalFormat.ExponentContinuationBits - 2);
}

// Brings the coefficient and exponent into the ranges of the format without changing the numeric value
// (removing trailing zeros of the coefficient, or moving the exponent into the coefficient).
// Throws when that is impossible, i.e. when the value would have to be rounded, or overflows or underflows the format.
(BigInteger coefficient, int exponent) FitToFormat(FbDecFloat @decimal)
{
var coefficient = BigInteger.Abs(@decimal.Coefficient);
long exponent = @decimal.Exponent;
if (coefficient.IsZero)
{
return (coefficient, (int)Math.Clamp(exponent, _decimalFormat.MinExponent, _decimalFormat.MaxExponent));
}
while (coefficient > _decimalFormat.MaxCoefficient)
{
coefficient = BigInteger.DivRem(coefficient, 10, out var remainder);
if (!remainder.IsZero)
{
throw CannotRepresent(@decimal, $"more than {_decimalFormat.CoefficientDigits} significant digits");
}
exponent++;
}
while (exponent > _decimalFormat.MaxExponent)
{
coefficient *= 10;
if (coefficient > _decimalFormat.MaxCoefficient)
{
throw CannotRepresent(@decimal, "exponent overflow");
}
exponent--;
}
while (exponent < _decimalFormat.MinExponent)
{
coefficient = BigInteger.DivRem(coefficient, 10, out var remainder);
if (!remainder.IsZero)
{
throw CannotRepresent(@decimal, "exponent underflow");
}
exponent++;
}
return (coefficient, (int)exponent);
}

OverflowException CannotRepresent(FbDecFloat @decimal, string reason)
{
return new OverflowException($"Value {@decimal} cannot be represented exactly as DECFLOAT({_decimalFormat.CoefficientDigits}): {reason}.");
}

static void EncodeExponentContinuation(byte[] decBytes, int expAndBias, int expBitsRemaining)
{
var expByteIndex = 1;
Expand Down