using System.Globalization; using System.Text; 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,isPerfect"; private static int Main(string[] args) { if (args.Length == 0) { PrintHelp(); return 1; } if (args.Length == 1 && IsHelpOption(args[0])) { PrintHelp(); return 0; } if (args.Length != 1) { Console.Error.WriteLine("Only a single CSV file path is supported."); PrintHelp(); return 1; } string inputPath = args[0]; if (!File.Exists(inputPath)) { Console.Error.WriteLine($"Input file '{inputPath}' was not found."); return 1; } try { ProcessFile(inputPath); return 0; } catch (Exception exception) { Console.Error.WriteLine($"Processing failed: {exception.Message}"); return 1; } } private static bool IsHelpOption(string value) { return HelpOptions.Contains(value); } 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 "); Console.WriteLine(); Console.WriteLine("The program accepts the following help switches:"); Console.WriteLine(" --?, -?, /?, -help, --help, --h, -h, /h, /help"); 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 isPerfect = true"); Console.WriteLine(" sorted-primes-rejected- - sorted prime entries with isPerfect = false"); Console.WriteLine(); Console.WriteLine("Example:"); Console.WriteLine(" EvenPerfectBitScanner.ResultsParser results.csv"); } private static void ProcessFile(string inputPath) { 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, Array.Empty()); 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; List pendingResults = new(); List primeResults = new(); while (enumerator.MoveNext()) { string currentLine = enumerator.Current; if (string.IsNullOrWhiteSpace(currentLine)) { continue; } CandidateResult result = ParseLine(currentLine); InsertPendingResult(pendingResults, result); DrainPrimeMatches(pendingResults, primeResults, ref currentPrime, primeEnumerator, false); } DrainPrimeMatches(pendingResults, primeResults, ref currentPrime, primeEnumerator, true); string rawOutputPath = BuildOutputPath(inputPath, "raw-primes-"); WriteCsv(rawOutputPath, header, 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 = new(primeResults); BubbleSort(sortedResults); List passedResults = new(); List rejectedResults = new(); for (int i = 0; i < sortedResults.Count; i++) { CandidateResult candidate = sortedResults[i]; if (candidate.IsPerfect) { passedResults.Add(candidate); } else { rejectedResults.Add(candidate); } } string sortedOutputPath = BuildOutputPath(inputPath, "sorted-primes-"); string passedOutputPath = BuildOutputPath(inputPath, "sorted-primes-passed-"); string rejectedOutputPath = BuildOutputPath(inputPath, "sorted-primes-rejected-"); WriteCsv(sortedOutputPath, header, sortedResults); WriteCsv(passedOutputPath, header, passedResults); WriteCsv(rejectedOutputPath, header, 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(string inputPath, 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, Array.Empty()); WriteCsv(passedOutputPath, header, Array.Empty()); WriteCsv(rejectedOutputPath, header, Array.Empty()); } private static CandidateResult ParseLine(string line) { string trimmedLine = line.Trim(); ReadOnlySpan span = trimmedLine.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."); } ReadOnlySpan pSpan = span[..firstCommaIndex]; ReadOnlySpan isPerfectSpan = span[(lastCommaIndex + 1)..]; ulong p = ulong.Parse(pSpan, NumberStyles.Integer, CultureInfo.InvariantCulture); bool isPerfect = bool.Parse(isPerfectSpan); return new CandidateResult(p, isPerfect, trimmedLine); } private static string BuildOutputPath(string inputPath, 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(string path, string header, IEnumerable entries) { using StreamWriter writer = new(path, false, Encoding.UTF8); writer.WriteLine(header); foreach (CandidateResult entry in entries) { writer.WriteLine(entry.ToCsv()); } } private static void BubbleSort(List results) { if (results.Count < 2) { return; } int length = results.Count; bool swapped; do { swapped = false; for (int i = 1; i < length; i++) { if (results[i - 1].P <= results[i].P) { continue; } (results[i - 1], results[i]) = (results[i], results[i - 1]); swapped = true; } length--; } while (swapped); } private static void DrainPrimeMatches( List pendingResults, List primeResults, ref ulong currentPrime, IEnumerator primeEnumerator, bool inputCompleted) { if (pendingResults.Count == 0) { return; } RemoveEntriesBelowPrime(pendingResults, currentPrime); while (pendingResults.Count > 0) { bool extractedPrime = ExtractPrimeEntries(pendingResults, primeResults, currentPrime); if (extractedPrime) { if (!TryAdvancePrime(primeEnumerator, ref currentPrime, pendingResults)) { pendingResults.Clear(); return; } RemoveEntriesBelowPrime(pendingResults, currentPrime); continue; } if (!inputCompleted) { return; } if (!TryAdvancePrime(primeEnumerator, ref currentPrime, pendingResults)) { pendingResults.Clear(); return; } RemoveEntriesBelowPrime(pendingResults, currentPrime); } } private static bool ExtractPrimeEntries( List pendingResults, List primeResults, ulong prime) { if (pendingResults.Count == 0) { return false; } int startIndex = FindFirstIndexAtLeast(pendingResults, prime); if (startIndex >= pendingResults.Count) { return false; } CandidateResult firstCandidate = pendingResults[startIndex]; if (firstCandidate.P != prime) { return false; } int endIndex = startIndex + 1; while (endIndex < pendingResults.Count && pendingResults[endIndex].P == prime) { endIndex++; } for (int i = startIndex; i < endIndex; i++) { primeResults.Add(pendingResults[i]); } pendingResults.RemoveRange(startIndex, endIndex - startIndex); return true; } private static void RemoveEntriesBelowPrime(List pendingResults, ulong currentPrime) { if (pendingResults.Count == 0) { return; } int removeCount = FindFirstIndexAtLeast(pendingResults, currentPrime); if (removeCount == 0) { return; } pendingResults.RemoveRange(0, removeCount); } private static bool TryAdvancePrime( IEnumerator primeEnumerator, ref ulong currentPrime, List pendingResults) { ulong minimumPending = pendingResults.Count > 0 ? pendingResults[0].P : 0UL; do { if (!primeEnumerator.MoveNext()) { currentPrime = ulong.MaxValue; return false; } currentPrime = primeEnumerator.Current; } while (pendingResults.Count > 0 && currentPrime < minimumPending); return true; } private static void InsertPendingResult(List pendingResults, CandidateResult result) { int insertIndex = FindFirstIndexAtLeast(pendingResults, result.P); pendingResults.Insert(insertIndex, result); } private static int FindFirstIndexAtLeast(List pendingResults, ulong value) { int low = 0; int high = pendingResults.Count - 1; while (low <= high) { int mid = low + ((high - low) >> 1); ulong midValue = pendingResults[mid].P; if (midValue < value) { low = mid + 1; continue; } high = mid - 1; } return low; } private readonly record struct CandidateResult(ulong P, bool IsPerfect, string Csv) { public string ToCsv() => Csv; public readonly string Csv = Csv; public readonly ulong P = P; public readonly bool IsPerfect = IsPerfect; } }