using System.Globalization; using System.Numerics; using System.Text; namespace BuildDivisorTransitions; internal static class Program { private const string OutputFileName = "NextKForecastTraning.csv"; private static int Main(string[] args) { if (args.Length != 2) { PrintUsage(); return 1; } string candidatesPath = args[0]; string divisorsPath = args[1]; if (!File.Exists(candidatesPath)) { Console.Error.WriteLine($"Candidates file not found: {candidatesPath}"); return 1; } if (!File.Exists(divisorsPath)) { Console.Error.WriteLine($"Divisors file not found: {divisorsPath}"); return 1; } List candidatePs = LoadCandidates(candidatesPath); Dictionary divisors = LoadDivisors(divisorsPath); List transitions = BuildTransitions(candidatePs, divisors); WriteOutput(transitions); Console.WriteLine($"Saved {transitions.Count} transitions to {OutputFileName}."); return 0; } private static void PrintUsage() { Console.WriteLine("Usage: BuildDivisorTransitions "); Console.WriteLine(" candidatesCsv: CSV with columns p,searchedMersenne,detailedCheck,passedAllTests"); Console.WriteLine(" divisorsCsv: CSV with columns p,divisor"); } private static List LoadCandidates(string path) { var result = new List(); var seen = new HashSet(); using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true); while (!reader.EndOfStream) { string? line = reader.ReadLine(); if (string.IsNullOrWhiteSpace(line)) { continue; } string trimmedLine = line.Trim(); if (trimmedLine.StartsWith("p", StringComparison.OrdinalIgnoreCase)) { continue; } string[] parts = trimmedLine.Split(',', StringSplitOptions.TrimEntries); if (parts.Length == 0) { continue; } if (!long.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out long p)) { throw new FormatException($"Invalid p value '{parts[0]}' in {path}."); } if (seen.Add(p)) { result.Add(p); } } return result; } private static Dictionary LoadDivisors(string path) { var result = new Dictionary(); using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true); while (!reader.EndOfStream) { string? line = reader.ReadLine(); if (string.IsNullOrWhiteSpace(line)) { continue; } string trimmedLine = line.Trim(); if (trimmedLine.StartsWith("p", StringComparison.OrdinalIgnoreCase)) { continue; } string[] parts = trimmedLine.Split(',', StringSplitOptions.TrimEntries); if (parts.Length < 2) { throw new FormatException($"Line '{line}' in {path} does not contain both p and divisor columns."); } if (!long.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out long p)) { throw new FormatException($"Invalid p value '{parts[0]}' in {path}."); } if (!BigInteger.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out BigInteger divisor)) { throw new FormatException($"Invalid divisor value '{parts[1]}' for p={p} in {path}."); } if (IsProperMersenneDivisor(p, divisor)) { result[p] = divisor; } else { Console.Error.WriteLine($"Skipping invalid divisor entry p={p}, divisor={divisor} (not a proper divisor of 2^{p}-1 or fails powmod check)."); } } return result; } private static List BuildTransitions(IReadOnlyList candidatePs, IReadOnlyDictionary divisors) { var transitions = new List(); for (int i = 0; i < candidatePs.Count - 1; i++) { long currentP = candidatePs[i]; long nextP = candidatePs[i + 1]; if (!divisors.TryGetValue(currentP, out BigInteger currentDivisor)) { continue; } if (!divisors.TryGetValue(nextP, out BigInteger nextDivisor)) { continue; } BigInteger currentK = CalculateK(currentP, currentDivisor); BigInteger nextK = CalculateK(nextP, nextDivisor); bool isNextKHigher = nextK > currentK; transitions.Add(new Transition(currentP, currentDivisor, LastDigit(currentP), currentK, LastMersenneDigit(currentP), isNextKHigher)); } return transitions; } private static BigInteger CalculateK(long p, BigInteger divisor) { BigInteger numerator = divisor - BigInteger.One; BigInteger denominator = new BigInteger(p) << 1; if (denominator <= BigInteger.Zero) { throw new InvalidOperationException($"Computed 2p is not positive for p={p}."); } BigInteger remainder = numerator % denominator; if (remainder != BigInteger.Zero) { throw new InvalidOperationException($"Divisor {divisor} is not congruent to 1 mod 2p for p={p}."); } BigInteger k = numerator / denominator; if (k < BigInteger.One) { throw new InvalidOperationException($"Calculated k={k} is less than 1 for p={p}, divisor={divisor}."); } return k; } private static bool IsProperMersenneDivisor(long p, in BigInteger divisor) { if (p <= 1 || divisor <= BigInteger.One) { return false; } int bitLength = GetBitLength(divisor); if (bitLength > p) { return false; } if (bitLength == p && IsPowerOfTwo(divisor + BigInteger.One)) { // Divisor equals the Mersenne number itself. return false; } return BigInteger.ModPow(2, p, divisor) == BigInteger.One; } private static int GetBitLength(in BigInteger value) { if (value.IsZero) { return 0; } byte[] bytes = value.ToByteArray(isUnsigned: true, isBigEndian: false); byte msb = bytes[^1]; return ((bytes.Length - 1) * 8) + (BitOperations.Log2(msb) + 1); } private static bool IsPowerOfTwo(in BigInteger value) { return value > BigInteger.One && (value & (value - BigInteger.One)) == BigInteger.Zero; } private static void WriteOutput(IReadOnlyCollection transitions) { using var writer = new StreamWriter(OutputFileName, false, Encoding.UTF8); writer.WriteLine("p,divisor,lastDigit,k,lastMersenneDigit,isNextKHigher"); foreach (Transition transition in transitions) { writer.WriteLine($"{transition.P.ToString(CultureInfo.InvariantCulture)},{FormatBigInteger(transition.Divisor)},{transition.LastDigit},{FormatBigInteger(transition.CurrentK)},{transition.LastMersenneDigit},{(transition.IsNextKHigher ? "1" : "0")}"); } } private static int LastDigit(long p) => (int)(Math.Abs(p) % 10); private static int LastMersenneDigit(long p) { long mod = p & 3; return mod == 1 ? 1 : 7; // odd primes give p%4 == 1 or 3; 2^p mod 10 cycles -> 1 or 7 after subtracting 1 } private static string FormatBigInteger(in BigInteger value) => value.ToString(CultureInfo.InvariantCulture); private sealed record Transition(long P, BigInteger Divisor, int LastDigit, BigInteger CurrentK, int LastMersenneDigit, bool IsNextKHigher); }