diff --git a/src/Clast.DatabaseDecimal/DecimalType.cs b/src/Clast.DatabaseDecimal/DecimalType.cs
index e6da690..831e2a2 100644
--- a/src/Clast.DatabaseDecimal/DecimalType.cs
+++ b/src/Clast.DatabaseDecimal/DecimalType.cs
@@ -1,6 +1,8 @@
// Copyright (c) clast-project. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+using System.Runtime.CompilerServices;
+
namespace Clast.DatabaseDecimal;
///
@@ -9,7 +11,12 @@ namespace Clast.DatabaseDecimal;
/// Scale is the number of digits after the decimal point (0..precision).
/// The backing integer width is derived from the precision.
///
-public readonly record struct DecimalType(byte Precision, byte Scale)
+///
+/// Every constructed value is validated, so is never
+/// negative. The one unvalidated value is default(DecimalType), which a
+/// struct always permits: it has a precision of 0 and behaves as NUMERIC(0,0).
+///
+public readonly record struct DecimalType
{
/// Max digits for a 32-bit mantissa: floor(log10(2^31)) = 9.
public const int MaxPrecision32 = 9;
@@ -23,6 +30,32 @@ public readonly record struct DecimalType(byte Precision, byte Scale)
/// Max digits for a 256-bit mantissa: floor(log10(2^255)) = 76.
public const int MaxPrecision256 = 76;
+ ///
+ /// Creates a DecimalType, validating precision and scale.
+ ///
+ /// Total number of significant digits, 1..76.
+ /// Digits after the decimal point, 0...
+ ///
+ /// The precision is outside 1..76, or the scale exceeds the precision.
+ ///
+ public DecimalType(byte precision, byte scale)
+ {
+ if (precision < 1 || precision > MaxPrecision256)
+ ThrowPrecisionOutOfRange(precision);
+
+ if (scale > precision)
+ ThrowScaleOutOfRange(precision, scale);
+
+ Precision = precision;
+ Scale = scale;
+ }
+
+ /// The total number of significant digits, 1..76.
+ public byte Precision { get; }
+
+ /// The number of digits after the decimal point, 0...
+ public byte Scale { get; }
+
///
/// The backing integer width tier, derived from the precision.
///
@@ -40,20 +73,40 @@ public readonly record struct DecimalType(byte Precision, byte Scale)
public int IntegerDigits => Precision - Scale;
///
- /// Creates a DecimalType with validation.
+ /// Creates a DecimalType with validation. Equivalent to the constructor, but
+ /// takes arguments so out-of-range values are rejected rather
+ /// than silently truncated by the conversion to .
///
+ ///
+ /// The precision is outside 1..76, or the scale is negative or exceeds the precision.
+ ///
public static DecimalType Numeric(int precision, int scale)
{
if (precision < 1 || precision > MaxPrecision256)
- throw new ArgumentOutOfRangeException(nameof(precision),
- $"Precision must be between 1 and {MaxPrecision256}, got {precision}.");
+ ThrowPrecisionOutOfRange(precision);
if (scale < 0 || scale > precision)
- throw new ArgumentOutOfRangeException(nameof(scale),
- $"Scale must be between 0 and precision ({precision}), got {scale}.");
+ ThrowScaleOutOfRange(precision, scale);
return new DecimalType((byte)precision, (byte)scale);
}
+ /// Splits the type into its precision and scale.
+ public void Deconstruct(out byte precision, out byte scale)
+ {
+ precision = Precision;
+ scale = Scale;
+ }
+
public override string ToString() => $"NUMERIC({Precision},{Scale})";
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static void ThrowPrecisionOutOfRange(int precision) =>
+ throw new ArgumentOutOfRangeException(nameof(precision),
+ $"Precision must be between 1 and {MaxPrecision256}, got {precision}.");
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static void ThrowScaleOutOfRange(int precision, int scale) =>
+ throw new ArgumentOutOfRangeException(nameof(scale),
+ $"Scale must be between 0 and precision ({precision}), got {scale}.");
}
diff --git a/tests/Clast.DatabaseDecimal.Tests/DecimalTypeTests.cs b/tests/Clast.DatabaseDecimal.Tests/DecimalTypeTests.cs
index 6ea125b..75584fb 100644
--- a/tests/Clast.DatabaseDecimal.Tests/DecimalTypeTests.cs
+++ b/tests/Clast.DatabaseDecimal.Tests/DecimalTypeTests.cs
@@ -75,4 +75,51 @@ public void Inequality_DifferentScale()
var b = DecimalType.Numeric(5, 3);
Assert.NotEqual(a, b);
}
+
+ [Fact]
+ public void Constructor_ScaleExceedsPrecision_Throws()
+ {
+ var ex = Assert.Throws(() => new DecimalType(10, 30));
+ Assert.Equal("scale", ex.ParamName);
+ }
+
+ [Fact]
+ public void Constructor_PrecisionZero_Throws()
+ {
+ var ex = Assert.Throws(() => new DecimalType(0, 0));
+ Assert.Equal("precision", ex.ParamName);
+ }
+
+ [Fact]
+ public void Constructor_PrecisionAboveMax_Throws()
+ {
+ var ex = Assert.Throws(() => new DecimalType(77, 0));
+ Assert.Equal("precision", ex.ParamName);
+ }
+
+ [Fact]
+ public void Constructor_MatchesNumeric()
+ {
+ Assert.Equal(DecimalType.Numeric(18, 4), new DecimalType(18, 4));
+ }
+
+ [Fact]
+ public void Deconstruct_YieldsPrecisionAndScale()
+ {
+ var (precision, scale) = DecimalType.Numeric(18, 4);
+ Assert.Equal(18, precision);
+ Assert.Equal(4, scale);
+ }
+
+ [Fact]
+ public void IntegerDigits_NeverNegative_ForConstructibleTypes()
+ {
+ for (int precision = 1; precision <= DecimalType.MaxPrecision256; precision++)
+ {
+ for (int scale = 0; scale <= precision; scale++)
+ Assert.True(DecimalType.Numeric(precision, scale).IntegerDigits >= 0);
+
+ Assert.Throws(() => DecimalType.Numeric(precision, precision + 1));
+ }
+ }
}