diff --git a/csharp/src/Apache.Arrow/Arrays/Decimal128Array.cs b/csharp/src/Apache.Arrow/Arrays/Decimal128Array.cs index 7b147f5124c1d..01724e2acda3e 100644 --- a/csharp/src/Apache.Arrow/Arrays/Decimal128Array.cs +++ b/csharp/src/Apache.Arrow/Arrays/Decimal128Array.cs @@ -64,6 +64,37 @@ public Builder AppendRange(IEnumerable values) return Instance; } + public Builder Append(string value) + { + if (value == null) + { + AppendNull(); + } + else + { + Span bytes = stackalloc byte[DataType.ByteWidth]; + DecimalUtility.GetBytes(value, DataType.Precision, DataType.Scale, ByteWidth, bytes); + Append(bytes); + } + + return Instance; + } + + public Builder AppendRange(IEnumerable values) + { + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + foreach (string s in values) + { + Append(s); + } + + return Instance; + } + #if !NETSTANDARD1_3 public Builder Append(SqlDecimal value) { @@ -120,6 +151,15 @@ public Decimal128Array(ArrayData data) return DecimalUtility.GetDecimal(ValueBuffer, index, Scale, ByteWidth); } + public string GetString(int index) + { + if (IsNull(index)) + { + return null; + } + return DecimalUtility.GetString(ValueBuffer, index, Precision, Scale, ByteWidth); + } + #if !NETSTANDARD1_3 public SqlDecimal? GetSqlDecimal(int index) { diff --git a/csharp/src/Apache.Arrow/Arrays/Decimal256Array.cs b/csharp/src/Apache.Arrow/Arrays/Decimal256Array.cs index fb4cd6be396e3..f314c2d6ebc9e 100644 --- a/csharp/src/Apache.Arrow/Arrays/Decimal256Array.cs +++ b/csharp/src/Apache.Arrow/Arrays/Decimal256Array.cs @@ -15,8 +15,10 @@ using System; using System.Collections.Generic; +#if !NETSTANDARD1_3 +using System.Data.SqlTypes; +#endif using System.Diagnostics; -using System.Numerics; using Apache.Arrow.Arrays; using Apache.Arrow.Types; @@ -61,6 +63,68 @@ public Builder AppendRange(IEnumerable values) return Instance; } + public Builder Append(string value) + { + if (value == null) + { + AppendNull(); + } + else + { + Span bytes = stackalloc byte[DataType.ByteWidth]; + DecimalUtility.GetBytes(value, DataType.Precision, DataType.Scale, ByteWidth, bytes); + Append(bytes); + } + + return Instance; + } + + public Builder AppendRange(IEnumerable values) + { + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + foreach (string s in values) + { + Append(s); + } + + return Instance; + } + +#if !NETSTANDARD1_3 + public Builder Append(SqlDecimal value) + { + Span bytes = stackalloc byte[DataType.ByteWidth]; + DecimalUtility.GetBytes(value, DataType.Precision, DataType.Scale, bytes); + if (!value.IsPositive) + { + var span = bytes.CastTo(); + span[2] = -1; + span[3] = -1; + } + + return Append(bytes); + } + + public Builder AppendRange(IEnumerable values) + { + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + foreach (SqlDecimal d in values) + { + Append(d); + } + + return Instance; + } +#endif + public Builder Set(int index, decimal value) { Span bytes = stackalloc byte[DataType.ByteWidth]; @@ -92,5 +156,37 @@ public Decimal256Array(ArrayData data) return DecimalUtility.GetDecimal(ValueBuffer, index, Scale, ByteWidth); } + + public string GetString(int index) + { + if (IsNull(index)) + { + return null; + } + return DecimalUtility.GetString(ValueBuffer, index, Precision, Scale, ByteWidth); + } + +#if !NETSTANDARD1_3 + public bool TryGetSqlDecimal(int index, out SqlDecimal? value) + { + if (IsNull(index)) + { + value = null; + return true; + } + + const int longWidth = 4; + var span = ValueBuffer.Span.CastTo().Slice(index * longWidth); + if ((span[2] == 0 && span[3] == 0) || + (span[2] == -1 && span[3] == -1)) + { + value = DecimalUtility.GetSqlDecimal128(ValueBuffer, 2 * index, Precision, Scale); + return true; + } + + value = null; + return false; + } +#endif } } diff --git a/csharp/src/Apache.Arrow/DecimalUtility.cs b/csharp/src/Apache.Arrow/DecimalUtility.cs index 35e56ff65e2ed..bb3f0834fcec3 100644 --- a/csharp/src/Apache.Arrow/DecimalUtility.cs +++ b/csharp/src/Apache.Arrow/DecimalUtility.cs @@ -76,6 +76,113 @@ internal static decimal GetDecimal(in ArrowBuffer valueBuffer, int index, int sc } } +#if NETCOREAPP + internal unsafe static string GetString(in ArrowBuffer valueBuffer, int index, int precision, int scale, int byteWidth) + { + int startIndex = index * byteWidth; + ReadOnlySpan value = valueBuffer.Span.Slice(startIndex, byteWidth); + BigInteger integerValue = new BigInteger(value); + if (scale == 0) + { + return integerValue.ToString(); + } + + bool negative = integerValue.Sign < 0; + if (negative) + { + integerValue = -integerValue; + } + + int start = scale + 3; + Span result = stackalloc char[start + precision]; + if (!integerValue.TryFormat(result.Slice(start), out int charsWritten) || charsWritten > precision) + { + throw new OverflowException($"Value: {integerValue} cannot be formatted"); + } + + if (scale >= charsWritten) + { + int length = charsWritten; + result[++length] = '0'; + result[++length] = '.'; + while (scale > length - 2) + { + result[++length] = '0'; + } + start = charsWritten + 1; + charsWritten = length; + } + else + { + result.Slice(start, charsWritten - scale).CopyTo(result.Slice(--start)); + charsWritten++; + result[charsWritten + 1] = '.'; + } + + if (negative) + { + result[--start] = '-'; + charsWritten++; + } + + return new string(result.Slice(start, charsWritten)); + } +#else + internal unsafe static string GetString(in ArrowBuffer valueBuffer, int index, int precision, int scale, int byteWidth) + { + int startIndex = index * byteWidth; + ReadOnlySpan value = valueBuffer.Span.Slice(startIndex, byteWidth); + BigInteger integerValue = new BigInteger(value.ToArray()); + if (scale == 0) + { + return integerValue.ToString(); + } + + bool negative = integerValue.Sign < 0; + if (negative) + { + integerValue = -integerValue; + } + + string toString = integerValue.ToString(); + int charsWritten = toString.Length; + if (charsWritten > precision) + { + throw new OverflowException($"Value: {integerValue} cannot be formatted"); + } + + char[] result = new char[precision + 2]; + int pos = 0; + if (negative) + { + result[pos++] = '-'; + } + if (scale >= charsWritten) + { + result[pos++] = '0'; + result[pos++] = '.'; + int length = 0; + while (scale > charsWritten + length) + { + result[pos++] = '0'; + length++; + } + toString.CopyTo(0, result, pos, charsWritten); + pos += charsWritten; + } + else + { + int wholePartLength = charsWritten - scale; + toString.CopyTo(0, result, pos, wholePartLength); + pos += wholePartLength; + result[pos++] = '.'; + toString.CopyTo(wholePartLength, result, pos, scale); + pos += scale; + } + return new string(result, 0, pos); + } +#endif + #if !NETSTANDARD1_3 internal static SqlDecimal GetSqlDecimal128(in ArrowBuffer valueBuffer, int index, int precision, int scale) { @@ -199,6 +306,128 @@ internal static void GetBytes(decimal value, int precision, int scale, int byteW } } + internal static void GetBytes(string value, int precision, int scale, int byteWidth, Span bytes) + { + if (value == null || value.Length == 0) + { + throw new ArgumentException("numeric value may not be null or blank", nameof(value)); + } + + int start = 0; + if (value[0] == '-' || value[0] == '+') + { + start++; + } + while (value[start] == '0' && start < value.Length - 1) + { + start++; + } + + int pos = value.IndexOf('.'); + int neededPrecision = value.Length - start; + int neededScale; + if (pos == -1) + { + neededScale = 0; + } + else + { + neededPrecision--; + neededScale = value.Length - pos - 1; + } + + if (neededScale > scale) + { + throw new OverflowException($"Decimal scale cannot be greater than that in the Arrow vector: {value} has scale > {scale}"); + } + if (neededPrecision > precision) + { + throw new OverflowException($"Decimal precision cannot be greater than that in the Arrow vector: {value} has precision > {precision}"); + } + +#if NETCOREAPP + ReadOnlySpan src = value.AsSpan(); + Span buffer = stackalloc char[precision + start + 1]; + + int end; + if (pos == -1) + { + src.CopyTo(buffer); + end = src.Length; + } + else + { + src.Slice(0, pos).CopyTo(buffer); + src.Slice(pos + 1).CopyTo(buffer.Slice(pos)); + end = src.Length - 1; + } + + while (neededScale < scale) + { + buffer[end++] = '0'; + neededScale++; + } + + if (!BigInteger.TryParse(buffer.Slice(0, end), out BigInteger bigInt)) + { + throw new ArgumentException($"Unable to parse {value} as decimal"); + } + + if (!bigInt.TryWriteBytes(bytes, out int bytesWritten, false, !BitConverter.IsLittleEndian)) + { + throw new OverflowException("Could not extract bytes from integer value " + bigInt); + } +#else + char[] buffer = new char[precision + start + 1]; + + int end; + if (pos == -1) + { + value.CopyTo(0, buffer, 0, value.Length); + end = value.Length; + } + else + { + value.CopyTo(0, buffer, 0, pos); + value.CopyTo(pos + 1, buffer, pos, neededScale); + end = value.Length - 1; + } + + while (neededScale < scale) + { + buffer[end++] = '0'; + neededScale++; + } + + if (!BigInteger.TryParse(new string(buffer, 0, end), out BigInteger bigInt)) + { + throw new ArgumentException($"Unable to parse {value} as decimal"); + } + + byte[] tempBytes = bigInt.ToByteArray(); + try + { + tempBytes.CopyTo(bytes); + } + catch (ArgumentException) + { + throw new OverflowException("Could not extract bytes from integer value " + bigInt); + } + int bytesWritten = tempBytes.Length; +#endif + + if (bytes.Length > byteWidth) + { + throw new OverflowException($"Decimal size greater than {byteWidth} bytes: {bytes.Length}"); + } + + byte fill = bigInt.Sign == -1 ? (byte)255 : (byte)0; + for (int i = bytesWritten; i < byteWidth; i++) + { + bytes[i] = fill; + } + } + #if !NETSTANDARD1_3 internal static void GetBytes(SqlDecimal value, int precision, int scale, Span bytes) { diff --git a/csharp/test/Apache.Arrow.Tests/Decimal128ArrayTests.cs b/csharp/test/Apache.Arrow.Tests/Decimal128ArrayTests.cs index 8d7adfef42b54..497c9d2f6c6af 100644 --- a/csharp/test/Apache.Arrow.Tests/Decimal128ArrayTests.cs +++ b/csharp/test/Apache.Arrow.Tests/Decimal128ArrayTests.cs @@ -16,6 +16,7 @@ using System; #if !NETSTANDARD1_3 using System.Data.SqlTypes; +using System.Linq; #endif using Apache.Arrow.Types; using Xunit; @@ -378,6 +379,94 @@ public void AppendRangeSqlDecimal() Assert.Null(array.GetValue(range.Length)); } } + + public class Strings + { + [Theory] + [InlineData(200)] + public void AppendString(int count) + { + // Arrange + const int precision = 10; + var builder = new Decimal128Array.Builder(new Decimal128Type(14, precision)); + + // Act + string[] testData = new string[count]; + for (int i = 0; i < count; i++) + { + if (i == count - 2) + { + builder.AppendNull(); + testData[i] = null; + continue; + } + SqlDecimal rnd = i * (SqlDecimal)Math.Round(new Random().NextDouble(), 10); + builder.Append(rnd); + testData[i] = SqlDecimal.Round(rnd, precision).ToString(); + } + + // Assert + var array = builder.Build(); + Assert.Equal(count, array.Length); + for (int i = 0; i < count; i++) + { + if (testData[i] == null) + { + Assert.Null(array.GetString(i)); + Assert.Null(array.GetSqlDecimal(i)); + } + else + { + Assert.Equal(testData[i].TrimEnd('0'), array.GetString(i).TrimEnd('0')); + Assert.Equal(SqlDecimal.Parse(testData[i]), array.GetSqlDecimal(i)); + } + } + } + + [Fact] + public void AppendMaxAndMinSqlDecimal() + { + // Arrange + var builder = new Decimal128Array.Builder(new Decimal128Type(38, 0)); + + // Act + builder.Append(SqlDecimal.MaxValue.ToString()); + builder.Append(SqlDecimal.MinValue.ToString()); + string maxMinusTen = (SqlDecimal.MaxValue - 10).ToString(); + string minPlusTen = (SqlDecimal.MinValue + 10).ToString(); + builder.Append(maxMinusTen); + builder.Append(minPlusTen); + + // Assert + var array = builder.Build(); + Assert.Equal(SqlDecimal.MaxValue.ToString(), array.GetString(0)); + Assert.Equal(SqlDecimal.MinValue.ToString(), array.GetString(1)); + Assert.Equal(maxMinusTen, array.GetString(2)); + Assert.Equal(minPlusTen, array.GetString(3)); + } + + [Fact] + public void AppendRangeSqlDecimal() + { + // Arrange + var builder = new Decimal128Array.Builder(new Decimal128Type(24, 8)); + var range = new SqlDecimal[] { 2.123M, 1.5984M, -0.0000001M, 9878987987987987.1235407M }; + + // Act + builder.AppendRange(range.Select(d => d.ToString())); + builder.AppendNull(); + + // Assert + var array = builder.Build(); + for (int i = 0; i < range.Length; i++) + { + Assert.Equal(range[i], array.GetSqlDecimal(i)); + Assert.Equal(range[i].ToString(), array.GetString(i).TrimEnd('0')); + } + + Assert.Null(array.GetValue(range.Length)); + } + } #endif } } diff --git a/csharp/test/Apache.Arrow.Tests/Decimal256ArrayTests.cs b/csharp/test/Apache.Arrow.Tests/Decimal256ArrayTests.cs index e63c39d24eea8..3924c73a4e2f7 100644 --- a/csharp/test/Apache.Arrow.Tests/Decimal256ArrayTests.cs +++ b/csharp/test/Apache.Arrow.Tests/Decimal256ArrayTests.cs @@ -14,7 +14,10 @@ // limitations under the License. using System; -using System.Collections.Generic; +#if !NETSTANDARD1_3 +using System.Data.SqlTypes; +using System.Linq; +#endif using Apache.Arrow.Types; using Xunit; @@ -22,6 +25,25 @@ namespace Apache.Arrow.Tests { public class Decimal256ArrayTests { +#if !NETSTANDARD1_3 + static SqlDecimal? GetSqlDecimal(Decimal256Array array, int index) + { + SqlDecimal? result; + Assert.True(array.TryGetSqlDecimal(index, out result)); + return result; + } + + static SqlDecimal? Convert(decimal? value) + { + return value == null ? null : new SqlDecimal(value.Value); + } + + static decimal? Convert(SqlDecimal? value) + { + return value == null ? null : value.Value.Value; + } +#endif + public class Builder { public class AppendNull @@ -45,6 +67,12 @@ public void AppendThenGetGivesNull() Assert.Null(array.GetValue(0)); Assert.Null(array.GetValue(1)); Assert.Null(array.GetValue(2)); + +#if !NETSTANDARD1_3 + Assert.Null(GetSqlDecimal(array, 0)); + Assert.Null(GetSqlDecimal(array, 1)); + Assert.Null(GetSqlDecimal(array, 2)); +#endif } } @@ -78,6 +106,9 @@ public void AppendDecimal(int count) for (int i = 0; i < count; i++) { Assert.Equal(testData[i], array.GetValue(i)); +#if !NETSTANDARD1_3 + Assert.Equal(Convert(testData[i]), GetSqlDecimal(array, i)); +#endif } } @@ -95,6 +126,11 @@ public void AppendLargeDecimal() var array = builder.Build(); Assert.Equal(large, array.GetValue(0)); Assert.Equal(-large, array.GetValue(1)); + +#if !NETSTANDARD1_3 + Assert.Equal(Convert(large), GetSqlDecimal(array, 0)); + Assert.Equal(Convert(-large), GetSqlDecimal(array, 1)); +#endif } [Fact] @@ -115,6 +151,13 @@ public void AppendMaxAndMinDecimal() Assert.Equal(Decimal.MinValue, array.GetValue(1)); Assert.Equal(Decimal.MaxValue - 10, array.GetValue(2)); Assert.Equal(Decimal.MinValue + 10, array.GetValue(3)); + +#if !NETSTANDARD1_3 + Assert.Equal(Convert(Decimal.MaxValue), GetSqlDecimal(array, 0)); + Assert.Equal(Convert(Decimal.MinValue), GetSqlDecimal(array, 1)); + Assert.Equal(Convert(Decimal.MaxValue) - 10, GetSqlDecimal(array, 2)); + Assert.Equal(Convert(Decimal.MinValue) + 10, GetSqlDecimal(array, 3)); +#endif } [Fact] @@ -131,6 +174,11 @@ public void AppendFractionalDecimal() var array = builder.Build(); Assert.Equal(fraction, array.GetValue(0)); Assert.Equal(-fraction, array.GetValue(1)); + +#if !NETSTANDARD1_3 + Assert.Equal(Convert(fraction), GetSqlDecimal(array, 0)); + Assert.Equal(Convert(-fraction), GetSqlDecimal(array, 1)); +#endif } [Fact] @@ -149,8 +197,11 @@ public void AppendRangeDecimal() for(int i = 0; i < range.Length; i ++) { Assert.Equal(range[i], array.GetValue(i)); +#if !NETSTANDARD1_3 + Assert.Equal(Convert(range[i]), GetSqlDecimal(array, i)); +#endif } - + Assert.Null( array.GetValue(range.Length)); } @@ -256,6 +307,174 @@ public void SwapNull() Assert.Equal(123.456M, array.GetValue(1)); } } + +#if !NETSTANDARD1_3 + public class SqlDecimals + { + [Theory] + [InlineData(200)] + public void AppendSqlDecimal(int count) + { + // Arrange + const int precision = 10; + var builder = new Decimal256Array.Builder(new Decimal256Type(14, precision)); + + // Act + SqlDecimal?[] testData = new SqlDecimal?[count]; + for (int i = 0; i < count; i++) + { + if (i == count - 2) + { + builder.AppendNull(); + testData[i] = null; + continue; + } + SqlDecimal rnd = i * (SqlDecimal)Math.Round(new Random().NextDouble(), 10); + builder.Append(rnd); + testData[i] = SqlDecimal.Round(rnd, precision); + } + + // Assert + var array = builder.Build(); + Assert.Equal(count, array.Length); + for (int i = 0; i < count; i++) + { + Assert.Equal(testData[i], GetSqlDecimal(array, i)); + Assert.Equal(Convert(testData[i]), array.GetValue(i)); + } + } + + [Fact] + public void AppendMaxAndMinSqlDecimal() + { + // Arrange + var builder = new Decimal256Array.Builder(new Decimal256Type(38, 0)); + + // Act + builder.Append(SqlDecimal.MaxValue); + builder.Append(SqlDecimal.MinValue); + builder.Append(SqlDecimal.MaxValue - 10); + builder.Append(SqlDecimal.MinValue + 10); + + // Assert + var array = builder.Build(); + Assert.Equal(SqlDecimal.MaxValue, GetSqlDecimal(array, 0)); + Assert.Equal(SqlDecimal.MinValue, GetSqlDecimal(array, 1)); + Assert.Equal(SqlDecimal.MaxValue - 10, GetSqlDecimal(array, 2)); + Assert.Equal(SqlDecimal.MinValue + 10, GetSqlDecimal(array, 3)); + } + + [Fact] + public void AppendRangeSqlDecimal() + { + // Arrange + var builder = new Decimal256Array.Builder(new Decimal256Type(24, 8)); + var range = new SqlDecimal[] { 2.123M, 1.5984M, -0.0000001M, 9878987987987987.1235407M }; + + // Act + builder.AppendRange(range); + builder.AppendNull(); + + // Assert + var array = builder.Build(); + for (int i = 0; i < range.Length; i++) + { + Assert.Equal(range[i], GetSqlDecimal(array, i)); + Assert.Equal(Convert(range[i]), array.GetValue(i)); + } + + Assert.Null(array.GetValue(range.Length)); + } + } + + public class Strings + { + [Theory] + [InlineData(200)] + public void AppendString(int count) + { + // Arrange + const int precision = 10; + var builder = new Decimal256Array.Builder(new Decimal256Type(14, precision)); + + // Act + string[] testData = new string[count]; + for (int i = 0; i < count; i++) + { + if (i == count - 2) + { + builder.AppendNull(); + testData[i] = null; + continue; + } + SqlDecimal rnd = i * (SqlDecimal)Math.Round(new Random().NextDouble(), 10); + builder.Append(rnd); + testData[i] = SqlDecimal.Round(rnd, precision).ToString(); + } + + // Assert + var array = builder.Build(); + Assert.Equal(count, array.Length); + for (int i = 0; i < count; i++) + { + if (testData[i] == null) + { + Assert.Null(array.GetString(i)); + Assert.Null(GetSqlDecimal(array, i)); + } + else + { + Assert.Equal(testData[i].TrimEnd('0'), array.GetString(i).TrimEnd('0')); + Assert.Equal(SqlDecimal.Parse(testData[i]), GetSqlDecimal(array, i)); + } + } + } + + [Fact] + public void AppendMaxAndMinSqlDecimal() + { + // Arrange + var builder = new Decimal256Array.Builder(new Decimal256Type(38, 0)); + + // Act + builder.Append(SqlDecimal.MaxValue.ToString()); + builder.Append(SqlDecimal.MinValue.ToString()); + string maxMinusTen = (SqlDecimal.MaxValue - 10).ToString(); + string minPlusTen = (SqlDecimal.MinValue + 10).ToString(); + builder.Append(maxMinusTen); + builder.Append(minPlusTen); + + // Assert + var array = builder.Build(); + Assert.Equal(SqlDecimal.MaxValue.ToString(), array.GetString(0)); + Assert.Equal(SqlDecimal.MinValue.ToString(), array.GetString(1)); + Assert.Equal(maxMinusTen, array.GetString(2)); + Assert.Equal(minPlusTen, array.GetString(3)); + } + + [Fact] + public void AppendRangeSqlDecimal() + { + // Arrange + var builder = new Decimal256Array.Builder(new Decimal256Type(24, 8)); + var range = new SqlDecimal[] { 2.123M, 1.5984M, -0.0000001M, 9878987987987987.1235407M }; + + // Act + builder.AppendRange(range.Select(d => d.ToString())); + builder.AppendNull(); + + // Assert + var array = builder.Build(); + for (int i = 0; i < range.Length; i++) + { + Assert.Equal(range[i], GetSqlDecimal(array, i)); + Assert.Equal(range[i].ToString().TrimEnd('0'), array.GetString(i).TrimEnd('0')); + } + + Assert.Null(array.GetValue(range.Length)); + } + } +#endif } } } diff --git a/csharp/test/Apache.Arrow.Tests/DecimalUtilityTests.cs b/csharp/test/Apache.Arrow.Tests/DecimalUtilityTests.cs index dd5f7b9d3f67f..677e9b6cadfcf 100644 --- a/csharp/test/Apache.Arrow.Tests/DecimalUtilityTests.cs +++ b/csharp/test/Apache.Arrow.Tests/DecimalUtilityTests.cs @@ -121,5 +121,90 @@ public void LargeScale() } #endif } + + public class Strings + { + [Theory] + [InlineData(100.12, 10, 2, "100.12")] + [InlineData(100.12, 8, 3, "100.120")] + [InlineData(100.12, 7, 4, "100.1200")] + [InlineData(.12, 6, 3, "0.120")] + [InlineData(.0012, 5, 4, "0.0012")] + [InlineData(-100.12, 10, 2, "-100.12")] + [InlineData(-100.12, 8, 3, "-100.120")] + [InlineData(-100.12, 7, 4, "-100.1200")] + [InlineData(-.12, 6, 3, "-0.120")] + [InlineData(-.0012, 5, 4, "-0.0012")] + [InlineData(7.89, 76, 38, "7.89000000000000000000000000000000000000")] + public void FromDecimal(decimal d, int precision, int scale, string result) + { + if (precision <= 38) + { + TestFromDecimal(d, precision, scale, 16, result); + } + TestFromDecimal(d, precision, scale, 32, result); + } + + private void TestFromDecimal(decimal d, int precision, int scale, int byteWidth, string result) + { + var bytes = new byte[byteWidth]; + DecimalUtility.GetBytes(d, precision, scale, byteWidth, bytes); + Assert.Equal(result, DecimalUtility.GetString(new ArrowBuffer(bytes), 0, precision, scale, byteWidth)); + } + + [Theory] + [InlineData("100.12", 10, 2, "100.12")] + [InlineData("100.12", 8, 3, "100.120")] + [InlineData("100.12", 7, 4, "100.1200")] + [InlineData(".12", 6, 3, "0.120")] + [InlineData(".0012", 5, 4, "0.0012")] + [InlineData("-100.12", 10, 2, "-100.12")] + [InlineData("-100.12", 8, 3, "-100.120")] + [InlineData("-100.12", 7, 4, "-100.1200")] + [InlineData("-.12", 6, 3, "-0.120")] + [InlineData("-.0012", 5, 4, "-0.0012")] + [InlineData("+.0012", 5, 4, "0.0012")] + [InlineData("99999999999999999999999999999999999999", 38, 0, "99999999999999999999999999999999999999")] + [InlineData("-99999999999999999999999999999999999999", 38, 0, "-99999999999999999999999999999999999999")] + public void FromString(string s, int precision, int scale, string result) + { + TestFromString(s, precision, scale, 16, result); + TestFromString(s, precision, scale, 32, result); + } + + [Fact] + public void ThroughDecimal256() + { + var seventysix = new string('9', 76); + TestFromString(seventysix, 76, 0, 32, seventysix); + TestFromString("0000" + seventysix, 76, 0, 32, seventysix); + + seventysix = "-" + seventysix; + TestFromString(seventysix, 76, 0, 32, seventysix); + + var seventyseven = new string('9', 77); + Assert.Throws(() => TestFromString(seventyseven, 76, 0, 32, seventyseven)); + } + + private void TestFromString(string s, int precision, int scale, int byteWidth, string result) + { + var bytes = new byte[byteWidth]; + DecimalUtility.GetBytes(s, precision, scale, byteWidth, bytes); + Assert.Equal(result, DecimalUtility.GetString(new ArrowBuffer(bytes), 0, precision, scale, byteWidth)); + } + + [Theory] + [InlineData("", 10, 2, 16, typeof(ArgumentException))] + [InlineData("", 10, 2, 32, typeof(ArgumentException))] + [InlineData(null, 10, 2, 32, typeof(ArgumentException))] + [InlineData("1.23", 10, 1, 16, typeof(OverflowException))] + [InlineData("12345678901234567890", 24, 1, 8, typeof(OverflowException))] + [InlineData("abc", 24, 1, 8, typeof(ArgumentException))] + public void ParseErrors(string s, int precision, int scale, int byteWidth, Type exceptionType) + { + byte[] bytes = new byte[byteWidth]; + Assert.Throws(exceptionType, () => DecimalUtility.GetBytes(s, precision, scale, byteWidth, bytes)); + } + } } }