From b304cb1d2a928e54a7f5f82328e406fb429ac378 Mon Sep 17 00:00:00 2001 From: Corniel Nobel Date: Sat, 10 Feb 2024 17:45:31 +0100 Subject: [PATCH] Implement INumber. Improve on specs. --- .../Mathematics/Fraction_INumber_specs.cs | 144 +++++ .../Mathematics/Fraction_specs.cs | 596 +++++++++++++++++- .../TestTools/RandomFractionAttribute.cs | 31 + .../Mathematics/FractionOperationsTest.cs | 210 ------ .../_Legacy/Mathematics/FractionTest.cs | 359 ----------- .../Mathematics/FractionTypeConverter.cs | 24 + src/Qowaiv/Hashing/Hash.cs | 17 +- src/Qowaiv/Mathematics/Fraction.INumber.cs | 361 +++++++++++ src/Qowaiv/Mathematics/Fraction.cs | 252 +------- 9 files changed, 1166 insertions(+), 828 deletions(-) create mode 100644 specs/Qowaiv.Specs/Mathematics/Fraction_INumber_specs.cs create mode 100644 specs/Qowaiv.Specs/TestTools/RandomFractionAttribute.cs delete mode 100644 specs/Qowaiv.Specs/_Legacy/Mathematics/FractionOperationsTest.cs delete mode 100644 specs/Qowaiv.Specs/_Legacy/Mathematics/FractionTest.cs create mode 100644 src/Qowaiv/Mathematics/Fraction.INumber.cs diff --git a/specs/Qowaiv.Specs/Mathematics/Fraction_INumber_specs.cs b/specs/Qowaiv.Specs/Mathematics/Fraction_INumber_specs.cs new file mode 100644 index 000000000..66ef90b0b --- /dev/null +++ b/specs/Qowaiv.Specs/Mathematics/Fraction_INumber_specs.cs @@ -0,0 +1,144 @@ +#if NET8_0_OR_GREATER + +using Qowaiv.Specs.TestTools; +using Qowaiv.TestTools.Numerics; + +namespace Mathematics.Fraction_specs; + +public class Fraction_as_INumber +{ + [Test] + public void radixis_10() + => Number.Radix().Should().Be(10); + + [Test] + public void Additive_identityIs_1() + => Number.AdditiveIdentity().Should().Be(Fraction.One); + + [Test] + public void Multiplicative_identityis_1() + => Number.MultiplicativeIdentity().Should().Be(Fraction.One); + + [Test] + public void Is_always_canonical([RandomFraction(Count)] Fraction fraction) + => Number.IsCanonical(fraction).Should().BeTrue(); + + [Test] + public void Abs_equal_to_Fraction_Abs([RandomFraction(Count)] Fraction fraction) + => Number.Abs(fraction).Should().Be(Number.Abs((decimal)fraction).Fraction()); + + [Test] + public void is_never_a_complexnumber([RandomFraction(Count)] Fraction fraction) + => Number.IsComplexNumber(fraction).Should().BeFalse(); + + [TestCase(4, true)] + [TestCase(9, true)] + [TestCase(1, true)] + [TestCase(-1, true)] + [TestCase(-2, true)] + [TestCase("3/5", false)] + [TestCase("-3/4", false)] + public void is_integer(Fraction fraction, bool isEvenInteger) + => Number.IsInteger(fraction).Should().Be(isEvenInteger); + + [TestCase(4, true)] + [TestCase(8, true)] + [TestCase(0, true)] + [TestCase(-2, true)] + [TestCase(-1, false)] + [TestCase("3/5", false)] + [TestCase("-3/4", false)] + public void is_even_integer(Fraction fraction, bool isEvenInteger) + => Number.IsEvenInteger(fraction).Should().Be(isEvenInteger); + + [TestCase(5, true)] + [TestCase(9, true)] + [TestCase(1, true)] + [TestCase(-1, true)] + [TestCase(-2, false)] + [TestCase("3/5", false)] + [TestCase("-3/4", false)] + public void is_odd_integer(Fraction p, bool isEvenInteger) + => Number.IsOddInteger(p).Should().Be(isEvenInteger); + + [Test] + public void is_always_real([RandomFraction(Count)] Fraction fraction) + => Number.IsRealNumber(fraction).Should().BeTrue(); + + [Test] + public void is_never_complex([RandomFraction(Count)] Fraction fraction) + => Number.IsComplexNumber(fraction).Should().BeFalse(); + + [Test] + public void is_never_imaginary([RandomFraction(Count)] Fraction fraction) + => Number.IsImaginaryNumber(fraction).Should().BeFalse(); + + [Test] + public void is_always_finate([RandomFraction(Count)] Fraction fraction) + => Number.IsFinite(fraction).Should().BeTrue(); + + [Test] + public void is_never_infinite([RandomFraction(Count)] Fraction fraction) + { + Number.IsInfinity(fraction).Should().BeFalse(); + Number.IsNegativeInfinity(fraction).Should().BeFalse(); + Number.IsPositiveInfinity(fraction).Should().BeFalse(); + } + + [Test] + public void is_never_NaN([RandomFraction(Count)] Fraction fraction) + => Number.IsNaN(fraction).Should().BeFalse(); + + [Test] + public void is_negative_equal_to_decimal([RandomFraction(Count)] Fraction fraction) + => Number.IsNegative(fraction).Should().Be(Number.IsNegative((decimal)fraction)); + + [Test] + public void zero_is_positive_and_not_negative() + { + Number.IsPositive(Fraction.Zero).Should().BeTrue(); + Number.IsNegative(Fraction.Zero).Should().BeFalse(); + + Number.IsPositive(0m).Should().BeTrue(); + Number.IsNegative(0m).Should().BeFalse(); + } + + [Test] + public void is_zero_is_false_for_all_but_zero([RandomFraction(Count)] Fraction fraction) + => Number.IsZero(fraction).Should().BeFalse(); + + [Test] + public void is_positive_equal_to_decimal([RandomFraction(Count)] Fraction fraction) + => Number.IsPositive(fraction).Should().Be(Number.IsPositive((decimal)fraction)); + + [Test] + public void Is_always_normal([RandomFraction(Count)] Fraction fraction) + => Number.IsNormal(fraction).Should().BeTrue(); + + [Test] + public void Is_never_subnormal([RandomFraction(Count)] Fraction fraction) + => Number.IsSubnormal(fraction).Should().BeFalse(); + + [Test] + public void maxmaginiute_equal_to_decimal([RandomFraction(3)] Fraction x, [RandomFraction(3)] Fraction y) + { + var x_ = (decimal)x.Numerator / x.Denominator; + var y_ = (decimal)y.Numerator / y.Denominator; + + Number.MaxMagnitude(x, y).Should().Be(Number.MaxMagnitude(x_, y_).Fraction()); + Number.MaxMagnitudeNumber(x, y).Should().Be(Number.MaxMagnitudeNumber(x_, y_).Fraction()); + } + + [Test] + public void min_maginiute_equal_to_decimal([RandomFraction(3)] Fraction x, [RandomFraction(3)] Fraction y) + { + var x_ = (decimal)x.Numerator / x.Denominator; + var y_ = (decimal)y.Numerator / y.Denominator; + + Number.MinMagnitude(x, y).Should().Be(Number.MinMagnitude(x_, y_).Fraction()); + Number.MinMagnitudeNumber(x, y).Should().Be(Number.MinMagnitudeNumber(x_, y_).Fraction()); + } + + private const int Count = 8; +} +#endif diff --git a/specs/Qowaiv.Specs/Mathematics/Fraction_specs.cs b/specs/Qowaiv.Specs/Mathematics/Fraction_specs.cs index c0aba9667..905280577 100644 --- a/specs/Qowaiv.Specs/Mathematics/Fraction_specs.cs +++ b/specs/Qowaiv.Specs/Mathematics/Fraction_specs.cs @@ -1,6 +1,315 @@ -namespace Mathematics.Fraction_specs; +using Qowaiv.Specs.TestTools; -public class Overflows_when +namespace Mathematics.Fraction_specs; + +public class Defines +{ + [TestCase("0", "0")] + [TestCase("17/3", "17/3")] + [TestCase("-7/3", "-7/3")] + public void plus(Fraction fraction, Fraction negated) => (+fraction).Should().Be(negated); + + [TestCase("0", "0")] + [TestCase("-7/3", "+7/3")] + [TestCase("+7/3", "-7/3")] + public void negation(Fraction fraction, Fraction negated) => (-fraction).Should().Be(negated); + + [Test] + public void increment() + { + var fraction = Svo.Fraction; + fraction++; + fraction.Should().Be(-52.DividedBy(17)); + } + + [Test] + public void decrement() + { + var fraction = Svo.Fraction; + fraction--; + fraction.Should().Be(-86.DividedBy(17)); + } + + public class Multiplication + { + [TestCase("1/3", "0", "0")] + [TestCase("0", "5/7", "0")] + [TestCase("1/3", "1/4", "1/12")] + [TestCase("-1/3", "-1/4", "1/12")] + [TestCase("1/4", "4/7", "1/7")] + [TestCase("2/5", "11/16", "11/40")] + [TestCase("-2/5", "4", "-8/5")] + [TestCase("2/3", "-8/9", "-16/27")] + public void between_fractions(Fraction left, Fraction right, Fraction product) + => (left * right).Should().Be(product); + + [Test] + public void between_fractions_and_longs() + => (1.DividedBy(3) * 4L).Should().Be(4.DividedBy(3)); + + [Test] + public void between_fractions_and_ints() + => (1.DividedBy(3) * 4).Should().Be(4.DividedBy(3)); + + + [Test] + public void between_longs_and_fractions() + => (4L * 1.DividedBy(3)).Should().Be(4.DividedBy(3)); + + [Test] + public void between_ints_and_fractions() + => (4 * 1.DividedBy(3)).Should().Be(4.DividedBy(3)); + } + + public class Division + { + [TestCase("0", "1/3", "0")] + [TestCase("1/3", "1/4", "4/3")] + [TestCase("-1/3", "-1/4", "4/3")] + [TestCase("1/4", "4/7", "7/16")] + [TestCase("2/5", "11/16", "32/55")] + [TestCase("-2/5", "4", "-1/10")] + [TestCase("2/3", "-8/9", "-3/4")] + public void between_Fractions(Fraction left, Fraction right, Fraction division) + => (left / right).Should().Be(division); + + [Test] + public void between_fractions([RandomFraction(3)] Fraction left, [RandomFraction(3, false)] Fraction right) + { + var division = ((decimal)left) / ((decimal)right); + (left / right).Should().Be(division.Fraction()); + } + + [Test] + public void between_fractions_and_longs() + => (1.DividedBy(3) / 4L).Should().Be(1.DividedBy(12)); + + [Test] + public void between_fractions_and_ints() + => (1.DividedBy(3) / 4).Should().Be(1.DividedBy(12)); + + + [Test] + public void between_longs_and_fractions() + => (5L / 2.DividedBy(3)).Should().Be(15.DividedBy(2)); + + [Test] + public void between_ints_and_fractions() + => (5 / 2.DividedBy(3)).Should().Be(15.DividedBy(2)); + } + + public class Addition + { + [TestCase("1/4", "0", "1/4")] + [TestCase("0", "1/4", "1/4")] + [TestCase("5/7", "0", "5/7")] + [TestCase("1/3", "1/4", "7/12")] + [TestCase("1/4", "1/3", "7/12")] + [TestCase("1/4", "1/12", "1/3")] + [TestCase("-1/4", "-1/12", "-1/3")] + [TestCase("-1/4", "1/12", "-1/6")] + [TestCase("1/5", "2/5", "3/5")] + [TestCase("8/3", "1/2", "19/6")] + public void between_fractions(Fraction left, Fraction right, Fraction addition) + => (left + right).Should().Be(addition); + + [Test] + public void between_fractions([RandomFraction(3)] Fraction left, [RandomFraction(3)] Fraction right) + { + var sum = ((decimal)left) + ((decimal)right); + (left + right).Should().Be(sum.Fraction()); + } + + [Test] + public void between_fractions_and_longs() + => (1.DividedBy(3) + 4L).Should().Be(13.DividedBy(3)); + + [Test] + public void between_fractions_and_ints() + => (1.DividedBy(3) + 4).Should().Be(13.DividedBy(3)); + + + [Test] + public void between_longs_and_fractions() + => (4L + 1.DividedBy(3)).Should().Be(13.DividedBy(3)); + + [Test] + public void between_ints_and_fractions() + => (4 + 1.DividedBy(3)).Should().Be(13.DividedBy(3)); + } + + public class Subtraction + { + [TestCase("1/4", "0", "1/4")] + [TestCase("0", "1/4", "-1/4")] + [TestCase("1/3", "1/4", "1/12")] + [TestCase("1/4", "1/3", "-1/12")] + [TestCase("1/4", "1/12", "1/6")] + [TestCase("-1/4", "-1/12", "-1/6")] + [TestCase("-1/4", "1/12", "-1/3")] + public void between_fractions(Fraction left, Fraction right, Fraction subtraction) + => (left - right).Should().Be(subtraction); + + [Test] + public void between_fractions([RandomFraction(3)] Fraction left, [RandomFraction(3)] Fraction right) + { + var subtraction = ((decimal)left) - ((decimal)right); + (left - right).Should().Be(subtraction.Fraction()); + } + + [Test] + public void between_fractions_and_longs() + => (1.DividedBy(3) - 4L).Should().Be(-11.DividedBy(3)); + + [Test] + public void between_fractions_and_ints() + => (1.DividedBy(3) - 4).Should().Be(-11.DividedBy(3)); + + [Test] + public void between_longs_and_fractions() + => (4L - 1.DividedBy(3)).Should().Be(11.DividedBy(3)); + + [Test] + public void between_ints_and_fractions() + => (4 - 1.DividedBy(3)).Should().Be(11.DividedBy(3)); + } + + public class Modulation + { + [TestCase("5/4", "1/1", "1/4")] + [TestCase("-5/4", "1/1", "-1/4")] + [TestCase("5/3", "2/3", "1/3")] + public void between_fractions(Fraction fraction, Fraction divider, Fraction remainder) + => (fraction % divider).Should().Be(remainder); + + [Test] + public void between_fractions([RandomFraction(3)] Fraction fraction, [RandomFraction(3, false)] Fraction divider) + { + var modulo = ((decimal)fraction) % ((decimal)divider); + (fraction % divider).Should().Be(modulo.Fraction()); + } + + [Test] + public void between_fractions_and_longs() + => (1.DividedBy(3) % 4L).Should().Be(1.DividedBy(3)); + + [Test] + public void between_fractions_and_ints() + => (1.DividedBy(3) % 4).Should().Be(1.DividedBy(3)); + + [Test] + public void between_longs_and_fractions() + => (4L % 3.DividedBy(1)).Should().Be(1.DividedBy(1)); + + [Test] + public void between_ints_and_fractions() + => (4 % 3.DividedBy(1)).Should().Be(1.DividedBy(1)); + } + + [TestCase("0", "0")] + [TestCase("17/3", "17/3")] + [TestCase("-7/3", "+7/3")] + public void abs(Fraction fraction, Fraction absolute) + => fraction.Abs().Should().Be(absolute); + + [TestCase("+1/4", "4/1")] + [TestCase("-2/3", "-3/2")] + public void inverse(Fraction faction, Fraction inverse) + => faction.Inverse().Should().Be(inverse); + + [TestCase("0", 0)] + [TestCase("-1/4", -1)] + [TestCase("+1/4", +1)] + public void sign(Fraction fraction, int sign) + => fraction.Sign().Should().Be(sign); + + [TestCase("-69/17", 1)] + [TestCase("+69/17", 1)] + [TestCase("0", 0)] + public void remainder_as_positive_value(Fraction fraction, int remainder) + => fraction.Remainder.Should().Be(remainder); + + [TestCase("-69/17", -4)] + [TestCase("+69/17", +4)] + [TestCase("0", 0)] + public void whole(Fraction fraction, int remainder) + => fraction.Whole.Should().Be(remainder); +} + +public class Has_constant +{ + [Test] + public void Zero_represent_0_divided_by_1() + => Fraction.Zero.Should().BeEquivalentTo(new + { + Numerator = 0L, + Denominator = 1L, + }); + + [Test] + public void Zero_equal_to_default() + => Fraction.Zero.Should().Be(default); + + [Test] + public void One_represent_0_divided_by_1() + => Fraction.One.Should().BeEquivalentTo(new + { + Numerator = 1L, + Denominator = 1L, + }); + + [Test] + public void Epsilon_represent_0_divided_by_1() + => Fraction.Epsilon.Should().BeEquivalentTo(new + { + Numerator = 1L, + Denominator = long.MaxValue, + }); + + [Test] + public void MinValue_represent_0_divided_by_1() + => Fraction.MinValue.Should().BeEquivalentTo(new + { + Numerator = -long.MaxValue, + Denominator = 1L, + }); + + [Test] + public void MaxValue_represent_0_divided_by_1() + => Fraction.MaxValue.Should().BeEquivalentTo(new + { + Numerator = long.MaxValue, + Denominator = 1L, + }); +} + +public class Prevents_overflow +{ + [Test] + public void on_additions() + { + var l = 1.DividedBy(4_000_000_000L); + var r = 1.DividedBy(8_000_000_000L); + (l + r).Should().Be(3.DividedBy(8_000_000_000L)); + } + + [Test] + public void on_multiplications() + { + var l = 1.DividedBy(4_000_000_000L); + var r = 8_000_000_000L.DividedBy(3); + (l * r).Should().Be(2.DividedBy(3)); + } +} + +public class Does_not_define +{ + [Test] + public void inverse_on_zero() + => Fraction.Zero.Invoking(f => f.Inverse()).Should().Throw(); +} + +public class Throws_when { [Test] public void multiplication_can_not_be_represented_by_a_long() @@ -45,8 +354,82 @@ public void subtraction_can_not_be_represented_by_a_long() .Should().Throw() .WithMessage("Arithmetic operation resulted in an overflow.*"); } + + [TestCase(-19223372036854775809.0)] + [TestCase(+19223372036854775809.0)] + public void double_can_not_be_casted_to_fraction(double dbl) + => dbl.Invoking(d => (Fraction)d).Should().Throw() + .WithMessage("Value was either too large or too small for a Fraction."); + + [TestCase(-9223372036854775808.0)] + [TestCase(+9223372036854775808.0)] + public void decimal_can_not_be_casted_to_fraction(decimal dec) + => dec.Invoking(d => (Fraction)d).Should().Throw() + .WithMessage("Value was either too large or too small for a Fraction."); + + [TestCase(-9223372036854775808.0)] + [TestCase(+9223372036854775808.0)] + public void fraction_can_no_be_created_form_double(double dbl) + => dbl.Invoking(Fraction.Create).Should().Throw() + .WithMessage("Value was either too large or too small for a Fraction. *"); + + [TestCase(-9223372036854775808.0)] + [TestCase(+9223372036854775808.0)] + public void fraction_can_no_be_created_form_decimal(decimal dec) + => dec.Invoking(Fraction.Create).Should().Throw() + .WithMessage("Value was either too large or too small for a Fraction. *"); + + [Test] + public void invalid_input_is_parsed() + => "NaN".Invoking(Fraction.Parse) + .Should().Throw() + .WithMessage("Not a valid fraction"); } +public class Is_equal_by_value +{ + [Test] + public void not_equal_to_null() + => Svo.Fraction.Equals(null).Should().BeFalse(); + + [Test] + public void not_equal_to_other_type() + => Svo.Fraction.Equals(new object()).Should().BeFalse(); + + [Test] + public void not_equal_to_different_value() + => Svo.Fraction.Equals(17.DividedBy(42)).Should().BeFalse(); + + [Test] + public void equal_to_same_value() + => Svo.Fraction.Equals(-69.DividedBy(17)).Should().BeTrue(); + + [Test] + public void equal_operator_returns_true_for_same_values() + => (Svo.Fraction == -69.DividedBy(17)).Should().BeTrue(); + + [Test] + public void equal_operator_returns_false_for_different_values() + => (Svo.Fraction == 17.DividedBy(42)).Should().BeFalse(); + + [Test] + public void not_equal_operator_returns_false_for_same_values() + => (Svo.Fraction != -69.DividedBy(17)).Should().BeFalse(); + + [Test] + public void not_equal_operator_returns_true_for_different_values() + => (Svo.Fraction != 17.DividedBy(42)).Should().BeTrue(); + + [TestCase(0, 0)] + [TestCase("17/42", 490960136)] + public void hash_code_is_value_based(Fraction svo, int hash) + { + using (Hash.WithoutRandomizer()) + { + svo.GetHashCode().Should().Be(hash); + } + } +} public class Can_be_parsed { [TestCase(17, 1, "17")] @@ -104,6 +487,16 @@ public void from_fraction_strings(long numerator, long denominator, string str) [TestCase("1∕3")] public void from_multiple_bar_chars(string bar) => Fraction.Parse(bar, CultureInfo.InvariantCulture).Should().Be(1.DividedBy(3)); + + [Test] + public void without_specifying_format_provider() + { + Fraction.TryParse("-69/17", out var fraction).Should().BeTrue(); + fraction.Should().Be(Svo.Fraction); + } + + [Test] + public void using_pure_try_parse() => Fraction.TryParse("-69/17").Should().Be(Svo.Fraction); } public class Can_not_be_parsed @@ -126,10 +519,43 @@ public class Can_not_be_parsed [TestCase("3/0")] [TestCase("²/₀")] public void zero_denominator(string divideByZero) => Fraction.TryParse(divideByZero).Should().BeNull(); + + [TestCase("NaN", "NaN")] + [TestCase("-Infinity", "-Infinity")] + [TestCase("+Infinity", "+Infinity")] + [TestCase("0xFF", "Hexa-decimal")] + [TestCase("15/", "Ends with an operator")] + [TestCase("1//4", "Two division operators")] + [TestCase("1/½", "Vulgar with division operator")] + [TestCase("½1", "Vulgar not at the end")] + [TestCase("²3/₇", "Normal and superscript mixed")] + [TestCase("²/₇3", "Normal and subscript mixed")] + [TestCase("²/3₇", "Normal and subscript mixed")] + [TestCase("₇/3", "Subscript first")] + [TestCase("92233720368547758 17/32464364", "Numerator overflow")] + [TestCase("9223372036854775808", "Long.MaxValue + 1")] + [TestCase("-9223372036854775808", "Long.MinValue")] + [TestCase("-9223372036854775809", "Long.MinValue - 1")] + public void non_fractional_strings(string str, string because) + => Fraction.TryParse(str).Should().BeNull(because); + + [Test] + public void retrieving_null_for_invalid_input() => Fraction.TryParse("invalid").Should().BeNull(); } public class Can_be_created { + [TestCase("0/1", 0, 8, "Should set zero")] + [TestCase("1/4", 2, 8, "Should reduce")] + [TestCase("-1/4", -2, 8, "Should reduce")] + [TestCase("1/4", 3, 12, "Should reduce")] + [TestCase("-1/4", -3, 12, "Should reduce")] + [TestCase("3/7", -3, -7, "Should have no signs")] + [TestCase("-3/7", 3, -7, "Should have no sign on denominator")] + [TestCase("-3/7", -3, 7, "Should have no sign on denominator")] + public void with_constructor(Fraction fraction, long numerator, long denominator, string because) + => new Fraction(numerator, denominator).Should().Be(fraction, because); + [TestCase(0, 1, "0")] [TestCase(00000003, 000000010, "0.3")] [TestCase(00000033, 000000100, "0.33")] @@ -165,6 +591,14 @@ public void from_decimals_without_precision_loss(int runs) } failures.Should().BeEmpty(); } + + [Test] + public void applying_greatest_common_divisor() + => 60.DividedBy(420).Should().BeEquivalentTo(new + { + Numerator = 1L, + Denominator = 7L, + }); } public class Can_not_be_created @@ -186,6 +620,43 @@ public void from_decimal_with_error_out_of_range(decimal error) } } +public class Can_be_casted_explicit +{ + [TestCase(0, 0)] + [TestCase("-69/17", -69 / 17)] + public void to_Int32(Fraction fraction, int casted) => ((int)fraction).Should().Be(casted); + + [TestCase(0, 0)] + [TestCase("-69/17", -69 / 17)] + public void to_Int64(Fraction fraction, long casted) => ((long)fraction).Should().Be(casted); + + [TestCase(0, 0)] + [TestCase("-69/17", -69.0 / 17.0)] + public void to_double(Fraction fraction, double casted) => ((double)fraction).Should().Be(casted); + + [TestCase(0, 0)] + [TestCase(-84, -84)] + public void to_decimal(Fraction fraction, decimal casted) => ((decimal)fraction).Should().Be(casted); + + [Test] + public void to_percent() => ((Percentage)1.DividedBy(4)).Should().Be(25.Percent()); + + [Test] + public void from_Int32() => ((Fraction)42).Should().Be(42.DividedBy(1)); + + [Test] + public void from_Int64() => ((Fraction)42L).Should().Be(42.DividedBy(1)); + + [Test] + public void from_double() => ((Fraction)(-1.0/16.0)).Should().Be(-1.DividedBy(16)); + + [Test] + public void from_decimal() => ((Fraction)(-69m / 17m)).Should().Be(Svo.Fraction); + + [Test] + public void from_percent() => ((Fraction)25.Percent()).Should().Be(1.DividedBy(4)); +} + public class Is_comparable { [Test] @@ -245,6 +716,80 @@ public void by_operators_for_equal_values() } } +public class Has_custom_formatting +{ + [Test] + public void _default() + { + using (TestCultures.en_GB.Scoped()) + { + Svo.Fraction.ToString().Should().Be("-69/17"); + } + } + + [Test] + public void with_null_pattern_equal_to_default() + { + using (TestCultures.en_GB.Scoped()) + { + Svo.Fraction.ToString().Should().Be(Svo.Fraction.ToString(default(string))); + } + } + + [Test] + public void with_string_empty_pattern_equal_to_default() + { + using (TestCultures.en_GB.Scoped()) + { + Svo.Fraction.ToString().Should().Be(Svo.Fraction.ToString(string.Empty)); + } + } + + [Test] + public void default_value_is_represented_as_zero() + => default(Fraction).ToString().Should().Be("0/1"); + + [Test] + public void with_empty_format_provider() + { + using (TestCultures.es_EC.Scoped()) + { + Svo.Fraction.ToString(FormatProvider.Empty).Should().Be("-69/17"); + } + } + + [Test] + public void custom_format_provider_is_applied() + { + var formatted = Svo.Fraction.ToString("[0]super⁄sub", FormatProvider.CustomFormatter); + formatted.Should().Be("Unit Test Formatter, value: '-4¹⁄₁₇', format: '[0]super⁄sub'"); + } + + [TestCase(null, /*............*/ "-2/7", "-2/7")] + [TestCase("", /*..............*/ "-2/7", "-2/7")] + [TestCase("0:0", /*...........*/ "-2:7", "-2/7")] + [TestCase("0÷0", /*...........*/ "4÷3", "4/3")] + [TestCase("[0]0/0", /*........*/ "1 1/3", "4/3")] + [TestCase("[0]0/0", /*........*/ "-1 1/3", "-4/3")] + [TestCase("[0 ]0/0",/*........*/ "-1 1/3", "-4/3")] + [TestCase("#.00", /*..........*/ ".33", "1/3")] + [TestCase("[0]super⁄sub", /*..*/ "5¹¹⁄₁₂", "71/12")] + [TestCase("[0]super⁄0", /*....*/ "5¹¹⁄12", "71/12")] + [TestCase("[0] 0⁄sub", /*.....*/ "5 11⁄₁₂", "71/12")] + [TestCase("[0]super⁄sub", /*..*/ "-3¹⁄₂", "-7/2")] + [TestCase("[0 ]super⁄sub", /*.*/ "-3 ¹⁄₂", "-7/2")] + [TestCase("[#]super⁄sub", /*..*/ "-¹⁄₂", "-1/2")] + [TestCase("[0]super⁄sub", /*..*/ "-0¹⁄₂", "-1/2")] + [TestCase("super⁄sub", /*.....*/ "⁷¹⁄₁₂", "71/12")] + [TestCase("super⁄sub", /*.....*/ "-⁷⁄₂", "-7/2")] + public void for_format(string format, string formatted, Fraction fraction) + => fraction.ToString(format, CultureInfo.InvariantCulture).Should().Be(formatted); + + [Test] + public void that_throws_for_invalid_formats() + => "/invalid".Invoking(Svo.Fraction.ToString).Should().Throw(); +} + public class Has_humanizer_creators { [Test] @@ -349,6 +894,45 @@ public void storing_values_in_SerializationInfo() } #endif +public class Supports_XML_serialization +{ + [Test] + public void using_XmlSerializer_to_serialize() + { + var xml = Serialize.Xml(Svo.Fraction); + xml.Should().Be("-69/17"); + } + + [Test] + public void using_XmlSerializer_to_deserialize() + { + var svo = Deserialize.Xml("-69/17"); + svo.Should().Be(Svo.Fraction); + } + + [Test] + public void using_DataContractSerializer() + { + var round_tripped = SerializeDeserialize.DataContract(Svo.Fraction); + Svo.Fraction.Should().Be(round_tripped); + } + + [Test] + public void as_part_of_a_structure() + { + var structure = XmlStructure.New(Svo.Fraction); + var round_tripped = SerializeDeserialize.Xml(structure); + structure.Should().Be(round_tripped); + } + + [Test] + public void has_no_custom_XML_schema() + { + IXmlSerializable obj = Svo.Fraction; + obj.GetSchema().Should().BeNull(); + } +} + public class Is_Open_API_data_type { [Test] @@ -362,3 +946,11 @@ public void with_info() pattern: "-?[0-9]+(/[0-9]+)?", example: "13/42")); } + +public class Debugger +{ + [TestCase("⁰⁄₁ = 0", "0")] + [TestCase("-⁴²⁄₁₇ = -2.47058824", "-42/17")] + public void has_custom_display(object display, Fraction svo) + => svo.Should().HaveDebuggerDisplay(display); +} diff --git a/specs/Qowaiv.Specs/TestTools/RandomFractionAttribute.cs b/specs/Qowaiv.Specs/TestTools/RandomFractionAttribute.cs new file mode 100644 index 000000000..6ffbdc14f --- /dev/null +++ b/specs/Qowaiv.Specs/TestTools/RandomFractionAttribute.cs @@ -0,0 +1,31 @@ +using NUnit.Framework.Interfaces; +using NUnit.Framework.Internal; + +namespace Qowaiv.Specs.TestTools; +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +public sealed class RandomFractionAttribute(int count, bool includeZero = false) : NUnitAttribute, IParameterDataSource +{ + public int Count { get; } = count; + + public bool IncludeZero { get; } = includeZero; + + /// + [Pure] + public IEnumerable GetData(IParameterInfo parameter) + { + var rnd = Randomizer.GetRandomizer(parameter.ParameterInfo); + + return Enumerable.Range(0, Count).Select(_ => Fraction(rnd)); + + Fraction Fraction(Randomizer rnd) + { + var num = rnd.Next(-255, 255) * rnd.Next(255); + while (num == 0 && !IncludeZero) + { + num = rnd.Next(-255, 255) * rnd.Next(255); + } + var den = rnd.Next(1, 255) * rnd.Next(1, 255); + return num.DividedBy(den); + } + } +} diff --git a/specs/Qowaiv.Specs/_Legacy/Mathematics/FractionOperationsTest.cs b/specs/Qowaiv.Specs/_Legacy/Mathematics/FractionOperationsTest.cs deleted file mode 100644 index 26fbfe08b..000000000 --- a/specs/Qowaiv.Specs/_Legacy/Mathematics/FractionOperationsTest.cs +++ /dev/null @@ -1,210 +0,0 @@ -namespace Qowaiv.UnitTests.Mathematics; - -public class FractionOperationsTest -{ - [TestCase("0", 0)] - [TestCase("-1/4", -1)] - [TestCase("+1/4", +1)] - public void Sign(Fraction fraction, int sign) => fraction.Sign().Should().Be(sign); - - [TestCase("0", "0")] - [TestCase("17/3", "17/3")] - [TestCase("-7/3", "+7/3")] - public void Abs(Fraction fraction, Fraction absolute) => fraction.Abs().Should().Be(absolute); - - [TestCase("0", "0")] - [TestCase("17/3", "17/3")] - [TestCase("-7/3", "-7/3")] - public void Plus(Fraction fraction, Fraction expected) - { - Assert.AreEqual(expected, +fraction); - } - - [TestCase("0", "0")] - [TestCase("-7/3", "+7/3")] - [TestCase("+7/3", "-7/3")] - public void Negate(Fraction fraction, Fraction expected) - { - Assert.AreEqual(expected, -fraction); - } - - [Test] - public void Inverse_Zero_Throws() - { - Assert.Throws(() => Fraction.Zero.Inverse()); - } - - [TestCase("+1/4", "4/1")] - [TestCase("-2/3", "-3/2")] - public void Inverse(Fraction faction, Fraction expected) - { - faction.Inverse().Should().Be(expected); - } - - [Test] - public void Multiply_long() - { - var fraction = 19.DividedBy(72); - var actual = fraction * 48L; - var expected = 38.DividedBy(3); - actual.Should().Be(expected); - } - - [Test] - public void Multiply_int() - { - var fraction = 19.DividedBy(72); - var actual = fraction * 48; - var expected = 38.DividedBy(3); - actual.Should().Be(expected); - } - - [TestCase("1/3", "1/4", "1/12")] - [TestCase("-1/3", "-1/4", "1/12")] - [TestCase("1/4", "4/7", "1/7")] - [TestCase("2/5", "11/16", "11/40")] - [TestCase("-2/5", "4", "-8/5")] - [TestCase("2/3", "-8/9", "-16/27")] - public void Multiply(Fraction left, Fraction right, Fraction expected) - { - Assert.AreEqual(expected, left * right); - } - - [Test] - public void Divide_long() - { - var fraction = 19.DividedBy(72); - var actual = fraction / 48L; - var expected = 19.DividedBy(3456); - actual.Should().Be(expected); - } - - [Test] - public void Divide_int() - { - var fraction = 19.DividedBy(72); - var actual = fraction / 48; - var expected = 19.DividedBy(3456); - actual.Should().Be(expected); - } - - [TestCase("1/3", "1/4", "4/3")] - [TestCase("-1/3", "-1/4", "4/3")] - [TestCase("1/4", "4/7", "7/16")] - [TestCase("2/5", "11/16", "32/55")] - [TestCase("-2/5", "4", "-1/10")] - [TestCase("2/3", "-8/9", "-3/4")] - public void Divide(Fraction left, Fraction right, Fraction expected) - { - Assert.AreEqual(expected, left / right); - } - - [Test] - public void Add_WithPotentialOverflow_ShouldNotOverflow() - { - var l = 1.DividedBy(4_000_000_000L); - var r = 1.DividedBy(8_000_000_000L); - - var actual = l + r; - var expected = 3.DividedBy(8_000_000_000L); - - actual.Should().Be(expected); - } - - [Test] - public void Add_FractionPlusLong() - { - var fraction = 1.DividedBy(3); - var actual = fraction + 2L; - var expected = 7.DividedBy(3); - actual.Should().Be(expected); - } - - [Test] - public void Add_LongPlusFraction() - { - var fraction = 1.DividedBy(3); - var actual = 2L + fraction; - var expected = 7.DividedBy(3); - actual.Should().Be(expected); - } - - [Test] - public void Add_FractionPlusInt() - { - var fraction = 1.DividedBy(3); - var actual = fraction + 2; - var expected = 7.DividedBy(3); - actual.Should().Be(expected); - } - - [Test] - public void Add_IntPlusFraction() - { - var fraction = 1.DividedBy(3); - var actual = 2 + fraction; - var expected = 7.DividedBy(3); - actual.Should().Be(expected); - } - - [TestCase("1/4", "0", "1/4")] - [TestCase("0", "1/4", "1/4")] - [TestCase("1/3", "1/4", "7/12")] - [TestCase("1/4", "1/3", "7/12")] - [TestCase("1/4", "1/12", "1/3")] - [TestCase("-1/4", "-1/12", "-1/3")] - [TestCase("-1/4", "1/12", "-1/6")] - [TestCase("1/5", "2/5", "3/5")] - public void Add(Fraction left, Fraction right, Fraction expected) - { - Assert.AreEqual(expected, left + right); - } - - [Test] - public void Subtract_FractionMinLong() - { - var fraction = 1.DividedBy(3); - var actual = fraction - 2L; - var expected = -5.DividedBy(3); - actual.Should().Be(expected); - } - - [Test] - public void Subtract_LongMinFraction() - { - var fraction = 1.DividedBy(3); - var actual = 2L - fraction; - var expected = 5.DividedBy(3); - actual.Should().Be(expected); - } - - [Test] - public void Subtract_FractionMinInt() - { - var fraction = 1.DividedBy(3); - var actual = fraction - 2; - var expected = -5.DividedBy(3); - actual.Should().Be(expected); - } - - [Test] - public void Subtract_IntMinFraction() - { - var fraction = 1.DividedBy(3); - var actual = 2 - fraction; - var expected = 5.DividedBy(3); - actual.Should().Be(expected); - } - - [TestCase("1/4", "0", "1/4")] - [TestCase("0", "1/4", "-1/4")] - [TestCase("1/3", "1/4", "1/12")] - [TestCase("1/4", "1/3", "-1/12")] - [TestCase("1/4", "1/12", "1/6")] - [TestCase("-1/4", "-1/12", "-1/6")] - [TestCase("-1/4", "1/12", "-1/3")] - public void Subtract(Fraction left, Fraction right, Fraction expected) - { - Assert.AreEqual(expected, left - right); - } -} diff --git a/specs/Qowaiv.Specs/_Legacy/Mathematics/FractionTest.cs b/specs/Qowaiv.Specs/_Legacy/Mathematics/FractionTest.cs deleted file mode 100644 index 9fdd12b77..000000000 --- a/specs/Qowaiv.Specs/_Legacy/Mathematics/FractionTest.cs +++ /dev/null @@ -1,359 +0,0 @@ -namespace Qowaiv.UnitTests.Mathematics; - -/// Tests the fraction SVO. -public class FractionTest -{ - /// The test instance for most tests. - public static readonly Fraction TestStruct = Fraction.Parse("-69/17"); - - /// Fraction.Zero should be equal to the default of fraction. - [Test] - public void Zero_None_EqualsDefault() - { - Fraction.Zero.Should().Be(default); - } - - /// Fraction.IsZero() should be true for the default of fraction. - [Test] - public void IsZero_Default_IsTrue() - { - default(Fraction).IsZero().Should().BeTrue(); - } - - /// Fraction.IsZero() should be false for the TestStruct. - [Test] - public void IsZero_TestStruct_IsFalse() - { - TestStruct.IsZero().Should().BeFalse(); - } - - [Test] - public void Parse_InvalidInput_ThrowsFormatException() - { - using (TestCultures.en_GB.Scoped()) - { - Assert.Catch(() => - { - Fraction.Parse("InvalidInput"); - } - , "Not a valid fraction"); - } - } - - [Test] - public void TryParse_TestStructInput_AreEqual() - { - using (TestCultures.en_GB.Scoped()) - { - var exp = TestStruct; - var act = Fraction.TryParse(exp.ToString()); - act.Should().Be(exp); - } - } - - [Test] - public void from_invalid_as_null_with_TryParse() - => Fraction.TryParse("invalid input").Should().BeNull(); - - [TestCase("0/1", 0, 8, "Should set zero")] - [TestCase("1/4", 2, 8, "Should reduce")] - [TestCase("-1/4", -2, 8, "Should reduce")] - [TestCase("1/4", 3, 12, "Should reduce")] - [TestCase("-1/4", -3, 12, "Should reduce")] - [TestCase("3/7", -3, -7, "Should have no signs")] - [TestCase("-3/7", 3, -7, "Should have no sign on denominator")] - [TestCase("-3/7", -3, 7, "Should have no sign on denominator")] - public void Constructor(Fraction expected, long numerator, long denominator, string description) - { - var actual = new Fraction(numerator, denominator); - Assert.AreEqual(expected, actual, description); - } - -#if NET8_0_OR_GREATER -#else - [Test] - [Obsolete("Usage of the binary formatter is considered harmful.")] - public void SerializeDeserialize_TestStruct_AreEqual() - { - var input = TestStruct; - var exp = TestStruct; - var act = SerializeDeserialize.Binary(input); - act.Should().Be(exp); - } -#endif - - [Test] - public void DataContractSerializeDeserialize_TestStruct_AreEqual() - { - var input = TestStruct; - var exp = TestStruct; - var act = SerializeDeserialize.DataContract(input); - act.Should().Be(exp); - } - - [Test] - public void XmlSerialize_TestStruct_AreEqual() - { - var act = Serialize.Xml(TestStruct); - var exp = "-69/17"; - act.Should().Be(exp); - } - - [Test] - public void XmlDeserialize_XmlString_AreEqual() - { - var act = Deserialize.Xml("-69/17"); - act.Should().Be(TestStruct); - } - -#if NET8_0_OR_GREATER -#else - [Test] - [Obsolete("Usage of the binary formatter is considered harmful.")] - public void SerializeDeserialize_FractionSerializeObject_AreEqual() - { - var input = new FractionSerializeObject { Id = 17, Obj = TestStruct, Date = new DateTime(1970, 02, 14, 00, 00, 000, DateTimeKind.Local), }; - var exp = new FractionSerializeObject { Id = 17, Obj = TestStruct, Date = new DateTime(1970, 02, 14, 00, 00, 000, DateTimeKind.Local), }; - var act = SerializeDeserialize.Binary(input); - Assert.AreEqual(exp.Id, act.Id, "Id"); - Assert.AreEqual(exp.Obj, act.Obj, "Obj"); - Assert.AreEqual(exp.Date, act.Date, "Date"); - } -#endif - - [Test] - public void XmlSerializeDeserialize_FractionSerializeObject_AreEqual() - { - var input = new FractionSerializeObject { Id = 17, Obj = TestStruct, Date = new DateTime(1970, 02, 14, 00, 00, 000, DateTimeKind.Local), }; - var exp = new FractionSerializeObject { Id = 17, Obj = TestStruct, Date = new DateTime(1970, 02, 14, 00, 00, 000, DateTimeKind.Local), }; - var act = SerializeDeserialize.Xml(input); - Assert.AreEqual(exp.Id, act.Id, "Id"); - Assert.AreEqual(exp.Obj, act.Obj, "Obj"); - Assert.AreEqual(exp.Date, act.Date, "Date"); - } - - [Test] - public void DataContractSerializeDeserialize_FractionSerializeObject_AreEqual() - { - var input = new FractionSerializeObject { Id = 17, Obj = TestStruct, Date = new DateTime(1970, 02, 14, 00, 00, 000, DateTimeKind.Local), }; - var exp = new FractionSerializeObject { Id = 17, Obj = TestStruct, Date = new DateTime(1970, 02, 14, 00, 00, 000, DateTimeKind.Local), }; - var act = SerializeDeserialize.DataContract(input); - Assert.AreEqual(exp.Id, act.Id, "Id"); - Assert.AreEqual(exp.Obj, act.Obj, "Obj"); - Assert.AreEqual(exp.Date, act.Date, "Date"); - } - -#if NET8_0_OR_GREATER -#else - [Test] - [Obsolete("Usage of the binary formatter is considered harmful.")] - public void SerializeDeserialize_Default_AreEqual() - { - var input = new FractionSerializeObject { Id = 17, Obj = default, Date = new DateTime(1970, 02, 14, 00, 00, 000, DateTimeKind.Local), }; - var exp = new FractionSerializeObject { Id = 17, Obj = default, Date = new DateTime(1970, 02, 14, 00, 00, 000, DateTimeKind.Local), }; - var act = SerializeDeserialize.Binary(input); - Assert.AreEqual(exp.Id, act.Id, "Id"); - Assert.AreEqual(exp.Obj, act.Obj, "Obj"); - Assert.AreEqual(exp.Date, act.Date, "Date"); - } -#endif - - [Test] - public void XmlSerializeDeserialize_Default_AreEqual() - { - var input = new FractionSerializeObject { Id = 17, Obj = default, Date = new DateTime(1970, 02, 14, 00, 00, 000, DateTimeKind.Local), }; - var exp = new FractionSerializeObject { Id = 17, Obj = default, Date = new DateTime(1970, 02, 14, 00, 00, 000, DateTimeKind.Local), }; - var act = SerializeDeserialize.Xml(input); - Assert.AreEqual(exp.Id, act.Id, "Id"); - Assert.AreEqual(exp.Obj, act.Obj, "Obj"); - Assert.AreEqual(exp.Date, act.Date, "Date"); - } - - [Test] - public void GetSchema_None_IsNull() - { - IXmlSerializable obj = TestStruct; - obj.GetSchema().Should().BeNull(); - } - - [Test] - public void ToString_Zero_StringEmpty() - { - var act = Fraction.Zero.ToString(); - var exp = "0/1"; - act.Should().Be(exp); - } - - [Test] - public void ToString_CustomFormatter_SupportsCustomFormatting() - { - var act = TestStruct.ToString("[0] 0/000", FormatProvider.CustomFormatter); - var exp = "Unit Test Formatter, value: '-4 1/017', format: '[0] 0/000'"; - act.Should().Be(exp); - } - - [TestCase("-2:7", "-2/7", "0:0")] - [TestCase("4÷3", "4/3", "0÷0")] - [TestCase("1 1/3", "4/3", "[0]0/0")] - [TestCase("-1 1/3", "-4/3", "[0]0/0")] - [TestCase("-1 1/3", "-4/3", "[0 ]0/0")] - [TestCase(".33", "1/3", "#.00")] - [TestCase("5¹¹⁄₁₂", "71/12", "[0]super⁄sub")] - [TestCase("5¹¹⁄12", "71/12", "[0]super⁄0")] - [TestCase("5 11⁄₁₂", "71/12", "[0] 0⁄sub")] - [TestCase("-3¹⁄₂", "-7/2", "[0]super⁄sub")] - [TestCase("-3 ¹⁄₂", "-7/2", "[0 ]super⁄sub")] - [TestCase("-¹⁄₂", "-1/2", "[#]super⁄sub")] - [TestCase("-0¹⁄₂", "-1/2", "[0]super⁄sub")] - [TestCase("⁷¹⁄₁₂", "71/12", "super⁄sub")] - [TestCase("-⁷⁄₂", "-7/2", "super⁄sub")] - public void ToString_WithFormat(string expected, Fraction fraction, string format) - { - var formatted = fraction.ToString(format, CultureInfo.InvariantCulture); - formatted.Should().Be(expected); - } - - /// GetHash should not fail for Fraction.Zero. - [Test] - public void GetHash_Zero_Hash() - { - Fraction.Zero.GetHashCode().Should().Be(0); - } - - /// GetHash should not fail for the test struct. - [Test] - public void GetHash_TestStruct_Hash() - { - TestStruct.GetHashCode().Should().Be(132548); - } - - [Test] - public void Equals_ZeroZero_IsTrue() - { - Fraction.Zero.Equals(Fraction.Zero).Should().BeTrue(); - } - - [Test] - public void Equals_FormattedAndUnformatted_IsTrue() - { - var l = Fraction.Parse("-71,234/71,234", CultureInfo.InvariantCulture); - var r = Fraction.Parse("-1", CultureInfo.InvariantCulture); - l.Equals(r).Should().BeTrue(); - } - - [Test] - public void Equals_TestStructTestStruct_IsTrue() - { - TestStruct.Equals(TestStruct).Should().BeTrue(); - } - - [Test] - public void Equals_TestStructZero_IsFalse() - { - TestStruct.Equals(Fraction.Zero).Should().BeFalse(); - } - - [Test] - public void Equals_ZeroTestStruct_IsFalse() - { - Fraction.Zero.Equals(TestStruct).Should().BeFalse(); - } - - [Test] - public void Equals_TestStructObjectTestStruct_IsTrue() - { - TestStruct.Equals((object)TestStruct).Should().BeTrue(); - } - - [Test] - public void Equals_TestStructNull_IsFalse() - { - TestStruct.Equals(null).Should().BeFalse(); - } - - [Test] - public void Equals_TestStructObject_IsFalse() - { - TestStruct.Equals(new object()).Should().BeFalse(); - } - - [Test] - public void OperatorIs_TestStructTestStruct_IsTrue() - { - var l = TestStruct; - var r = TestStruct; - (l == r).Should().BeTrue(); - } - - [Test] - public void OperatorIsNot_TestStructTestStruct_IsFalse() - { - var l = TestStruct; - var r = TestStruct; - (l != r).Should().BeFalse(); - } - - [Test] - public void Explicit_Int32ToFraction_AreEqual() - { - var exp = 123456789.DividedBy(1); - var act = (Fraction)123456789; - act.Should().Be(exp); - } - - [Test] - public void Explicit_FractionToInt32_AreEqual() - { - Assert.AreEqual(-69 / 17, (int)TestStruct); - } - - [Test] - public void Explicit_FractionToInt64_AreEqual() - { - Assert.AreEqual(-69 / 17L, (long)TestStruct); - } - - [Test] - public void Explicit_FractionToDouble_AreEqual() - { - Assert.AreEqual(-69 / 17d, (double)TestStruct); - } - - [Test] - public void Explicit_FractionToDecimal_AreEqual() - { - Assert.AreEqual(-69 / 17m, (decimal)TestStruct); - } - - [Test] - public void ConverterExists_Fraction_IsTrue() - => typeof(Fraction).Should().HaveTypeConverterDefined(); - - [TestCase(null, "Null")] - [TestCase("", "String.Empty")] - [TestCase("NaN", "NaN")] - [TestCase("-Infinity", "-Infinity")] - [TestCase("+Infinity", "+Infinity")] - [TestCase("0xFF", "Hexa-decimal")] - [TestCase("15/", "Ends with an operator")] - [TestCase("1//4", "Two division operators")] - [TestCase("1/½", "Vulgar with division operator")] - [TestCase("½1", "Vulgar not at the end")] - [TestCase("²3/₇", "Normal and superscript mixed")] - [TestCase("²/₇3", "Normal and subscript mixed")] - [TestCase("²/3₇", "Normal and subscript mixed")] - [TestCase("₇/3", "Subscript first")] - [TestCase("9223372036854775808", "Long.MaxValue + 1")] - [TestCase("-9223372036854775808", "Long.MinValue")] - [TestCase("-9223372036854775809", "Long.MinValue - 1")] - public void IsInvalid_String(string str, string because) - => Fraction.TryParse(str).Should().BeNull(because); -} - -[Serializable] -public class FractionSerializeObject -{ - public int Id { get; set; } - public Fraction Obj { get; set; } - public DateTime Date { get; set; } -} diff --git a/src/Qowaiv/Conversion/Mathematics/FractionTypeConverter.cs b/src/Qowaiv/Conversion/Mathematics/FractionTypeConverter.cs index e22390b1a..9fa23ef54 100644 --- a/src/Qowaiv/Conversion/Mathematics/FractionTypeConverter.cs +++ b/src/Qowaiv/Conversion/Mathematics/FractionTypeConverter.cs @@ -6,6 +6,30 @@ namespace Qowaiv.Conversion.Mathematics; [Inheritable] public class FractionTypeConverter : SvoTypeConverter { + /// + [Pure] + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + => IsNumber(sourceType) + || base.CanConvertFrom(context, sourceType); + + [Pure] + private static bool IsNumber(Type sourceType) + => sourceType == typeof(decimal) + || sourceType == typeof(int) + || sourceType == typeof(long) + || sourceType == typeof(double); + + /// + [Pure] + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object? value) => value switch + { + int int32 => Fraction.Create(int32), + long int64 => Fraction.Create(int64), + decimal dec => Fraction.Create(dec), + double dbl => Fraction.Create(dbl), + _ => base.ConvertFrom(context, culture, value), + }; + /// [Pure] protected override Fraction FromString(string? str, CultureInfo? culture) => Fraction.Parse(str, culture); diff --git a/src/Qowaiv/Hashing/Hash.cs b/src/Qowaiv/Hashing/Hash.cs index dd304569a..141b80e6e 100644 --- a/src/Qowaiv/Hashing/Hash.cs +++ b/src/Qowaiv/Hashing/Hash.cs @@ -76,15 +76,14 @@ public static int NotSupportedBy() /// Gets a hash, based on its type. [Pure] - private static int HashCode(T obj) - => obj switch - { - null => 0, - int int32 => int32, - string str => HashCode(str), - IEnumerable enumerable => HashCodes(enumerable), - _ => obj.GetHashCode(), - }; + private static int HashCode(T obj) => obj switch + { + null => 0, + int int32 => int32, + string str => HashCode(str), + IEnumerable enumerable => HashCodes(enumerable), + _ => obj.GetHashCode(), + }; /// Gets a deterministic hash of a string. [Pure] diff --git a/src/Qowaiv/Mathematics/Fraction.INumber.cs b/src/Qowaiv/Mathematics/Fraction.INumber.cs new file mode 100644 index 000000000..b72c91747 --- /dev/null +++ b/src/Qowaiv/Mathematics/Fraction.INumber.cs @@ -0,0 +1,361 @@ +namespace Qowaiv.Mathematics; + +public readonly partial struct Fraction +{ + /// Pluses the fraction. + public static Fraction operator +(Fraction fraction) => fraction; + + /// Negates the fraction. + public static Fraction operator -(Fraction fraction) => New(-fraction.numerator, fraction.denominator); + + /// Increases the fraction with one. + public static Fraction operator ++(Fraction value) => value + One; + + /// Decreases the fraction with one. + public static Fraction operator --(Fraction value) => value - One; + + /// Multiplies the left and the right fractions. + public static Fraction operator *(Fraction left, Fraction right) => left.Multiply(right); + + /// Multiplies the left and the right fractions. + public static Fraction operator *(Fraction left, long right) => left * Create(right); + + /// Multiplies the left and the right fractions. + public static Fraction operator *(Fraction left, int right) => left * Create(right); + + /// Multiplies the left and the right fraction. + public static Fraction operator *(long left, Fraction right) => Create(left) * right; + + /// Multiplies the left and the right fraction. + public static Fraction operator *(int left, Fraction right) => Create(left) * right; + + /// Divide the fraction by a specified factor. + public static Fraction operator /(Fraction fraction, Fraction factor) => fraction * factor.Inverse(); + + /// Divide the fraction by a specified factor. + public static Fraction operator /(Fraction fraction, long factor) => fraction * New(1, factor); + + /// Divide the fraction by a specified factor. + public static Fraction operator /(Fraction fraction, int factor) => fraction * New(1, factor); + + /// Divide the fraction by a specified factor. + public static Fraction operator /(long number, Fraction factor) => Create(number) * factor.Inverse(); + + /// Divide the fraction by a specified factor. + public static Fraction operator /(int number, Fraction factor) => Create(number) * factor.Inverse(); + + /// Adds the left and the right fraction. + public static Fraction operator +(Fraction left, Fraction right) => left.Add(right); + + /// Adds the left and the right fraction. + public static Fraction operator +(Fraction left, long right) => left + Create(right); + + /// Adds the left and the right fraction. + public static Fraction operator +(Fraction left, int right) => left + Create(right); + + /// Adds the left and the right fraction. + public static Fraction operator +(long left, Fraction right) => Create(left) + right; + + /// Adds the left and the right fraction. + public static Fraction operator +(int left, Fraction right) => Create(left) + right; + + /// Subtracts the left from the right fraction. + public static Fraction operator -(Fraction left, Fraction right) => left + New(-right.numerator, right.denominator); + + /// Subtracts the left from the right fraction. + public static Fraction operator -(Fraction left, long right) => left - Create(right); + + /// Subtracts the left from the right fraction. + public static Fraction operator -(Fraction left, int right) => left - Create(right); + + /// Subtracts the left from the right fraction. + public static Fraction operator -(long left, Fraction right) => Create(left) - right; + + /// Subtracts the left from the right fraction. + public static Fraction operator -(int left, Fraction right) => Create(left) - right; + + /// Gets the remainder of the fraction. + public static Fraction operator %(Fraction fraction, Fraction divider) => fraction.Modulo(divider); + + /// Gets the remainder of the fraction. + public static Fraction operator %(Fraction fraction, int divider) => fraction % Create(divider); + + /// Gets the remainder of the fraction. + public static Fraction operator %(Fraction fraction, long divider) => fraction % Create(divider); + + /// Gets the remainder of the fraction. + public static Fraction operator %(int number, Fraction divider) => Create(number) % divider; + + /// Gets the remainder of the fraction. + public static Fraction operator %(long number, Fraction divider) => Create(number) % divider; + + /// Multiplies the fraction with the factor. + [Pure] + private Fraction Multiply(Fraction factor) + { + if (IsZero() || factor.IsZero()) return Zero; + else + { + var sign = Sign() * factor.Sign(); + long n0 = numerator.Abs(); + long d0 = denominator; + long n1 = factor.numerator.Abs(); + long d1 = factor.denominator; + + Reduce(ref n0, ref d1); + Reduce(ref n1, ref d0); + + return checked(New(sign * n0 * n1, d0 * d1)); + } + } + + /// Adds a fraction to the current fraction. + [Pure] + private Fraction Add(Fraction other) + { + if (IsZero()) return other; + else if (other.IsZero()) return this; + else + { + long d0 = denominator; + long d1 = other.denominator; + + Reduce(ref d0, ref d1); + + checked + { + // The new denominator is reduced product of d0 en d1. + var d2 = denominator * d1; + var n2 = (numerator * d1) + (other.numerator * d0); + + return new(new Data(n2, d2).Simplify()); + } + } + } + + /// Multiplies the fraction with the factor. + [Pure] + private Fraction Modulo(Fraction divider) + { + if (divider.IsZero()) throw new DivideByZeroException(); + + if (IsZero()) return Zero; + else + { + long d0 = denominator; + long d1 = divider.denominator; + + Reduce(ref d0, ref d1); + + checked + { + // The new denominator is reduced product of d0 en d1. + var d2 = denominator * d1; + var n2 = (numerator * d1) % (divider.numerator * d0); + + return new(new Data(n2, d2).Simplify()); + } + } + } +} + +#if NET8_0_OR_GREATER + +public readonly partial struct Fraction : INumber + , IAdditionOperators, ISubtractionOperators + , IAdditionOperators, ISubtractionOperators + , IMultiplyOperators, IDivisionOperators + , IMultiplyOperators, IDivisionOperators + , IModulusOperators, IModulusOperators + , IMinMaxValue +{ + /// + static int INumberBase.Radix => 10; + + /// + static Fraction IAdditiveIdentity.AdditiveIdentity => One; + + /// + static Fraction IMultiplicativeIdentity.MultiplicativeIdentity => One; + + /// + [Pure] + static Fraction INumberBase.Abs(Fraction value) => value.Abs(); + + /// + [Pure] + static bool INumberBase.IsFinite(Fraction value) => true; + + /// + [Pure] + static bool INumberBase.IsImaginaryNumber(Fraction value) => false; + + /// + [Pure] + static bool INumberBase.IsNaN(Fraction value) => false; + + /// + [Pure] + static bool INumberBase.IsInfinity(Fraction value) => false; + + /// + [Pure] + static bool INumberBase.IsPositiveInfinity(Fraction value) => false; + + /// + [Pure] + static bool INumberBase.IsNegativeInfinity(Fraction value) => false; + + /// + [Pure] + static bool INumberBase.IsRealNumber(Fraction value) => true; + + /// + [Pure] + static bool INumberBase.IsComplexNumber(Fraction value) => false; + + /// + [Pure] + static bool INumberBase.IsNormal(Fraction value) => true; + + /// + [Pure] + static bool INumberBase.IsSubnormal(Fraction value) => false; + + /// + [Pure] + static bool INumberBase.IsCanonical(Fraction value) => true; + + /// + [Pure] + static bool INumberBase.IsInteger(Fraction value) + => value.Denominator == 1; + + /// + [Pure] + static bool INumberBase.IsEvenInteger(Fraction value) + => value.Denominator == 1 + && long.IsEvenInteger(value.Numerator); + + /// + [Pure] + static bool INumberBase.IsNegative(Fraction value) => value.Numerator < 0; + + /// + [Pure] + static bool INumberBase.IsOddInteger(Fraction value) + => value.Denominator == 1 + && long.IsOddInteger(value.Numerator); + + /// + [Pure] + static bool INumberBase.IsPositive(Fraction value) => value.Numerator >= 0; + + /// + [Pure] + static bool INumberBase.IsZero(Fraction value) => value.Numerator == 0; + + /// + [Pure] + static Fraction INumberBase.MaxMagnitude(Fraction x, Fraction y) => MaxMagnitude(x, y); + + /// + [Pure] + static Fraction INumberBase.MaxMagnitudeNumber(Fraction x, Fraction y) => MaxMagnitude(x, y); + + [Pure] + private static Fraction MaxMagnitude(Fraction x, Fraction y) + { + var ax = x.Abs(); + var ay = y.Abs(); + + if (ax > ay) return x; + else if (ax == ay) return x < Zero ? y : x; + else return y; + } + + /// + [Pure] + static Fraction INumberBase.MinMagnitude(Fraction x, Fraction y) => MinMagnitude(x, y); + + /// + [Pure] + static Fraction INumberBase.MinMagnitudeNumber(Fraction x, Fraction y) => MinMagnitude(x, y); + + [Pure] + private static Fraction MinMagnitude(Fraction x, Fraction y) + { + var ax = x.Abs(); + var ay = y.Abs(); + + if (ax < ay) return x; + else if (ax == ay) return x < Zero ? x : y; + else return y; + } + + /// + bool ISpanFormattable.TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) + { + charsWritten = 0; + return false; + } + + /// + [Pure] + [ExcludeFromCodeCoverage(Justification = "Only explicitly exposed overload of thoroughly tested parsing method.")] + static Fraction INumberBase.Parse(ReadOnlySpan s, NumberStyles style, IFormatProvider? provider) + => Parse(s.ToString(), provider); + + /// + [Pure] + [ExcludeFromCodeCoverage(Justification = "Only explicitly exposed overload of thoroughly tested parsing method.")] + static Fraction INumberBase.Parse(string s, NumberStyles style, IFormatProvider? provider) + => Parse(s, provider); + + /// + [Pure] + [ExcludeFromCodeCoverage(Justification = "Only explicitly exposed overload of thoroughly tested parsing method.")] + static Fraction ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) + => Parse(s.ToString(), provider); + + /// + [ExcludeFromCodeCoverage(Justification = "Only explicitly exposed overload of thoroughly tested parsing method.")] + static bool INumberBase.TryParse(ReadOnlySpan s, NumberStyles style, IFormatProvider? provider, out Fraction result) + => TryParse(s.ToString(), provider, out result); + + /// + [ExcludeFromCodeCoverage(Justification = "Only explicitly exposed overload of thoroughly tested parsing method.")] + static bool INumberBase.TryParse(string? s, NumberStyles style, IFormatProvider? provider, out Fraction result) + => TryParse(s, provider, out result); + + /// + [ExcludeFromCodeCoverage(Justification = "Only explicitly exposed overload of thoroughly tested parsing method.")] + static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, out Fraction result) + => TryParse(s.ToString(), provider, out result); + + [ExcludeFromCodeCoverage(Justification = "Protected member of the contract that is not supported.")] + static bool INumberBase.TryConvertFromChecked(TOther value, out Fraction result) + => throw new NotSupportedException(); + + [ExcludeFromCodeCoverage(Justification = "Protected member of the contract that is not supported.")] + static bool INumberBase.TryConvertFromSaturating(TOther value, out Fraction result) + => throw new NotSupportedException(); + + [ExcludeFromCodeCoverage(Justification = "Protected member of the contract that is not supported.")] + static bool INumberBase.TryConvertFromTruncating(TOther value, out Fraction result) + => throw new NotSupportedException(); + + [ExcludeFromCodeCoverage(Justification = "Protected member of the contract that is not supported.")] + static bool INumberBase.TryConvertToChecked(Fraction value, out TOther result) + => throw new NotSupportedException(); + + [ExcludeFromCodeCoverage(Justification = "Protected member of the contract that is not supported.")] + static bool INumberBase.TryConvertToSaturating(Fraction value, out TOther result) + => throw new NotSupportedException(); + + [ExcludeFromCodeCoverage(Justification = "Protected member of the contract that is not supported.")] + static bool INumberBase.TryConvertToTruncating(Fraction value, out TOther result) + => throw new NotSupportedException(); +} + +#endif diff --git a/src/Qowaiv/Mathematics/Fraction.cs b/src/Qowaiv/Mathematics/Fraction.cs index bb5a228cf..caab435c2 100644 --- a/src/Qowaiv/Mathematics/Fraction.cs +++ b/src/Qowaiv/Mathematics/Fraction.cs @@ -14,15 +14,6 @@ namespace Qowaiv.Mathematics; #endif [StructLayout(LayoutKind.Sequential)] public readonly partial struct Fraction : IXmlSerializable, IFormattable, IEquatable, IComparable, IComparable -#if NET8_0_OR_GREATER - , IAdditionOperators, ISubtractionOperators - , IUnaryPlusOperators, IUnaryNegationOperators - , IAdditionOperators, ISubtractionOperators - , IAdditionOperators, ISubtractionOperators - , IMultiplyOperators, IDivisionOperators - , IMultiplyOperators, IDivisionOperators - , IMinMaxValue -#endif #if NET8_0_OR_GREATER #else , ISerializable @@ -160,14 +151,6 @@ private Fraction(Data data) [Pure] public Fraction Abs() => New(numerator.Abs(), denominator); - /// Pluses the fraction. - [Pure] - internal Fraction Plus() => New(+numerator, denominator); - - /// Negates the fraction. - [Pure] - internal Fraction Negate() => New(-numerator, denominator); - /// Gets the inverse of a faction. /// /// When the fraction is . @@ -178,230 +161,6 @@ public Fraction Inverse() ? throw new DivideByZeroException() : New(Sign() * denominator, numerator.Abs()); - /// Multiplies the fraction with the factor. - /// - /// The factor to multiply with. - /// - [Pure] - public Fraction Multiply(Fraction factor) - { - if (factor.IsZero()) return Zero; - else - { - var sign = Sign() * factor.Sign(); - long n0 = numerator.Abs(); - long d0 = denominator; - long n1 = factor.numerator.Abs(); - long d1 = factor.denominator; - - Reduce(ref n0, ref d1); - Reduce(ref n1, ref d0); - - return checked(New(sign * n0 * n1, d0 * d1)); - } - } - - /// Multiplies the fraction with the factor. - /// - /// The factor to multiply with. - /// - [Pure] - public Fraction Multiply(long factor) => Multiply(Create(factor)); - - /// Multiplies the fraction with the factor. - /// - /// The factor to multiply with. - /// - [Pure] - public Fraction Multiply(int factor) => Multiply((long)factor); - - /// Divide the fraction by a specified factor. - /// - /// The factor to multiply with. - /// - [Pure] - public Fraction Divide(Fraction factor) => Multiply(factor.Inverse()); - - /// Divide the fraction by a specified factor. - /// - /// The factor to multiply with. - /// - [Pure] - public Fraction Divide(long factor) - => factor == 0 - ? throw new DivideByZeroException() - : Multiply(New(1, factor)); - - /// Divide the fraction by a specified factor. - /// - /// The factor to multiply with. - /// - [Pure] - public Fraction Divide(int factor) => Divide((long)factor); - - /// Adds a fraction to the current fraction. - /// - /// The fraction to add. - /// - [Pure] - public Fraction Add(Fraction fraction) - { - if (IsZero()) return fraction; - else if (fraction.IsZero()) return this; - else - { - long n0 = numerator.Abs(); - long d0 = denominator; - long n1 = fraction.numerator.Abs(); - long d1 = fraction.denominator; - - Reduce(ref n0, ref d1); - Reduce(ref n1, ref d0); - - checked - { - long n; - long d; - - // Same denominator. - if (d0 == d1) - { - d = d0; - } - - // d0 is a multiple of d1 - else if (d0 > d1 && d0 % d1 == 0) - { - d = d0; - n1 *= d0 / d1; - } - - // d1 is a multiple of d0 - else if (d1 % d0 == 0) - { - d = d1; - n0 *= d1 / d0; - } - else - { - d = d0 * d1; - n0 *= d1; - n1 *= d0; - } - - n = (n0 * Sign()) + (n1 * fraction.Sign()); - - var sign = n.Sign(); - n = n.Abs(); - - Reduce(ref n, ref d); - - return New(n * sign, d); - } - } - } - - /// Adds a number to the current fraction. - /// - /// The number to add. - /// - [Pure] - public Fraction Add(long number) => Add(Create(number)); - - /// Adds a number to the current fraction. - /// - /// The number to add. - /// - [Pure] - public Fraction Add(int number) => Add((long)number); - - /// Subtracts a fraction from the current fraction. - /// - /// The fraction to subtract. - /// - [Pure] - public Fraction Subtract(Fraction fraction) => Add(fraction.Negate()); - - /// Subtracts a number from the current fraction. - /// - /// The number to subtract. - /// - [Pure] - public Fraction Subtract(long number) => Subtract(Create(number)); - - /// Subtracts a number from the current fraction. - /// - /// The number to subtract. - /// - [Pure] - public Fraction Subtract(int number) => Subtract((long)number); - - /// Pluses the fraction. - public static Fraction operator +(Fraction fraction) => fraction.Plus(); - - /// Negates the fraction. - public static Fraction operator -(Fraction fraction) => fraction.Negate(); - - /// Multiplies the left and the right fractions. - public static Fraction operator *(Fraction left, Fraction right) => left.Multiply(right); - - /// Multiplies the left and the right fractions. - public static Fraction operator *(Fraction left, long right) => left.Multiply(right); - - /// Multiplies the left and the right fractions. - public static Fraction operator *(Fraction left, int right) => left.Multiply(right); - - /// Multiplies the left and the right fraction. - public static Fraction operator *(long left, Fraction right) => right.Multiply(left); - - /// Multiplies the left and the right fraction. - public static Fraction operator *(int left, Fraction right) => right.Multiply(left); - - /// Divide the fraction by a specified factor. - public static Fraction operator /(Fraction fraction, Fraction factor) => fraction.Divide(factor); - - /// Divide the fraction by a specified factor. - public static Fraction operator /(Fraction fraction, long factor) => fraction.Divide(factor); - - /// Divide the fraction by a specified factor. - public static Fraction operator /(Fraction fraction, int factor) => fraction.Divide(factor); - - /// Divide the fraction by a specified factor. - public static Fraction operator /(long number, Fraction factor) => Create(number).Divide(factor); - - /// Divide the fraction by a specified factor. - public static Fraction operator /(int number, Fraction factor) => Create(number).Divide(factor); - - /// Adds the left and the right fraction. - public static Fraction operator +(Fraction left, Fraction right) => left.Add(right); - - /// Adds the left and the right fraction. - public static Fraction operator +(Fraction left, long right) => left.Add(right); - - /// Adds the left and the right fraction. - public static Fraction operator +(Fraction left, int right) => left.Add(right); - - /// Adds the left and the right fraction. - public static Fraction operator +(long left, Fraction right) => right.Add(left); - - /// Adds the left and the right fraction. - public static Fraction operator +(int left, Fraction right) => right.Add(left); - - /// Subtracts the left from the right fraction. - public static Fraction operator -(Fraction left, Fraction right) => left.Subtract(right); - - /// Subtracts the left from the right fraction. - public static Fraction operator -(Fraction left, long right) => left.Subtract(right); - - /// Subtracts the left from the right fraction. - public static Fraction operator -(Fraction left, int right) => left.Subtract(right); - - /// Subtracts the left from the right fraction. - public static Fraction operator -(long left, Fraction right) => Create(left).Subtract(right); - - /// Subtracts the left from the right fraction. - public static Fraction operator -(int left, Fraction right) => Create(left).Subtract(right); - /// Returns a formatted that represents the fraction. /// /// The format that this describes the formatting. @@ -422,7 +181,7 @@ public string ToString(string? format, IFormatProvider? formatProvider) } // if no fraction bar character has been provided, format as a decimal. - else if (!format.WithDefault().Any(ch => Formatting.IsFractionBar(ch))) + else if (!format.WithDefault().Any(Formatting.IsFractionBar)) { return ToDecimal().ToString(format, formatProvider); } @@ -509,7 +268,7 @@ public bool Equals(Fraction other) /// [Pure] - public override int GetHashCode() => unchecked(denominator * 113 * numerator).GetHashCode(); + public override int GetHashCode() => Hash.Code(denominator).And(numerator); /// [Pure] @@ -714,8 +473,7 @@ private static void Reduce(ref long a, ref long b) [Pure] private static long Gcd(long a, long b) { - var even = 1; - long remainder; + long even = 1; // while both are even. while ((a & 1) == 0 && (b & 1) == 0) @@ -726,9 +484,7 @@ private static long Gcd(long a, long b) } while (b != 0) { - remainder = a % b; - a = b; - b = remainder; + (a, b) = (b, a % b); } return a * even; }