Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix | Improve decimal conversion from SqlDecimal to .Net Decimal #1179

Merged
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ internal DateTime DateTime
}
}

#region Decimal
internal decimal Decimal
{
get
Expand All @@ -215,6 +216,43 @@ internal decimal Decimal
{
if (_value._numericInfo._data4 != 0 || _value._numericInfo._scale > 28)
{
// Only removing trailing zeros from a decimal part won't hit its value!
if (_value._numericInfo._scale > 0)
{
int zeroCnt = FindTrailingZerosAndPrec((uint)_value._numericInfo._data1, (uint)_value._numericInfo._data2,
(uint)_value._numericInfo._data3, (uint)_value._numericInfo._data4,
_value._numericInfo._scale, out int precision);

int minScale = _value._numericInfo._scale - zeroCnt; // minimum possible sacle after removing the trailing zeros.

if (zeroCnt > 0 && minScale <= 28 && precision <= 29)
{
SqlDecimal sqlValue = new(_value._numericInfo._precision, _value._numericInfo._scale, _value._numericInfo._positive,
_value._numericInfo._data1, _value._numericInfo._data2,
_value._numericInfo._data3, _value._numericInfo._data4);

int integral = precision - minScale;
int newPrec = 29;

if (integral != 1 && precision != 29)
{
newPrec = 28;
}

try
{
// Precision could be 28 or 29
// ex: (precision == 29 && scale == 28)
// valid: (+/-)7.1234567890123456789012345678
// invalid: (+/-)8.1234567890123456789012345678
return SqlDecimal.ConvertToPrecScale(sqlValue, newPrec, newPrec - integral).Value;
}
catch (OverflowException)
{
throw new OverflowException(SQLResource.ConversionOverflowMessage);
}
}
}
throw new OverflowException(SQLResource.ConversionOverflowMessage);
}
return new decimal(_value._numericInfo._data1, _value._numericInfo._data2, _value._numericInfo._data3, !_value._numericInfo._positive, _value._numericInfo._scale);
Expand All @@ -234,6 +272,85 @@ internal decimal Decimal
}
}

/// <summary>
/// Returns number of trailing zeros using the supplied parameters.
/// </summary>
/// <param name="data1">An 32-bit unsigned integer which will be combined with data2, data3, and data4</param>
/// <param name="data2">An 32-bit unsigned integer which will be combined with data1, data3, and data4</param>
/// <param name="data3">An 32-bit unsigned integer which will be combined with data1, data2, and data4</param>
/// <param name="data4">An 32-bit unsigned integer which will be combined with data1, data2, and data3</param>
/// <param name="scale">The number of decimal places</param>
/// <param name="valuablePrecision">OUT |The number of digits without trailing zeros</param>
/// <returns>Number of trailing zeros</returns>
private static int FindTrailingZerosAndPrec(uint data1, uint data2, uint data3, uint data4, byte scale, out int valuablePrecision)
{
// Make local copy of data to avoid modifying input.
Span<uint> rgulNumeric = stackalloc uint[4] { data1, data2, data3, data4 };
int zeroCnt = 0; //Number of trailing zero digits
int precCnt = 0; //Valuable precision
uint uiRem = 0; //Remainder of a division by 10
int len = 4; // Max possible items

//Retrieve each digit from the lowest significant digit
while (len > 1 || rgulNumeric[0] != 0)
{
SqlDecimalDivBy(rgulNumeric, ref len, 10, out uiRem);
if (uiRem == 0 && precCnt == 0)
{
zeroCnt++;
}
else
{
precCnt++;
}
}

if (uiRem == 0)
{
zeroCnt = scale;
}

// if scale of the number has not been reached, pad remaining number with zeros.
if (zeroCnt + precCnt <= scale)
{
precCnt = scale - zeroCnt + 1;
}
valuablePrecision = precCnt;
return zeroCnt;
}

/// <summary>
/// Multi-precision one super-digit divide in place.
/// U = U / D,
/// R = U % D
/// (Length of U can decrease)
/// </summary>
/// <param name="data">InOut | U</param>
/// <param name="len">InOut | Number of items with non-zero value in U between 1 to 4</param>
/// <param name="divisor">In | D</param>
/// <param name="remainder">Out | R</param>
private static void SqlDecimalDivBy(Span<uint> data, ref int len, uint divisor, out uint remainder)
{
uint uiCarry = 0;
ulong ulAccum;
ulong ulDivisor = (ulong)divisor;
int iLen = len;

while (iLen > 0)
{
iLen--;
ulAccum = (((ulong)uiCarry) << 32) + (ulong)(data[iLen]);
data[iLen] = (uint)(ulAccum / ulDivisor);
uiCarry = (uint)(ulAccum - (ulong)data[iLen] * ulDivisor); // (ULONG) (ulAccum % divisor)
}
remainder = uiCarry;

// Normalize multi-precision number - remove leading zeroes
while (len > 1 && data[len - 1] == 0)
{ len--; }
}
#endregion

internal double Double
{
get
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ internal DateTime DateTime
}
}

#region Decimal
internal decimal Decimal
{
get
Expand All @@ -212,6 +213,43 @@ internal decimal Decimal
{
if (_value._numericInfo._data4 != 0 || _value._numericInfo._scale > 28)
{
// Only removing trailing zeros from a decimal part won't hit its value!
if (_value._numericInfo._scale > 0)
{
int zeroCnt = FindTrailingZerosAndPrec((uint)_value._numericInfo._data1, (uint)_value._numericInfo._data2,
(uint)_value._numericInfo._data3, (uint)_value._numericInfo._data4,
_value._numericInfo._scale, out int precision);

int minScale = _value._numericInfo._scale - zeroCnt; // minimum possible sacle after removing the trailing zeros.

if (zeroCnt > 0 && minScale <= 28 && precision <= 29)
{
SqlDecimal sqlValue = new(_value._numericInfo._precision, _value._numericInfo._scale, _value._numericInfo._positive,
_value._numericInfo._data1, _value._numericInfo._data2,
_value._numericInfo._data3, _value._numericInfo._data4);

int integral = precision - minScale;
int newPrec = 29;

if (integral != 1 && precision != 29)
{
newPrec = 28;
}

try
{
// Precision could be 28 or 29
// ex: (precision == 29 && scale == 28)
// valid: (+/-)7.1234567890123456789012345678
// invalid: (+/-)8.1234567890123456789012345678
return SqlDecimal.ConvertToPrecScale(sqlValue, newPrec, newPrec - integral).Value;
}
catch (OverflowException)
{
throw new OverflowException(SQLResource.ConversionOverflowMessage);
}
}
}
throw new OverflowException(SQLResource.ConversionOverflowMessage);
}
return new decimal(_value._numericInfo._data1, _value._numericInfo._data2, _value._numericInfo._data3, !_value._numericInfo._positive, _value._numericInfo._scale);
Expand All @@ -231,6 +269,85 @@ internal decimal Decimal
}
}

/// <summary>
/// Returns number of trailing zeros using the supplied parameters.
/// </summary>
/// <param name="data1">An 32-bit unsigned integer which will be combined with data2, data3, and data4</param>
/// <param name="data2">An 32-bit unsigned integer which will be combined with data1, data3, and data4</param>
/// <param name="data3">An 32-bit unsigned integer which will be combined with data1, data2, and data4</param>
/// <param name="data4">An 32-bit unsigned integer which will be combined with data1, data2, and data3</param>
/// <param name="scale">The number of decimal places</param>
/// <param name="valuablePrecision">OUT |The number of digits without trailing zeros</param>
/// <returns>Number of trailing zeros</returns>
private static int FindTrailingZerosAndPrec(uint data1, uint data2, uint data3, uint data4, byte scale, out int valuablePrecision)
{
// Make local copy of data to avoid modifying input.
Span<uint> rgulNumeric = stackalloc uint[4] { data1, data2, data3, data4 };
int zeroCnt = 0; //Number of trailing zero digits
int precCnt = 0; //Valuable precision
uint uiRem = 0; //Remainder of a division by 10
int len = 4; // Max possible items

//Retrieve each digit from the lowest significant digit
while (len > 1 || rgulNumeric[0] != 0)
{
SqlDecimalDivBy(rgulNumeric, ref len, 10, out uiRem);
if (uiRem == 0 && precCnt == 0)
{
zeroCnt++;
}
else
{
precCnt++;
}
}

if (uiRem == 0)
{
zeroCnt = scale;
}

// if scale of the number has not been reached, pad remaining number with zeros.
if (zeroCnt + precCnt <= scale)
{
precCnt = scale - zeroCnt + 1;
}
valuablePrecision = precCnt;
return zeroCnt;
}

/// <summary>
/// Multi-precision one super-digit divide in place.
/// U = U / D,
/// R = U % D
/// (Length of U can decrease)
/// </summary>
/// <param name="data">InOut | U</param>
/// <param name="len">InOut | Number of items with non-zero value in U between 1 to 4</param>
/// <param name="divisor">In | D</param>
/// <param name="remainder">Out | R</param>
private static void SqlDecimalDivBy(Span<uint> data, ref int len, uint divisor, out uint remainder)
{
uint uiCarry = 0;
ulong ulAccum;
ulong ulDivisor = (ulong)divisor;
int iLen = len;

while (iLen > 0)
{
iLen--;
ulAccum = (((ulong)uiCarry) << 32) + (ulong)(data[iLen]);
data[iLen] = (uint)(ulAccum / ulDivisor);
uiCarry = (uint)(ulAccum - (ulong)data[iLen] * ulDivisor); // (ULONG) (ulAccum % divisor)
}
remainder = uiCarry;

// Normalize multi-precision number - remove leading zeroes
while (len > 1 && data[len - 1] == 0)
{ len--; }
}
#endregion

internal double Double
{
get
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,67 @@ public static void TestParametersWithDatatablesTVPInsert()
}

#region Scaled Decimal Parameter & TVP Test
[ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))]
[InlineData("CAST(1.0 as decimal(38, 37))", "1.0000000000000000000000000000")]
[InlineData("CAST(7.1234567890123456789012345678 as decimal(38, 35))", "7.1234567890123456789012345678")]
[InlineData("CAST(-7.1234567890123456789012345678 as decimal(38, 35))", "-7.1234567890123456789012345678")]
[InlineData("CAST(-0.1234567890123456789012345678 as decimal(38, 35))", "-0.1234567890123456789012345678")]
[InlineData("CAST(4210862852.86 as decimal(38, 20))", "4210862852.860000000000000000")]
[InlineData("CAST(0 as decimal(38, 36))", "0.0000000000000000000000000000")]
[InlineData("CAST(79228162514264337593543950335 as decimal(38, 9))", "79228162514264337593543950335")]
[InlineData("CAST(-79228162514264337593543950335 as decimal(38, 9))", "-79228162514264337593543950335")]
[InlineData("CAST(0.4210862852 as decimal(38, 38))", "0.4210862852000000000000000000")]
[InlineData("CAST(0.1234567890123456789012345678 as decimal(38, 38))", "0.1234567890123456789012345678")]
[InlineData("CAST(249454727.14678312032280248320 as decimal(38, 20))", "249454727.14678312032280248320")]
[InlineData("CAST(3961408124790879675.7769715711 as decimal(38, 10))", "3961408124790879675.7769715711")]
[InlineData("CAST(3961408124790879675776971571.1 as decimal(38, 1))", "3961408124790879675776971571.1")]
[InlineData("CAST(79228162514264337593543950335 as decimal(38, 0))", "79228162514264337593543950335")]
[InlineData("CAST(-79228162514264337593543950335 as decimal(38, 0))", "-79228162514264337593543950335")]
[InlineData("CAST(0.0000000000000000000000000001 as decimal(38, 38))", "0.0000000000000000000000000001")]
[InlineData("CAST(-0.0000000000000000000000000001 as decimal(38, 38))", "-0.0000000000000000000000000001")]
public static void SqlDecimalConvertToDecimal_TestInRange(string sqlDecimalValue, string expectedDecimalValue)
{
using(SqlConnection cnn = new(s_connString))
{
cnn.Open();
using(SqlCommand cmd = new($"SELECT {sqlDecimalValue} val"))
{
cmd.Connection = cnn;
using (SqlDataReader rdr = cmd.ExecuteReader())
{
Assert.True(rdr.Read(), "SqlDataReader must have a value");
decimal retrunValue = rdr.GetDecimal(0);
Assert.Equal(expectedDecimalValue, retrunValue.ToString());
}
}
}
}

[ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))]
[InlineData("CAST(7.9999999999999999999999999999 as decimal(38, 35))")]
[InlineData("CAST(8.1234567890123456789012345678 as decimal(38, 35))")]
[InlineData("CAST(-8.1234567890123456789012345678 as decimal(38, 35))")]
[InlineData("CAST(123456789012345678901234567890 as decimal(38, 0))")]
[InlineData("CAST(7922816251426433759354395.9999 as decimal(38, 8))")]
[InlineData("CAST(-7922816251426433759354395.9999 as decimal(38, 8))")]
[InlineData("CAST(0.123456789012345678901234567890 as decimal(38, 36))")]
public static void SqlDecimalConvertToDecimal_TestOutOfRange(string sqlDecimalValue)
{
using (SqlConnection cnn = new(s_connString))
{
cnn.Open();
using (SqlCommand cmd = new($"SELECT {sqlDecimalValue} val"))
{
cmd.Connection = cnn;
using (SqlDataReader rdr = cmd.ExecuteReader())
{
Assert.True(rdr.Read(), "SqlDataReader must have a value");
Assert.Throws<OverflowException>(() => rdr.GetDecimal(0));
}
}
}
}

[Theory]
[ClassData(typeof(ConnectionStringsProvider))]
public static void TestScaledDecimalParameter_CommandInsert(string connectionString, bool truncateScaledDecimal)
Expand Down