diff --git a/csharp/Platform.Numbers.Tests/MathTests.cs b/csharp/Platform.Numbers.Tests/MathTests.cs index b5f38cd..cb68211 100644 --- a/csharp/Platform.Numbers.Tests/MathTests.cs +++ b/csharp/Platform.Numbers.Tests/MathTests.cs @@ -1,10 +1,93 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net.Http; +using System.Numerics; +using System.Text.RegularExpressions; using Xunit; namespace Platform.Numbers.Tests { public static class MathTests { + private static readonly TimeSpan ComputationTimeLimit = TimeSpan.FromSeconds(10); + private static readonly HttpClient HttpClient = new(); + + // Fetches all entries from an OEIS b-file (plain text format: "n value" per line). + // Returns a dictionary mapping index -> value as BigInteger. + // The b-file URL format is: https://oeis.org/AXXXXXX/b000XXX.txt + private static Dictionary FetchOeisSequence(string bFileUrl) + { + var result = new Dictionary(); + string content = HttpClient.GetStringAsync(bFileUrl).GetAwaiter().GetResult(); + foreach (var line in content.Split('\n')) + { + var trimmed = line.Trim(); + if (trimmed.StartsWith("#") || trimmed.Length == 0) + { + continue; + } + var parts = Regex.Split(trimmed, @"\s+"); + if (parts.Length >= 2 && ulong.TryParse(parts[0], out var index) && BigInteger.TryParse(parts[1], out var value)) + { + result[index] = value; + } + } + return result; + } + + // Lazily-fetched OEIS sequences, cached for the lifetime of the test run. + private static Dictionary? _oeisFactorials; + private static Dictionary OeisFactorials => + _oeisFactorials ??= FetchOeisSequence("https://oeis.org/A000142/b000142.txt"); + + private static Dictionary? _oeisCatalans; + private static Dictionary OeisCatalans => + _oeisCatalans ??= FetchOeisSequence("https://oeis.org/A000108/b000108.txt"); + + // Computes factorial of n using BigInteger so it never overflows. + // Returns null if computation exceeds the time limit. + private static BigInteger? ComputeFactorialWithTimeLimit(ulong n) + { + var stopwatch = Stopwatch.StartNew(); + BigInteger result = BigInteger.One; + for (ulong i = 2; i <= n; i++) + { + result *= i; + if (stopwatch.Elapsed > ComputationTimeLimit) + { + return null; + } + } + return result; + } + + // Computes the nth Catalan number from scratch using the formula: + // C(n) = (2n)! / ((n+1)! * n!) + // Returns null if computation exceeds the time limit. + private static BigInteger? ComputeCatalanWithTimeLimit(ulong n) + { + var twoNFact = ComputeFactorialWithTimeLimit(2 * n); + if (twoNFact is null) + { + return null; + } + + var nPlusOneFact = ComputeFactorialWithTimeLimit(n + 1); + if (nPlusOneFact is null) + { + return null; + } + + var nFact = ComputeFactorialWithTimeLimit(n); + if (nFact is null) + { + return null; + } + + return twoNFact / (nPlusOneFact * nFact); + } + [Theory] [InlineData(0ul, 1ul)] [InlineData(1ul, 1ul)] @@ -107,5 +190,58 @@ public static void MaximumConstantsTest() Assert.Equal(20ul, Math.MaximumFactorialNumber); Assert.Equal(36ul, Math.MaximumCatalanIndex); } + + [Fact] + public static void PrecalculatedFactorialsMatchComputedValues() + { + // Verify that every precalculated factorial constant in Math._factorials is actually + // a correct factorial value. For values that can be computed locally within 10 seconds, + // an independent BigInteger computation is used. For values that take longer, the + // expected value is fetched from OEIS A000142 (https://oeis.org/A000142) via HTTP. + for (ulong n = 0; n <= Math.MaximumFactorialNumber; n++) + { + var computed = ComputeFactorialWithTimeLimit(n); + BigInteger expected; + if (computed is null) + { + // Computation exceeded 10 seconds — verify against OEIS A000142. + Assert.True(OeisFactorials.TryGetValue(n, out expected), + $"OEIS A000142 did not contain a value for n={n}"); + } + else + { + expected = computed.Value; + } + var precalculated = Math.Factorial(n); + Assert.Equal((ulong)expected, precalculated); + } + } + + [Fact] + public static void PrecalculatedCatalansMatchComputedFactorials() + { + // Verify that every precalculated Catalan constant in Math._catalans is actually + // a correct Catalan number. For values that can be computed locally within 10 seconds + // using the formula C(n) = (2n)! / ((n+1)! * n!), an independent BigInteger computation + // is used. For values that take longer, the expected value is fetched from OEIS A000108 + // (https://oeis.org/A000108) via HTTP. + for (ulong n = 0; n <= Math.MaximumCatalanIndex; n++) + { + var computed = ComputeCatalanWithTimeLimit(n); + BigInteger expected; + if (computed is null) + { + // Computation exceeded 10 seconds — verify against OEIS A000108. + Assert.True(OeisCatalans.TryGetValue(n, out expected), + $"OEIS A000108 did not contain a value for n={n}"); + } + else + { + expected = computed.Value; + } + var precalculated = Math.Catalan(n); + Assert.Equal((ulong)expected, precalculated); + } + } } -} \ No newline at end of file +}