using System.Collections.Concurrent; using System.Globalization; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; using Open.Numeric.Primes; namespace EvenPerfectBitScanner.ResultsParser; internal static class Program { private static readonly HashSet HelpOptions = new(StringComparer.OrdinalIgnoreCase) { "--?", "-?", "-help", "--help", "--h", "-h", "/?", "/h", "/help" }; private const string DefaultHeader = "p,searchedMersenne,detailedCheck,passedAllTests"; private static int Main(string[] args) { if (args.Length == 0) { PrintHelp(); return 1; } if (args.Length == 1 && IsHelpOption(args[0])) { PrintHelp(); return 0; } string inputPath = args[0]; if (IsHelpOption(inputPath)) { PrintHelp(); return 0; } if (!File.Exists(inputPath)) { Console.Error.WriteLine($"Input file '{inputPath}' was not found."); return 1; } if (!TryParseRangeArguments(args.AsSpan(1), out ulong pMin, out ulong pMax)) { return 1; } if (pMin > pMax) { Console.Error.WriteLine("The --p-min value cannot be greater than --p-max."); return 1; } try { ProcessFile(inputPath, pMin, pMax); return 0; } catch (Exception exception) { Console.Error.WriteLine($"Processing failed: {exception.Message}"); return 1; } } private static bool IsHelpOption(string value) => HelpOptions.Contains(value); private static bool TryParseRangeArguments(ReadOnlySpan arguments, out ulong pMin, out ulong pMax) { pMin = 0UL; pMax = ulong.MaxValue; if (arguments.Length == 0) { return true; } if ((arguments.Length & 1) == 1) { Console.Error.WriteLine("Each range option must be followed by a numeric value."); PrintHelp(); return false; } int index; string option, optionAndValue, value; for (int i = 0; i < arguments.Length; i += 2) { optionAndValue = arguments[i]; index = optionAndValue.IndexOf('='); option = optionAndValue[..index]; value = optionAndValue[(index + 1)..]; try { switch (option) { case "--p-min": pMin = ParseRangeValue(option, value); break; case "--p-max": pMax = ParseRangeValue(option, value); break; default: Console.Error.WriteLine($"Unsupported option '{option}'."); PrintHelp(); return false; } } catch (FormatException exception) { Console.Error.WriteLine(exception.Message); PrintHelp(); return false; } } return true; } private static ulong ParseRangeValue(in string option, in string value) { if (!ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong parsed)) { throw new FormatException($"Value '{value}' for option '{option}' is not a valid non-negative integer."); } return parsed; } private static void PrintHelp() { Console.WriteLine("EvenPerfectBitScanner.ResultsParser"); Console.WriteLine("Parses CSV results produced by the candidate scanner."); Console.WriteLine(); Console.WriteLine("Usage:"); Console.WriteLine(" EvenPerfectBitScanner.ResultsParser [--p-min ] [--p-max ]"); Console.WriteLine(); Console.WriteLine("The program accepts the following help switches:"); Console.WriteLine(" --?, -?, /?, -help, --help, --h, -h, /h, /help"); Console.WriteLine(); Console.WriteLine("Optional range arguments allow filtering primes by exponent:"); Console.WriteLine(" --p-min - include entries with p >= value"); Console.WriteLine(" --p-max - include entries with p <= value"); Console.WriteLine(); Console.WriteLine("Outputs prime results into four files placed next to the source file:"); Console.WriteLine(" raw-primes- - prime entries in the original order"); Console.WriteLine(" sorted-primes- - prime entries sorted by p"); Console.WriteLine(" sorted-primes-passed- - sorted prime entries with passedAllTests = true"); Console.WriteLine(" sorted-primes-rejected- - sorted prime entries with passedAllTests = false"); Console.WriteLine(); Console.WriteLine("Example:"); Console.WriteLine(" EvenPerfectBitScanner.ResultsParser results.csv --p-min 89 --p-max 107"); } private const int CandidateBufferCapacity = 512; private static void ProcessFile(in string inputPath, ulong pMin, ulong pMax) { using IEnumerator enumerator = File.ReadLines(inputPath).GetEnumerator(); if (!enumerator.MoveNext()) { Console.WriteLine("Input file did not contain any data. Created empty outputs using the default header."); string fallbackHeader = DefaultHeader; string rawFallbackPath = BuildOutputPath(inputPath, "raw-primes-"); WriteCsv(rawFallbackPath, fallbackHeader, File.Exists(rawFallbackPath), []); CreateEmptyOutputs(inputPath, fallbackHeader); return; } string headerLine = enumerator.Current; string header = string.IsNullOrWhiteSpace(headerLine) ? DefaultHeader : headerLine.Trim(); using IEnumerator primeEnumerator = Prime.Numbers.GetEnumerator(); if (!primeEnumerator.MoveNext()) { throw new InvalidOperationException("Prime generator did not provide any values."); } ulong currentPrime = primeEnumerator.Current; PendingCandidateList pendingResults = new(); List primeResults = []; int consoleProgress = 0; ulong lastReportedCandidate = 0UL; CandidateResult[] candidateBuffer = new CandidateResult[CandidateBufferCapacity]; int bufferedCount = 0; ulong pEmpty = CandidateResult.Empty.P; while (enumerator.MoveNext()) { string currentLine = enumerator.Current; if (string.IsNullOrWhiteSpace(currentLine)) { continue; } CandidateResult result = ParseLine(currentLine, pMin, pMax); if (result.P == pEmpty) { continue; } candidateBuffer[bufferedCount++] = result; lastReportedCandidate = result.P; if (bufferedCount == CandidateBufferCapacity) { ProcessCandidateBuffer(candidateBuffer.AsSpan(0, bufferedCount), pendingResults); DrainPrimeMatches(pendingResults, primeResults, ref currentPrime, primeEnumerator, false); bufferedCount = 0; } if (++consoleProgress == 10_000) { Console.WriteLine($"Processed {lastReportedCandidate}"); consoleProgress = 0; } } if (bufferedCount > 0) { ProcessCandidateBuffer(candidateBuffer.AsSpan(0, bufferedCount), pendingResults); DrainPrimeMatches(pendingResults, primeResults, ref currentPrime, primeEnumerator, false); } DrainPrimeMatches(pendingResults, primeResults, ref currentPrime, primeEnumerator, true); string rawOutputPath = BuildOutputPath(inputPath, "raw-primes-"); bool append = File.Exists(rawOutputPath); WriteCsv(rawOutputPath, header, append, primeResults); if (primeResults.Count == 0) { Console.WriteLine("No prime entries were found in the input file."); Console.WriteLine($"Created an empty prime snapshot at: {rawOutputPath}"); CreateEmptyOutputs(inputPath, header); return; } List sortedResults = [.. primeResults]; Console.WriteLine("Sorting candidates..."); SortByPrime(sortedResults); Console.WriteLine("Splitting results..."); SplitResults(sortedResults, out List passedResults, out List rejectedResults); Console.WriteLine("Saving files..."); string sortedOutputPath = BuildOutputPath(inputPath, "sorted-primes-"); string passedOutputPath = BuildOutputPath(inputPath, "sorted-primes-passed-"); string rejectedOutputPath = BuildOutputPath(inputPath, "sorted-primes-rejected-"); append = File.Exists(sortedOutputPath); WriteCsv(sortedOutputPath, header, append, sortedResults); append = File.Exists(passedOutputPath); WriteCsv(passedOutputPath, header, append, passedResults); append = File.Exists(rejectedOutputPath); WriteCsv(rejectedOutputPath, header, append, rejectedResults); Console.WriteLine($"Processed {primeResults.Count} prime entr{(primeResults.Count == 1 ? "y" : "ies")}."); Console.WriteLine($"Prime results (raw order): {rawOutputPath}"); Console.WriteLine($"Prime results (sorted): {sortedOutputPath}"); Console.WriteLine($"Prime results (passed): {passedOutputPath}"); Console.WriteLine($"Prime results (rejected): {rejectedOutputPath}"); } private static void CreateEmptyOutputs(in string inputPath, in string header) { string sortedOutputPath = BuildOutputPath(inputPath, "sorted-primes-"); string passedOutputPath = BuildOutputPath(inputPath, "sorted-primes-passed-"); string rejectedOutputPath = BuildOutputPath(inputPath, "sorted-primes-rejected-"); WriteCsv(sortedOutputPath, header, false, []); WriteCsv(passedOutputPath, header, false, []); WriteCsv(rejectedOutputPath, header, false, []); } private static CandidateResult ParseLine(in string line, ulong pMin, ulong pMax) { ReadOnlySpan span = line.AsSpan(); int firstCommaIndex = span.IndexOf(','); int lastCommaIndex = span.LastIndexOf(','); if (firstCommaIndex < 0 || lastCommaIndex <= firstCommaIndex) { throw new FormatException("Input line does not contain expected comma separators."); } ulong p = ulong.Parse(span[..firstCommaIndex], NumberStyles.Integer, CultureInfo.InvariantCulture); if (p < pMin || p > pMax) { return CandidateResult.Empty; } bool passedAllTests = bool.Parse(span[(lastCommaIndex + 1)..]); return new CandidateResult(p, passedAllTests, line); } private static string BuildOutputPath(in string inputPath, in string prefix) { string fileName = Path.GetFileName(inputPath); string prefixedName = prefix + fileName; string? directory = Path.GetDirectoryName(inputPath); if (string.IsNullOrEmpty(directory)) { return prefixedName; } return Path.Combine(directory, prefixedName); } private static void WriteCsv(in string path, in string header, bool append, IEnumerable entries) { using StreamWriter writer = new(path, append, Encoding.UTF8); if (!append) { writer.WriteLine(header); } foreach (CandidateResult entry in entries) { writer.WriteLine(entry.Csv); } } private static void ProcessCandidateBuffer(Span buffer, PendingCandidateList pendingResults) { if (buffer.Length == 0) { return; } buffer.Sort(CandidateResultComparer.Instance); int start = 0; int length = buffer.Length; while (start < length) { int end = start + 1; ulong previous = buffer[start].P; while (end < length) { ulong current = buffer[end].P; if (current > previous + 1) { break; } previous = current; end++; } pendingResults.AddRange(buffer[start..end]); start = end; } } private static void SortByPrime(List results) { if (results.Count < 2) { return; } results.Sort(CandidateResultComparer.Instance); } private static void SplitResults( List sortedResults, out List passedResults, out List rejectedResults) { int count = sortedResults.Count; if (count == 0) { passedResults = []; rejectedResults = []; return; } if (count < Environment.ProcessorCount * 4) { passedResults = new List(count); rejectedResults = new List(count); for (int i = 0; i < count; i++) { CandidateResult candidate = sortedResults[i]; if (candidate.PassedAllTests) { passedResults.Add(candidate); continue; } rejectedResults.Add(candidate); } return; } List sharedPassed = new(count); List sharedRejected = new(count); object passedLock = new(); object rejectedLock = new(); Parallel.ForEach( Partitioner.Create(0, count), () => new SplitAccumulator(), (range, _, local) => { List localPassed = local.Passed; List localRejected = local.Rejected; for (int i = range.Item1; i < range.Item2; i++) { CandidateResult candidate = sortedResults[i]; if (candidate.PassedAllTests) { localPassed.Add(candidate); continue; } localRejected.Add(candidate); } return local; }, local => { if (local.Passed.Count > 0) { lock (passedLock) { sharedPassed.AddRange(local.Passed); } } if (local.Rejected.Count > 0) { lock (rejectedLock) { sharedRejected.AddRange(local.Rejected); } } }); passedResults = sharedPassed; rejectedResults = sharedRejected; } private sealed class CandidateResultComparer : IComparer { public static readonly CandidateResultComparer Instance = new(); public int Compare(CandidateResult x, CandidateResult y) { if (x.P == y.P) { return 0; } return x.P < y.P ? -1 : 1; } } private static void DrainPrimeMatches( PendingCandidateList pendingResults, List primeResults, ref ulong currentPrime, IEnumerator primeEnumerator, bool inputCompleted) { if (pendingResults.Count == 0) { return; } pendingResults.RemoveLessThan(currentPrime); bool extractedPrime; while (pendingResults.Count > 0) { extractedPrime = ExtractPrimeEntries(pendingResults, primeResults, currentPrime); if (extractedPrime) { if (!TryAdvancePrime(primeEnumerator, ref currentPrime, pendingResults)) { pendingResults.Clear(); return; } pendingResults.RemoveLessThan(currentPrime); continue; } if (!inputCompleted) { return; } if (!TryAdvancePrime(primeEnumerator, ref currentPrime, pendingResults)) { pendingResults.Clear(); return; } pendingResults.RemoveLessThan(currentPrime); } } private static bool ExtractPrimeEntries( PendingCandidateList pendingResults, List primeResults, ulong prime) { if (pendingResults.Count == 0) { return false; } if (!pendingResults.TryPeek(out CandidateResult next) || next.P != prime) { return false; } do { primeResults.Add(pendingResults.Dequeue()); } while (pendingResults.TryPeek(out CandidateResult candidate) && candidate.P == prime); return true; } private static bool TryAdvancePrime( IEnumerator primeEnumerator, ref ulong currentPrime, PendingCandidateList pendingResults) { ulong minimumPending = pendingResults.Count > 0 && pendingResults.TryPeek(out CandidateResult pending) ? pending.P : 0UL; do { if (!primeEnumerator.MoveNext()) { currentPrime = ulong.MaxValue; return false; } currentPrime = primeEnumerator.Current; } while (pendingResults.Count > 0 && currentPrime < minimumPending); return true; } private sealed class PendingCandidateList { private readonly List _items = []; public int Count => _items.Count; [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AddRange(ReadOnlySpan candidates) { if (candidates.Length == 0) { return; } List items = _items; items.EnsureCapacity(items.Count + candidates.Length); int insertIndex = FindInsertIndex(items, candidates[0].P); int candidateIndex = 0; int candidateCount = candidates.Length; while (candidateIndex < candidateCount) { CandidateResult candidate = candidates[candidateIndex]; while (insertIndex < items.Count && items[insertIndex].P <= candidate.P) { insertIndex++; } items.Insert(insertIndex, candidate); insertIndex++; candidateIndex++; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryPeek(out CandidateResult result) { if (_items.Count == 0) { result = default; return false; } result = _items[0]; return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public CandidateResult Dequeue() { CandidateResult value = _items[0]; _items.RemoveAt(0); return value; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void RemoveLessThan(ulong prime) { int removeCount = 0; List items = _items; int count = items.Count; while (removeCount < count && items[removeCount].P < prime) { removeCount++; } if (removeCount > 0) { items.RemoveRange(0, removeCount); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Clear() { _items.Clear(); } private static int FindInsertIndex(List items, ulong value) { int low = 0; int high = items.Count; while (low < high) { int mid = (low + high) >> 1; if (items[mid].P < value) { low = mid + 1; continue; } high = mid; } return low; } } private readonly struct SplitAccumulator { public readonly List Passed; public readonly List Rejected; public SplitAccumulator() { Passed = []; Rejected = []; } } private readonly record struct CandidateResult(ulong P, bool PassedAllTests, string Csv) { public static readonly CandidateResult Empty = new(0UL, false, ""); public readonly string Csv = Csv; public readonly ulong P = P; public readonly bool PassedAllTests = PassedAllTests; } }