From 465b40bf353de7e1850f60e9820db94aad99fd53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Tue, 28 Oct 2025 23:09:41 +0100 Subject: [PATCH] Add three-A-one-B schedule to combined tester --- .../HeuristicCombinedPrimeTesterTests.cs | 93 +++++++++++++++++++ .../HeuristicCombinedPrimeTester.cs | 24 ++++- 2 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 PerfectNumbers.Core.Tests/HeuristicCombinedPrimeTesterTests.cs diff --git a/PerfectNumbers.Core.Tests/HeuristicCombinedPrimeTesterTests.cs b/PerfectNumbers.Core.Tests/HeuristicCombinedPrimeTesterTests.cs new file mode 100644 index 0000000..d6bdd28 --- /dev/null +++ b/PerfectNumbers.Core.Tests/HeuristicCombinedPrimeTesterTests.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using PerfectNumbers.Core; +using FluentAssertions; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +public class HeuristicCombinedPrimeTesterTests +{ + [Theory] + [Trait("Category", "Fast")] + [InlineData((byte)1)] + [InlineData((byte)7)] + [InlineData((byte)9)] + public void Enumerator_uses_three_a_one_b_schedule_by_default(byte lastDigit) + { + Span groupBState = + stackalloc HeuristicCombinedPrimeTester.HeuristicGroupBSequenceState[4]; + + var enumerator = HeuristicCombinedPrimeTester.CreateHeuristicDivisorEnumerator(500, lastDigit, groupBState); + var results = new List(32); + + while (results.Count < 20 && enumerator.TryGetNext(out var candidate)) + { + results.Add(candidate); + } + + results.Count.Should().BeGreaterThanOrEqualTo(16); + results[0].Value.Should().Be(3UL); + results[0].Group.Should().Be(HeuristicCombinedPrimeTester.HeuristicDivisorGroup.GroupAConstant); + results[1].Value.Should().Be(7UL); + results[1].Group.Should().Be(HeuristicCombinedPrimeTester.HeuristicDivisorGroup.GroupAConstant); + results[2].Value.Should().Be(11UL); + results[2].Group.Should().Be(HeuristicCombinedPrimeTester.HeuristicDivisorGroup.GroupAConstant); + results[3].Value.Should().Be(13UL); + results[3].Group.Should().Be(HeuristicCombinedPrimeTester.HeuristicDivisorGroup.GroupAConstant); + + int index = 4; + const int segmentsToCheck = 3; + + for (int segment = 0; segment < segmentsToCheck; segment++) + { + for (int i = 0; i < 3; i++) + { + HeuristicCombinedPrimeTester.HeuristicDivisorCandidate groupACandidate = results[index++]; + groupACandidate.Value.Should().BeGreaterThan(13UL); + (groupACandidate.Value % 10UL).Should().Be(3UL); + groupACandidate.Group.Should().NotBe(HeuristicCombinedPrimeTester.HeuristicDivisorGroup.GroupB); + } + + HeuristicCombinedPrimeTester.HeuristicDivisorCandidate groupBCandidate = results[index++]; + groupBCandidate.Value.Should().BeGreaterThan(13UL); + (groupBCandidate.Value % 10UL).Should().NotBe(3UL); + groupBCandidate.Group.Should().Be(HeuristicCombinedPrimeTester.HeuristicDivisorGroup.GroupB); + } + } + + [Theory] + [Trait("Category", "Fast")] + [InlineData((byte)1)] + [InlineData((byte)7)] + [InlineData((byte)9)] + public void Three_a_one_b_pattern_interleaves_three_group_a_values_before_group_b(byte lastDigit) + { + ReadOnlySpan combined = HeuristicCombinedPrimeTester.GetCombinedDivisors( + lastDigit, + HeuristicCombinedPrimeTester.CombinedDivisorPattern.ThreeAOneB); + + combined.Length.Should().BeGreaterThan(24); + combined[0].Should().Be(3U); + combined[1].Should().Be(7U); + combined[2].Should().Be(11U); + combined[3].Should().Be(13U); + + int index = 4; + const int segmentsToCheck = 4; + + for (int segment = 0; segment < segmentsToCheck; segment++) + { + for (int i = 0; i < 3; i++) + { + uint groupAValue = combined[index++]; + groupAValue.Should().BeGreaterThan(13U); + (groupAValue % 10U).Should().Be(3U); + } + + uint groupBValue = combined[index++]; + groupBValue.Should().BeGreaterThan(13U); + (groupBValue % 10U).Should().NotBe(3U); + } + } +} diff --git a/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs b/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs index 1942066..4a26976 100644 --- a/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs +++ b/PerfectNumbers.Core/HeuristicCombinedPrimeTester.cs @@ -34,12 +34,18 @@ public sealed class HeuristicCombinedPrimeTester private static readonly Lazy CombinedDivisorsEnding7OneAOneB = new(() => BuildCombinedDivisors(7, CombinedDivisorPattern.OneAOneB), LazyThreadSafetyMode.ExecutionAndPublication); private static readonly Lazy CombinedDivisorsEnding9OneAOneB = new(() => BuildCombinedDivisors(9, CombinedDivisorPattern.OneAOneB), LazyThreadSafetyMode.ExecutionAndPublication); + private static readonly Lazy CombinedDivisorsEnding1ThreeAOneB = new(() => BuildCombinedDivisors(1, CombinedDivisorPattern.ThreeAOneB), LazyThreadSafetyMode.ExecutionAndPublication); + private static readonly Lazy CombinedDivisorsEnding3ThreeAOneB = new(() => BuildCombinedDivisors(3, CombinedDivisorPattern.ThreeAOneB), LazyThreadSafetyMode.ExecutionAndPublication); + private static readonly Lazy CombinedDivisorsEnding7ThreeAOneB = new(() => BuildCombinedDivisors(7, CombinedDivisorPattern.ThreeAOneB), LazyThreadSafetyMode.ExecutionAndPublication); + private static readonly Lazy CombinedDivisorsEnding9ThreeAOneB = new(() => BuildCombinedDivisors(9, CombinedDivisorPattern.ThreeAOneB), LazyThreadSafetyMode.ExecutionAndPublication); + private static readonly ulong[] HeuristicSmallCycleSnapshot = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot(); internal enum CombinedDivisorPattern : byte { TwoAOneB = 0, OneAOneB = 1, + ThreeAOneB = 2, } internal enum HeuristicDivisorGroup : byte @@ -398,7 +404,7 @@ Cleanup: private static ReadOnlySpan GetCombinedDivisors(byte nMod10) { - return GetCombinedDivisors(nMod10, CombinedDivisorPattern.OneAOneB); + return GetCombinedDivisors(nMod10, CombinedDivisorPattern.ThreeAOneB); } internal static ReadOnlySpan GetCombinedDivisors(byte nMod10, CombinedDivisorPattern pattern) @@ -421,6 +427,14 @@ Cleanup: 9 => CombinedDivisorsEnding9OneAOneB.Value, _ => ReadOnlySpan.Empty, }, + CombinedDivisorPattern.ThreeAOneB => nMod10 switch + { + 1 => CombinedDivisorsEnding1ThreeAOneB.Value, + 3 => CombinedDivisorsEnding3ThreeAOneB.Value, + 7 => CombinedDivisorsEnding7ThreeAOneB.Value, + 9 => CombinedDivisorsEnding9ThreeAOneB.Value, + _ => ReadOnlySpan.Empty, + }, _ => ReadOnlySpan.Empty, }; } @@ -440,7 +454,13 @@ Cleanup: } int groupBIndex = GetGroupBStartIndex(groupB); - int groupAInterleaveCount = pattern == CombinedDivisorPattern.OneAOneB ? 1 : 2; + int groupAInterleaveCount = pattern switch + { + CombinedDivisorPattern.OneAOneB => 1, + CombinedDivisorPattern.TwoAOneB => 2, + CombinedDivisorPattern.ThreeAOneB => 3, + _ => 2, + }; while (groupAIndex < groupA.Length || groupBIndex < groupB.Length) {