feat: add math extensions for BigInteger

This commit is contained in:
Oliver Booth 2023-04-05 11:05:53 +01:00
parent 5e4af9a9e1
commit dc1b9d6c04
No known key found for this signature in database
GPG Key ID: 20BEB9DC87961025
5 changed files with 525 additions and 1 deletions

View File

@ -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`.

View File

@ -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);
}
}
}

View File

@ -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<ArithmeticException>(() => ((BigInteger)(-1)).Factorial());
}
}

View File

@ -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());

View File

@ -0,0 +1,253 @@
using System.Diagnostics.Contracts;
using System.Numerics;
using System.Runtime.CompilerServices;
using X10D.CompilerServices;
namespace X10D.Math;
/// <summary>
/// Math-related extension methods for <see cref="BigInteger" />.
/// </summary>
public static class BigIntegerExtensions
{
/// <summary>
/// Computes the digital root of this 8-bit integer.
/// </summary>
/// <param name="value">The value whose digital root to compute.</param>
/// <returns>The digital root of <paramref name="value" />.</returns>
/// <remarks>The digital root is defined as the recursive sum of digits until that result is a single digit.</remarks>
/// <remarks>
/// <para>The digital root is defined as the recursive sum of digits until that result is a single digit.</para>
/// <para>For example, the digital root of 239 is 5: <c>2 + 3 + 9 = 14</c>, then <c>1 + 4 = 5</c>.</para>
/// </remarks>
[Pure]
[MethodImpl(CompilerResources.MethodImplOptions)]
public static int DigitalRoot(this BigInteger value)
{
BigInteger root = BigInteger.Abs(value).Mod(9);
return (int)(root == 0 ? 9 : root);
}
/// <summary>
/// Returns the factorial of the current 64-bit signed integer.
/// </summary>
/// <param name="value">The value whose factorial to compute.</param>
/// <returns>The factorial of <paramref name="value" />.</returns>
/// <exception cref="ArithmeticException"><paramref name="value" /> is less than 0.</exception>
[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;
}
/// <summary>
/// Calculates the greatest common factor between this, and another, <see cref="BigInteger" />.
/// </summary>
/// <param name="value">The first value.</param>
/// <param name="other">The second value.</param>
/// <returns>The greatest common factor between <paramref name="value" /> and <paramref name="other" />.</returns>
[Pure]
[MethodImpl(CompilerResources.MethodImplOptions)]
public static BigInteger GreatestCommonFactor(this BigInteger value, BigInteger other)
{
while (other != 0)
{
(value, other) = (other, value % other);
}
return value;
}
/// <summary>
/// Returns a value indicating whether the current value is not evenly divisible by 2.
/// </summary>
/// <param name="value">The value whose parity to check.</param>
/// <returns>
/// <see langword="true" /> if <paramref name="value" /> is not evenly divisible by 2, or <see langword="false" />
/// otherwise.
/// </returns>
[Pure]
[MethodImpl(CompilerResources.MethodImplOptions)]
public static bool IsOdd(this BigInteger value)
{
return !value.IsEven;
}
/// <summary>
/// Returns a value indicating whether the current value is a prime number.
/// </summary>
/// <param name="value">The value whose primality to check.</param>
/// <returns>
/// <see langword="true" /> if <paramref name="value" /> is prime; otherwise, <see langword="false" />.
/// </returns>
[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;
}
/// <summary>
/// Calculates the lowest common multiple between the current 64-bit signed integer, and another 64-bit signed integer.
/// </summary>
/// <param name="value">The first value.</param>
/// <param name="other">The second value.</param>
/// <returns>The lowest common multiple between <paramref name="value" /> and <paramref name="other" />.</returns>
[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);
}
/// <summary>
/// Performs a modulo operation which supports a negative dividend.
/// </summary>
/// <param name="dividend">The dividend.</param>
/// <param name="divisor">The divisor.</param>
/// <returns>The result of <c>dividend mod divisor</c>.</returns>
/// <remarks>
/// The <c>%</c> 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 <c>x % y</c> where x is
/// negative will return a negative value, akin to <c>-(x % y)</c>, even if precedence is forced. This method provides a
/// modulo operation which supports negative dividends.
/// </remarks>
/// <author>ShreevatsaR, https://stackoverflow.com/a/1082938/1467293</author>
/// <license>CC-BY-SA 2.5</license>
[Pure]
[MethodImpl(CompilerResources.MethodImplOptions)]
public static BigInteger Mod(this BigInteger dividend, BigInteger divisor)
{
BigInteger r = dividend % divisor;
return r < 0 ? r + divisor : r;
}
/// <summary>
/// Returns the multiplicative persistence of a specified value.
/// </summary>
/// <param name="value">The value whose multiplicative persistence to calculate.</param>
/// <returns>The multiplicative persistence.</returns>
/// <remarks>
/// Multiplicative persistence is defined as the recursive digital product until that product is a single digit.
/// </remarks>
[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;
}
/// <summary>
/// Wraps the current integer between a low and a high value.
/// </summary>
/// <param name="value">The value to wrap.</param>
/// <param name="low">The inclusive lower bound.</param>
/// <param name="high">The exclusive upper bound.</param>
/// <returns>The wrapped value.</returns>
[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;
}
/// <summary>
/// Wraps the current integer between 0 and a high value.
/// </summary>
/// <param name="value">The value to wrap.</param>
/// <param name="length">The exclusive upper bound.</param>
/// <returns>The wrapped value.</returns>
[Pure]
[MethodImpl(CompilerResources.MethodImplOptions)]
public static BigInteger Wrap(this BigInteger value, BigInteger length)
{
return ((value % length) + length) % length;
}
}