From dc1b9d6c0470c7059de20b2de96e79aaf0067f77 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Wed, 5 Apr 2023 11:05:53 +0100 Subject: [PATCH] feat: add math extensions for BigInteger --- CHANGELOG.md | 1 + X10D.Tests/src/Math/BigIntegerTests.Wrap.cs | 105 ++++++++ X10D.Tests/src/Math/BigIntegerTests.cs | 159 ++++++++++++ X10D.Tests/src/Math/IsPrimeTests.cs | 8 +- X10D/src/Math/BigIntegerExtensions.cs | 253 ++++++++++++++++++++ 5 files changed, 525 insertions(+), 1 deletion(-) create mode 100644 X10D.Tests/src/Math/BigIntegerTests.Wrap.cs create mode 100644 X10D.Tests/src/Math/BigIntegerTests.cs create mode 100644 X10D/src/Math/BigIntegerExtensions.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index e5d5ba7..c978814 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - X10D: Added extension methods for `DateOnly`, for parity with `DateTime` and `DateTimeOffset`. +- X10D: Added math-related extension methods for `BigInteger`. ### Changed - X10D: `DateTime.Age(DateTime)` and `DateTimeOffset.Age(DateTimeOffset)` parameter renamed from `asOf` to `referenceDate`. diff --git a/X10D.Tests/src/Math/BigIntegerTests.Wrap.cs b/X10D.Tests/src/Math/BigIntegerTests.Wrap.cs new file mode 100644 index 0000000..63a661a --- /dev/null +++ b/X10D.Tests/src/Math/BigIntegerTests.Wrap.cs @@ -0,0 +1,105 @@ +using System.Numerics; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Math; + +namespace X10D.Tests.Math; + +public partial class BigIntegerTests +{ + [TestClass] + public class WrapTests + { + [TestMethod] + public void Wrap_ShouldReturnLow_WhenValueIsEqualToLow() + { + BigInteger value = 10; + BigInteger low = 10; + BigInteger high = 20; + + BigInteger result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnHigh_WhenValueIsEqualToHigh() + { + BigInteger value = 20; + BigInteger low = 10; + BigInteger high = 20; + + BigInteger result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsGreaterThanHigh() + { + BigInteger value = 30; + BigInteger low = 10; + BigInteger high = 20; + + BigInteger result = value.Wrap(low, high); + + Assert.AreEqual(low, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsLessThanLow() + { + BigInteger value = 5; + BigInteger low = 10; + BigInteger high = 20; + + BigInteger result = value.Wrap(low, high); + + Assert.AreEqual(15L, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsInBetweenLowAndHigh() + { + BigInteger value = 15; + BigInteger low = 10; + BigInteger high = 20; + + BigInteger result = value.Wrap(low, high); + + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Wrap_ShouldReturnZero_WhenValueIsEqualToLength() + { + BigInteger value = 10; + BigInteger length = 10; + + BigInteger result = value.Wrap(length); + + Assert.AreEqual(0L, result); + } + + [TestMethod] + public void Wrap_ShouldReturnValue_WhenValueIsLessThanLength() + { + BigInteger value = 5; + BigInteger length = 10; + + BigInteger result = value.Wrap(length); + + Assert.AreEqual(value, result); + } + + [TestMethod] + public void Wrap_ShouldReturnCorrectResult_WhenValueIsGreaterThanLength() + { + BigInteger value = 15; + BigInteger length = 10; + + BigInteger result = value.Wrap(length); + + Assert.AreEqual(5L, result); + } + } +} diff --git a/X10D.Tests/src/Math/BigIntegerTests.cs b/X10D.Tests/src/Math/BigIntegerTests.cs new file mode 100644 index 0000000..304086c --- /dev/null +++ b/X10D.Tests/src/Math/BigIntegerTests.cs @@ -0,0 +1,159 @@ +using System.Numerics; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Math; + +namespace X10D.Tests.Math; + +[TestClass] +public partial class BigIntegerTests +{ + [TestMethod] + public void DigitalRootShouldBeCorrect() + { + BigInteger value = 238; + Assert.AreEqual(4, value.DigitalRoot()); + Assert.AreEqual(4, (-value).DigitalRoot()); + } + + [TestMethod] + public void FactorialShouldBeCorrect() + { + Assert.AreEqual(1, ((BigInteger)0).Factorial()); + Assert.AreEqual(1, ((BigInteger)1).Factorial()); + Assert.AreEqual(2, ((BigInteger)2).Factorial()); + Assert.AreEqual(6, ((BigInteger)3).Factorial()); + Assert.AreEqual(24, ((BigInteger)4).Factorial()); + Assert.AreEqual(120, ((BigInteger)5).Factorial()); + Assert.AreEqual(720, ((BigInteger)6).Factorial()); + Assert.AreEqual(5040, ((BigInteger)7).Factorial()); + Assert.AreEqual(40320, ((BigInteger)8).Factorial()); + Assert.AreEqual(362880, ((BigInteger)9).Factorial()); + Assert.AreEqual(3628800, ((BigInteger)10).Factorial()); + } + + [TestMethod] + public void GreatestCommonFactor_ShouldBe1_ForPrimeNumbers() + { + BigInteger first = 5L; + BigInteger second = 7L; + + BigInteger multiple = first.GreatestCommonFactor(second); + + Assert.AreEqual(1L, multiple); + } + + [TestMethod] + public void GreatestCommonFactor_ShouldBe6_Given12And18() + { + BigInteger first = 12L; + BigInteger second = 18L; + + BigInteger multiple = first.GreatestCommonFactor(second); + + Assert.AreEqual(6L, multiple); + } + + [TestMethod] + public void IsOddShouldBeCorrect() + { + BigInteger one = 1; + BigInteger two = 2; + + Assert.IsTrue(one.IsOdd()); + Assert.IsFalse(two.IsOdd()); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnCorrectValue_WhenCalledWithValidInput() + { + BigInteger value1 = 2; + BigInteger value2 = 3; + BigInteger expected = 6; + + BigInteger result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnZero_WhenCalledWithZero() + { + BigInteger value1 = 0; + BigInteger value2 = 10; + BigInteger expected = 0; + + BigInteger result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnGreaterValue_WhenCalledWithOne() + { + BigInteger value1 = 1; + BigInteger value2 = 10; + BigInteger expected = 10; + + BigInteger result1 = value1.LowestCommonMultiple(value2); + BigInteger result2 = value2.LowestCommonMultiple(value1); + + Assert.AreEqual(expected, result1); + Assert.AreEqual(expected, result2); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnOtherValue_WhenCalledWithSameValue() + { + BigInteger value1 = 5; + BigInteger value2 = 5; + BigInteger expected = 5; + + BigInteger result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void LowestCommonMultiple_ShouldReturnCorrectValue_WhenCalledWithNegativeValues() + { + BigInteger value1 = -2; + BigInteger value2 = 3; + BigInteger expected = -6; + + BigInteger result = value1.LowestCommonMultiple(value2); + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void MultiplicativePersistence_ShouldReturn1_ForAnyDigitBeing0() + { + Assert.AreEqual(1, ((BigInteger)10).MultiplicativePersistence()); + Assert.AreEqual(1, ((BigInteger)201).MultiplicativePersistence()); + Assert.AreEqual(1, ((BigInteger)200).MultiplicativePersistence()); + Assert.AreEqual(1, ((BigInteger)20007).MultiplicativePersistence()); + } + + [TestMethod] + public void MultiplicativePersistence_ShouldBeCorrect_ForRecordHolders() + { + Assert.AreEqual(0, ((BigInteger)0).MultiplicativePersistence()); + Assert.AreEqual(1, ((BigInteger)10).MultiplicativePersistence()); + Assert.AreEqual(2, ((BigInteger)25).MultiplicativePersistence()); + Assert.AreEqual(3, ((BigInteger)39).MultiplicativePersistence()); + Assert.AreEqual(4, ((BigInteger)77).MultiplicativePersistence()); + Assert.AreEqual(5, ((BigInteger)679).MultiplicativePersistence()); + Assert.AreEqual(6, ((BigInteger)6788).MultiplicativePersistence()); + Assert.AreEqual(7, ((BigInteger)68889).MultiplicativePersistence()); + Assert.AreEqual(8, ((BigInteger)2677889).MultiplicativePersistence()); + Assert.AreEqual(9, ((BigInteger)26888999).MultiplicativePersistence()); + Assert.AreEqual(10, ((BigInteger)3778888999).MultiplicativePersistence()); + Assert.AreEqual(11, ((BigInteger)277777788888899).MultiplicativePersistence()); + } + + [TestMethod] + public void NegativeFactorialShouldThrow() + { + Assert.ThrowsException(() => ((BigInteger)(-1)).Factorial()); + } +} diff --git a/X10D.Tests/src/Math/IsPrimeTests.cs b/X10D.Tests/src/Math/IsPrimeTests.cs index acc577b..11c336a 100644 --- a/X10D.Tests/src/Math/IsPrimeTests.cs +++ b/X10D.Tests/src/Math/IsPrimeTests.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using System.Numerics; +using System.Reflection; using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; using X10D.Math; @@ -26,6 +27,7 @@ public class IsPrimeTests Assert.IsTrue(value.IsPrime()); Assert.IsTrue(((long)value).IsPrime()); + Assert.IsTrue(((BigInteger)value).IsPrime()); if (value is >= byte.MinValue and <= byte.MaxValue) { @@ -68,6 +70,7 @@ public class IsPrimeTests Assert.IsFalse(value.IsPrime()); Assert.IsFalse(((int)value).IsPrime()); Assert.IsFalse(((long)value).IsPrime()); + Assert.IsFalse(((BigInteger)value).IsPrime()); if (value is >= sbyte.MinValue and <= sbyte.MaxValue) { @@ -85,6 +88,7 @@ public class IsPrimeTests Assert.IsFalse(((byte)value).IsPrime()); Assert.IsFalse(((short)value).IsPrime()); Assert.IsFalse(((long)value).IsPrime()); + Assert.IsFalse(((BigInteger)value).IsPrime()); Assert.IsFalse(((sbyte)value).IsPrime()); Assert.IsFalse(((ushort)value).IsPrime()); @@ -103,6 +107,7 @@ public class IsPrimeTests Assert.AreEqual(expected, ((short)value).IsPrime()); Assert.AreEqual(expected, value.IsPrime()); Assert.AreEqual(expected, ((long)value).IsPrime()); + Assert.AreEqual(expected, ((BigInteger)value).IsPrime()); Assert.AreEqual(expected, ((ushort)value).IsPrime()); Assert.AreEqual(expected, ((uint)value).IsPrime()); @@ -121,6 +126,7 @@ public class IsPrimeTests Assert.AreEqual(expected, ((short)value).IsPrime()); Assert.AreEqual(expected, ((int)value).IsPrime()); Assert.AreEqual(expected, ((long)value).IsPrime()); + Assert.AreEqual(expected, ((BigInteger)value).IsPrime()); Assert.AreEqual(expected, ((ushort)value).IsPrime()); Assert.AreEqual(expected, ((uint)value).IsPrime()); diff --git a/X10D/src/Math/BigIntegerExtensions.cs b/X10D/src/Math/BigIntegerExtensions.cs new file mode 100644 index 0000000..4ce5a0d --- /dev/null +++ b/X10D/src/Math/BigIntegerExtensions.cs @@ -0,0 +1,253 @@ +using System.Diagnostics.Contracts; +using System.Numerics; +using System.Runtime.CompilerServices; +using X10D.CompilerServices; + +namespace X10D.Math; + +/// +/// Math-related extension methods for . +/// +public static class BigIntegerExtensions +{ + /// + /// Computes the digital root of this 8-bit integer. + /// + /// The value whose digital root to compute. + /// The digital root of . + /// The digital root is defined as the recursive sum of digits until that result is a single digit. + /// + /// The digital root is defined as the recursive sum of digits until that result is a single digit. + /// For example, the digital root of 239 is 5: 2 + 3 + 9 = 14, then 1 + 4 = 5. + /// + [Pure] + [MethodImpl(CompilerResources.MethodImplOptions)] + public static int DigitalRoot(this BigInteger value) + { + BigInteger root = BigInteger.Abs(value).Mod(9); + return (int)(root == 0 ? 9 : root); + } + + /// + /// Returns the factorial of the current 64-bit signed integer. + /// + /// The value whose factorial to compute. + /// The factorial of . + /// is less than 0. + [Pure] + [MethodImpl(CompilerResources.MethodImplOptions)] + public static BigInteger Factorial(this BigInteger value) + { + if (value < 0) + { + throw new ArithmeticException(nameof(value)); + } + + if (value == 0) + { + return 1; + } + + BigInteger result = 1; + for (var i = 1L; i <= value; i++) + { + result *= i; + } + + return result; + } + + /// + /// Calculates the greatest common factor between this, and another, . + /// + /// The first value. + /// The second value. + /// The greatest common factor between and . + [Pure] + [MethodImpl(CompilerResources.MethodImplOptions)] + public static BigInteger GreatestCommonFactor(this BigInteger value, BigInteger other) + { + while (other != 0) + { + (value, other) = (other, value % other); + } + + return value; + } + + /// + /// Returns a value indicating whether the current value is not evenly divisible by 2. + /// + /// The value whose parity to check. + /// + /// if is not evenly divisible by 2, or + /// otherwise. + /// + [Pure] + [MethodImpl(CompilerResources.MethodImplOptions)] + public static bool IsOdd(this BigInteger value) + { + return !value.IsEven; + } + + /// + /// Returns a value indicating whether the current value is a prime number. + /// + /// The value whose primality to check. + /// + /// if is prime; otherwise, . + /// + [Pure] + [MethodImpl(CompilerResources.MethodImplOptions)] + public static bool IsPrime(this BigInteger value) + { + if (value <= 1) + { + return false; + } + + if (value <= 3) + { + return true; + } + + if (value.IsEven || value % 3 == 0) + { + return false; + } + + for (var iterator = 5L; iterator * iterator <= value; iterator += 6) + { + if (value % iterator == 0 || value % (iterator + 2) == 0) + { + return false; + } + } + + return true; + } + + /// + /// Calculates the lowest common multiple between the current 64-bit signed integer, and another 64-bit signed integer. + /// + /// The first value. + /// The second value. + /// The lowest common multiple between and . + [Pure] + [MethodImpl(CompilerResources.MethodImplOptions)] + public static BigInteger LowestCommonMultiple(this BigInteger value, BigInteger other) + { + if (value == 0 || other == 0) + { + return 0; + } + + if (value == 1) + { + return other; + } + + if (other == 1) + { + return value; + } + + return value * other / value.GreatestCommonFactor(other); + } + + /// + /// Performs a modulo operation which supports a negative dividend. + /// + /// The dividend. + /// The divisor. + /// The result of dividend mod divisor. + /// + /// The % operator (commonly called the modulo operator) in C# is not defined to be modulo, but is instead + /// remainder. This quirk inherently makes it difficult to use modulo in a negative context, as x % y where x is + /// negative will return a negative value, akin to -(x % y), even if precedence is forced. This method provides a + /// modulo operation which supports negative dividends. + /// + /// ShreevatsaR, https://stackoverflow.com/a/1082938/1467293 + /// CC-BY-SA 2.5 + [Pure] + [MethodImpl(CompilerResources.MethodImplOptions)] + public static BigInteger Mod(this BigInteger dividend, BigInteger divisor) + { + BigInteger r = dividend % divisor; + return r < 0 ? r + divisor : r; + } + + /// + /// Returns the multiplicative persistence of a specified value. + /// + /// The value whose multiplicative persistence to calculate. + /// The multiplicative persistence. + /// + /// Multiplicative persistence is defined as the recursive digital product until that product is a single digit. + /// + [Pure] + [MethodImpl(CompilerResources.MethodImplOptions)] + public static int MultiplicativePersistence(this BigInteger value) + { + var persistence = 0; + BigInteger product = BigInteger.Abs(value); + + while (product > 9) + { + if (value % 10 == 0) + { + return persistence + 1; + } + + while (value > 9) + { + value /= 10; + if (value % 10 == 0) + { + return persistence + 1; + } + } + + BigInteger newProduct = 1; + BigInteger currentProduct = product; + while (currentProduct > 0) + { + newProduct *= currentProduct % 10; + currentProduct /= 10; + } + + product = newProduct; + persistence++; + } + + return persistence; + } + + /// + /// Wraps the current integer between a low and a high value. + /// + /// The value to wrap. + /// The inclusive lower bound. + /// The exclusive upper bound. + /// The wrapped value. + [Pure] + [MethodImpl(CompilerResources.MethodImplOptions)] + public static BigInteger Wrap(this BigInteger value, BigInteger low, BigInteger high) + { + BigInteger difference = high - low; + return low + (((value - low) % difference) + difference) % difference; + } + + /// + /// Wraps the current integer between 0 and a high value. + /// + /// The value to wrap. + /// The exclusive upper bound. + /// The wrapped value. + [Pure] + [MethodImpl(CompilerResources.MethodImplOptions)] + public static BigInteger Wrap(this BigInteger value, BigInteger length) + { + return ((value % length) + length) % length; + } +}