Skip to content

Commit

Permalink
Implement INumber on Amount (#370)
Browse files Browse the repository at this point in the history
  • Loading branch information
Corniel committed Feb 3, 2024
1 parent 428eb12 commit d6ce00f
Show file tree
Hide file tree
Showing 12 changed files with 614 additions and 353 deletions.
1 change: 1 addition & 0 deletions .editorconfig
Expand Up @@ -51,6 +51,7 @@ dotnet_diagnostic.NUnit2005.severity = suggestion # Consider using Assert.That(a
dotnet_diagnostic.NUnit2021.severity = warning # Incompatible types for EqualTo constraint

dotnet_diagnostic.QW0001.severity = none # Use a testable Time Provider
dotnet_diagnostic.QW0010.severity = none # Use DateOnly instead of Date
dotnet_diagnostic.QW0003.severity = warning # Decorate Pure functions

dotnet_diagnostic.SA1001.severity = none # Commas should not be preceded by whitespace
Expand Down
174 changes: 174 additions & 0 deletions specs/Qowaiv.Specs/Financial/Amount_INumber_specs.cs
@@ -0,0 +1,174 @@
#if NET8_0_OR_GREATER

using Qowaiv.TestTools.Numerics;

namespace Financial.Amount_specs;

public class Defines
{
[TestCase(-34, 30, -4)]
[TestCase(+34, 30, +4)]
[TestCase(+34, 02, +0)]
public void modulo(Amount p, Amount mod, Amount remainder)
=> (p % mod).Should().Be(remainder);
}

public class Amount_as_INumber
{
[Test]
public void radixis_10()
=> Number.Radix<Amount>().Should().Be(10);

[Test]
public void Additive_identityIs_1()
=> Number.AdditiveIdentity<Amount>().Should().Be(1.Amount());

[Test]
public void Multiplicative_identityis_1()
=> Number.MultiplicativeIdentity<Amount>().Should().Be(1.Amount());

[Test]
public void Is_canonical_equal_to_decimal([Random(Min, Max, Count)] decimal d)
{
Number.IsCanonical(d.Amount()).Should().Be(Number.IsCanonical(d));
}

[Test]
public void Abs_equal_to_Amount_Abs([Random(Min, Max, Count)] decimal d)
{
Number.Abs(d.Amount()).Should().Be(d.Amount().Abs());
}

[Test]
public void is_never_a_complexnumber([Random(Min, Max, Count)] decimal d)
=> Number.IsComplexNumber(d.Amount()).Should().BeFalse();

[TestCase(4, true)]
[TestCase(9, true)]
[TestCase(1, true)]
[TestCase(-1, true)]
[TestCase(-2, true)]
[TestCase(3.3, false)]
[TestCase(-4.4, false)]
public void is_integer(Amount p, bool isEvenInteger)
=> Number.IsInteger(p).Should().Be(isEvenInteger);

[TestCase(4, true)]
[TestCase(8, true)]
[TestCase(0, true)]
[TestCase(-2, true)]
[TestCase(-1, false)]
[TestCase(2.2, false)]
public void is_even_integer(Amount p, bool isEvenInteger)
=> Number.IsEvenInteger(p).Should().Be(isEvenInteger);

[TestCase(5, true)]
[TestCase(9, true)]
[TestCase(1, true)]
[TestCase(-1, true)]
[TestCase(-2, false)]
[TestCase(3.3, false)]
public void is_odd_integer(Amount p, bool isEvenInteger)
=> Number.IsOddInteger(p).Should().Be(isEvenInteger);

[Test]
public void is_always_real([Random(Min, Max, Count)] decimal d)
=> Number.IsRealNumber(d.Amount()).Should().BeTrue();

[Test]
public void is_never_complex([Random(Min, Max, Count)] decimal d)
=> Number.IsComplexNumber(d.Amount()).Should().BeFalse();

[Test]
public void is_never_imaginary([Random(Min, Max, Count)] decimal d)
=> Number.IsImaginaryNumber(d.Amount()).Should().BeFalse();

[Test]
public void is_always_finate([Random(Min, Max, Count)] decimal d)
=> Number.IsFinite(d.Amount()).Should().BeTrue();

[Test]
public void is_never_infinite([Random(Min, Max, Count)] decimal d)
{
Number.IsInfinity(d.Amount()).Should().BeFalse();
Number.IsNegativeInfinity(d.Amount()).Should().BeFalse();
Number.IsPositiveInfinity(d.Amount()).Should().BeFalse();
}

[Test]
public void is_never_NaN([Random(Min, Max, Count)] decimal d)
=> Number.IsNaN(d.Amount()).Should().BeFalse();

[Test]
public void is_negative_equal_to_decimal([Random(Min, Max, Count)] decimal d)
{
Number.IsNegative(d.Amount()).Should().Be(Number.IsNegative(d));
}

[Test]
public void zero_is_positive_and_not_negative()
{
Number.IsPositive(Amount.Zero).Should().BeTrue();
Number.IsNegative(Amount.Zero).Should().BeFalse();

Number.IsPositive(0m).Should().BeTrue();
Number.IsNegative(0m).Should().BeFalse();
}

[Test]
public void is_zero_is_false_for_all_but_zero([Random(Min, Max, Count)] decimal d)
{
Number.IsZero(d.Amount()).Should().BeFalse();
}

[Test]
public void is_positive_equal_to_decimal([Random(Min, Max, Count)] decimal d)
{
Number.IsPositive(d.Amount()).Should().Be(Number.IsPositive(d.Amount()));
}

[Test]
public void Is_not_Normal_when_zero()
=> Number.IsNormal(Amount.Zero).Should().BeFalse();

[Test]
public void Is_never_subnormal([Random(Min, Max, Count)] decimal d)
=> Number.IsSubnormal(d.Amount()).Should().BeFalse();

[Test]
public void Is_normal_when_not_zero([Random(Min, Max, Count)] decimal d)
{
Number.IsNormal(d.Amount()).Should().BeTrue();
}

[Test]
public void maxmaginiute_equal_to_decimal([Random(Min, Max, 3)] decimal x, [Random(Min, Max, 3)] decimal y)
{
Number.MaxMagnitude(x.Amount(), y.Amount()).Should().Be(Number.MaxMagnitude(x, y).Amount());
Number.MaxMagnitudeNumber(x.Amount(), y.Amount()).Should().Be(Number.MaxMagnitudeNumber(x, y).Amount());
}

[Test]
public void min_maginiute_equal_to_decimal([Random(Min, Max, 3)] decimal x, [Random(Min, Max, 3)] decimal y)
{
Number.MinMagnitude(x.Amount(), y.Amount()).Should().Be(Number.MinMagnitude(x, y).Amount());
Number.MinMagnitudeNumber(x.Amount(), y.Amount()).Should().Be((Amount)Number.MinMagnitudeNumber(x, y));
}

[Test]
public void multiplication_is_supported_via_explicit_contract_only([Random(-100_000, +100_000, 3)] decimal x, [Random(-100_000, +100_000, 3)] decimal y)
{
Number.Multiply(x.Amount(), y.Amount()).Should().Be((x * y).Amount());
}

[Test]
public void division_is_supported_via_explicit_contract_only([Random(Min, Max, 3)] decimal x, [Random(Min, Max, 3)] decimal y)
{
Number.Divide(x.Amount(), y.Amount()).Should().Be((x / y).Amount());
}

private const double Min = -79228162514264337593543950335d;
private const double Max = +79228162514264337593543950335d;
private const int Count = 8;
}
#endif
50 changes: 49 additions & 1 deletion specs/Qowaiv.Specs/Financial/Amount_specs.cs
Expand Up @@ -24,14 +24,30 @@ public void Min_of_collection_returns_minimum()
[TestCase(17, 17)]
[TestCase(17, 16)]
[TestCase(16, 17)]
public void Max_of_two_returns_minimum(Amount a1, Amount a2)
public void Max_of_two_returns_minimum(Amount a1, Amount a2)
=> Amount.Max(a1, a2).Should().Be(17.Amount());

[Test]
public void Max_of_collection_returns_maximum()
=> Amount.Max(7.Amount(), 17.Amount(), -48.Amount()).Should().Be(17.Amount());
}

public class Can_not_be_parsed
{
[TestCase(NumberStyles.Number)]
[TestCase(NumberStyles.Integer)]
public void strings_with_currency_if_style_does_not_allow_currency_sign(NumberStyles style)
=> Amount.TryParse("42 EUR", style, CultureInfo.InvariantCulture, out _)
.Should().BeFalse();

[TestCase(NumberStyles.HexNumber)]
[TestCase(NumberStyles.AllowExponent)]
public void using_a_number_style_other_then_Curency(NumberStyles style)
=> style.Invoking(s => Amount.TryParse("4.50", s, CultureInfo.InvariantCulture, out _))
.Should().Throw<ArgumentOutOfRangeException>()
.WithMessage("The number style '*' is not supported.*");
}

public class Is_comparable
{
[Test]
Expand Down Expand Up @@ -120,6 +136,38 @@ public void throws_for_invalid_json(object json, Type exceptionType)
.And.Should().BeOfType(exceptionType);
}

public class Casts_explicit
{
public class From
{
[Test]
public void Int32() => ((Amount)42).Should().Be(42.Amount());

[Test]
public void Int64() => ((Amount)42L).Should().Be(42.Amount());

[Test]
public void Decimal() => ((Amount)42.0m).Should().Be(42.Amount());

[Test]
public void Double() => ((Amount)42.0).Should().Be(42.Amount());
}

public class To
{
[Test]
public void Int32() => ((int)42.Amount()).Should().Be(42);

[Test]
public void Int64() => ((long)42.Amount()).Should().Be(42L);

[Test]
public void Decimal() => ((decimal)42.Amount()).Should().Be(42m);

[Test]
public void Double() => ((double)42.Amount()).Should().Be(42.0);
}
}
public class Has_operators
{
[Test]
Expand Down
9 changes: 9 additions & 0 deletions specs/Qowaiv.Specs/Local_date_time_specs.cs
@@ -1,5 +1,14 @@
namespace Local_date_time_specs;

public class Has_constant
{
[Test]
public void MinValue_is_0001Y_01M_01D() => LocalDateTime.MinValue.Should().Be(new(0001, 01, 01));

[Test]
public void MaxValue_equal_to_9999Y_12M_31D() => LocalDateTime.MaxValue.Should().Be(new(DateTime.MaxValue.Ticks));
}

public class Is_invalid
{
[Test]
Expand Down
4 changes: 1 addition & 3 deletions specs/Qowaiv.Specs/OpenApi/Open_API_specs.cs
@@ -1,6 +1,4 @@
using OpenApiDataTypeAttribute = Qowaiv.OpenApi.OpenApiDataTypeAttribute;

namespace Open_API_specs;
namespace Open_API_specs;

public class Open_API_data_type
{
Expand Down
11 changes: 0 additions & 11 deletions specs/Qowaiv.Specs/Svo_numeric_contract_specs.cs
Expand Up @@ -25,7 +25,6 @@ public void Abs(Type svo)
methods.Should().ContainSingle();
}

[TestCase(typeof(Amount))]
[TestCase(typeof(Money))]
[TestCase(typeof(StreamSize))]
public void Plus(Type svo)
Expand All @@ -52,7 +51,6 @@ public void Plus(Type svo)
operators.Should().ContainSingle();
}

[TestCase(typeof(Amount))]
[TestCase(typeof(Money))]
[TestCase(typeof(StreamSize))]
public void Negate(Type svo)
Expand All @@ -79,7 +77,6 @@ public void Negate(Type svo)
operators.Should().ContainSingle();
}

[TestCase(typeof(Amount))]
[TestCase(typeof(Money))]
[TestCase(typeof(StreamSize))]
public void Increment(Type svo)
Expand All @@ -106,7 +103,6 @@ public void Increment(Type svo)
operators.Should().ContainSingle();
}

[TestCase(typeof(Amount))]
[TestCase(typeof(Money))]
[TestCase(typeof(StreamSize))]
public void Decrement(Type svo)
Expand All @@ -133,7 +129,6 @@ public void Decrement(Type svo)
operators.Should().ContainSingle();
}

[TestCase(typeof(Amount), typeof(Amount), typeof(Percentage))]
[TestCase(typeof(Money), typeof(Money), typeof(Percentage))]
[TestCase(typeof(StreamSize), typeof(StreamSize), typeof(Percentage))]
public void Add(Type svo, params Type[] expected)
Expand Down Expand Up @@ -162,7 +157,6 @@ public void Add(Type svo, params Type[] expected)
operators.Should().BeEquivalentTo(expected);
}

[TestCase(typeof(Amount), typeof(Amount), typeof(Percentage))]
[TestCase(typeof(Money), typeof(Money), typeof(Percentage))]
[TestCase(typeof(StreamSize), typeof(StreamSize), typeof(Percentage))]
public void Subtract(Type svo, params Type[] expected)
Expand Down Expand Up @@ -191,7 +185,6 @@ public void Subtract(Type svo, params Type[] expected)
operators.Should().BeEquivalentTo(expected);
}

[TestCase(typeof(Amount), typeof(short), typeof(int), typeof(long), typeof(ushort), typeof(uint), typeof(ulong), typeof(float), typeof(double), typeof(decimal), typeof(Percentage))]
[TestCase(typeof(Money), typeof(short), typeof(int), typeof(long), typeof(ushort), typeof(uint), typeof(ulong), typeof(float), typeof(double), typeof(decimal), typeof(Percentage))]
[TestCase(typeof(StreamSize), typeof(short), typeof(int), typeof(long), typeof(ushort), typeof(uint), typeof(ulong), typeof(float), typeof(double), typeof(decimal), typeof(Percentage))]
public void Multiply(Type svo, params Type[] expected)
Expand Down Expand Up @@ -220,7 +213,6 @@ public void Multiply(Type svo, params Type[] expected)
operators.Should().BeEquivalentTo(expected);
}

[TestCase(typeof(Amount), typeof(short), typeof(int), typeof(long), typeof(ushort), typeof(uint), typeof(ulong), typeof(float), typeof(double), typeof(decimal), typeof(Percentage))]
[TestCase(typeof(Money), typeof(short), typeof(int), typeof(long), typeof(ushort), typeof(uint), typeof(ulong), typeof(float), typeof(double), typeof(decimal), typeof(Percentage))]
[TestCase(typeof(StreamSize), typeof(short), typeof(int), typeof(long), typeof(ushort), typeof(uint), typeof(ulong), typeof(float), typeof(double), typeof(decimal), typeof(Percentage))]
public void Divide(Type svo, params Type[] expected)
Expand Down Expand Up @@ -249,7 +241,6 @@ public void Divide(Type svo, params Type[] expected)
operators.Should().BeEquivalentTo(expected);
}

[TestCase(typeof(Amount))]
[TestCase(typeof(Money))]
[TestCase(typeof(Percentage))]
public void Round(Type svo)
Expand All @@ -265,7 +256,6 @@ public void Round(Type svo)
methods.Should().ContainSingle();
}

[TestCase(typeof(Amount))]
[TestCase(typeof(Money))]
[TestCase(typeof(Percentage))]
public void Round_Decimals(Type svo)
Expand All @@ -281,7 +271,6 @@ public void Round_Decimals(Type svo)
methods.Should().ContainSingle();
}

[TestCase(typeof(Amount))]
[TestCase(typeof(Money))]
[TestCase(typeof(Percentage))]
public void Round_Decimals_DecimalRounding(Type svo)
Expand Down
10 changes: 10 additions & 0 deletions src/Qowaiv.TestTools/Numerics/Number.cs
Expand Up @@ -116,6 +116,16 @@ public static class Number
public static bool IsImaginaryNumber<T>(T number) where T : INumberBase<T>
=> T.IsImaginaryNumber(number);

/// <summary>Defines a mechanism for computing the product of two values.</summary>
[Pure]
public static T Multiply<T>(T x, T y) where T : IMultiplyOperators<T, T, T>
=> x * y;

/// <summary>Divides two values together to compute their quotient.</summary>
[Pure]
public static T Divide<T>(T x, T y) where T : IDivisionOperators<T, T, T>
=> x / y;

/// <inheritdoc cref="INumberBase{T}.MaxMagnitude(T, T)" />
[Pure]
public static T MaxMagnitude<T>(T x, T y) where T : INumberBase<T>
Expand Down

0 comments on commit d6ce00f

Please sign in to comment.