diff --git a/.vscode/launch.json b/.vscode/launch.json index d6eecc8..5e38301 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,6 +7,7 @@ "description": "Select project to run", "options": [ "EvenPerfectBitScanner", + "EvenPerfectBitScanner.ResultsParser" ], "default": "EvenPerfectBitScanner" } @@ -53,6 +54,20 @@ "internalConsoleOptions": "neverOpen", "justMyCode": false }, + { + "name": "Run EvenPerfectBitScanner.ResultsParser", + "type": "coreclr", + "request": "launch", + "noDebug": true, + "preLaunchTask": "Build EvenPerfectBitScanner.ResultsParser Release", + "program": "${workspaceFolder}/EvenPerfectBitScanner.ResultsParser/bin/Release/net8.0/EvenPerfectBitScanner.ResultsParser.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "justMyCode": false + }, { "name": "Debug EvenPerfectBitScanner", "type": "coreclr", @@ -65,6 +80,19 @@ "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", "justMyCode": false + }, + { + "name": "Debug EvenPerfectBitScanner.ResultsParser", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "Build EvenPerfectBitScanner.ResultsParser Debug", + "program": "${workspaceFolder}/EvenPerfectBitScanner.ResultsParser/bin/Debug/net8.0/EvenPerfectBitScanner.ResultsParser.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "justMyCode": false } ] -} \ No newline at end of file +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 5c54148..3af1b71 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -49,10 +49,10 @@ "group": "build", "problemMatcher": "$msCompile" }, - { - "label": "Build EvenPerfectBitScanner Release", - "command": "dotnet", - "type": "process", + { + "label": "Build EvenPerfectBitScanner Release", + "command": "dotnet", + "type": "process", "args": [ "build", "${workspaceFolder}/EvenPerfectBitScanner/EvenPerfectBitScanner.csproj", @@ -61,24 +61,69 @@ "--framework", "net8.0" ], - "group": "build", - "problemMatcher": "$msCompile" - }, - { - "label": "Run EvenPerfectBitScanner", - "type": "process", + "group": "build", + "problemMatcher": "$msCompile" + }, + { + "label": "Build EvenPerfectBitScanner.ResultsParser Debug", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/EvenPerfectBitScanner.ResultsParser/EvenPerfectBitScanner.ResultsParser.csproj", + "--configuration", + "Debug", + "--framework", + "net8.0" + ], + "group": "build", + "problemMatcher": "$msCompile" + }, + { + "label": "Build EvenPerfectBitScanner.ResultsParser Release", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/EvenPerfectBitScanner.ResultsParser/EvenPerfectBitScanner.ResultsParser.csproj", + "--configuration", + "Release", + "--framework", + "net8.0" + ], + "group": "build", + "problemMatcher": "$msCompile" + }, + { + "label": "Run EvenPerfectBitScanner", + "type": "process", "command": "dotnet", - "args": [ - "run", - "--project", - "${workspaceFolder}/EvenPerfectBitScanner/EvenPerfectBitScanner.csproj", + "args": [ + "run", + "--project", + "${workspaceFolder}/EvenPerfectBitScanner/EvenPerfectBitScanner.csproj", "--configuration", "Release", "--framework", "net8.0" ], - "problemMatcher": [] - }, + "problemMatcher": [] + }, + { + "label": "Run EvenPerfectBitScanner.ResultsParser", + "type": "process", + "command": "dotnet", + "args": [ + "run", + "--project", + "${workspaceFolder}/EvenPerfectBitScanner.ResultsParser/EvenPerfectBitScanner.ResultsParser.csproj", + "--configuration", + "Release", + "--framework", + "net8.0" + ], + "problemMatcher": [] + }, { "label": "Run Release (no debug)", "type": "process", @@ -93,4 +138,4 @@ "problemMatcher": [] } ] -} \ No newline at end of file +} diff --git a/EvenPerfectBitScanner.ResultsParser/EvenPerfectBitScanner.ResultsParser.csproj b/EvenPerfectBitScanner.ResultsParser/EvenPerfectBitScanner.ResultsParser.csproj new file mode 100644 index 0000000..3bc7595 --- /dev/null +++ b/EvenPerfectBitScanner.ResultsParser/EvenPerfectBitScanner.ResultsParser.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/EvenPerfectBitScanner.ResultsParser/Program.cs b/EvenPerfectBitScanner.ResultsParser/Program.cs new file mode 100644 index 0000000..d7b5198 --- /dev/null +++ b/EvenPerfectBitScanner.ResultsParser/Program.cs @@ -0,0 +1,358 @@ +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); + pendingResults.Add(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.Clear(); + return; + } + + RemoveEntriesBelowPrime(pendingResults, currentPrime); + continue; + } + + if (!inputCompleted) + { + return; + } + + if (!TryAdvancePrime(primeEnumerator, ref currentPrime)) + { + pendingResults.Clear(); + return; + } + + RemoveEntriesBelowPrime(pendingResults, currentPrime); + } + } + + private static bool ExtractPrimeEntries( + List pendingResults, + List primeResults, + ulong prime) + { + bool found = false; + for (int i = 0; i < pendingResults.Count; i++) + { + CandidateResult candidate = pendingResults[i]; + if (candidate.P != prime) + { + continue; + } + + primeResults.Add(candidate); + pendingResults.RemoveAt(i); + i--; + found = true; + } + + return found; + } + + private static void RemoveEntriesBelowPrime(List pendingResults, ulong currentPrime) + { + for (int i = 0; i < pendingResults.Count; i++) + { + if (pendingResults[i].P >= currentPrime) + { + continue; + } + + pendingResults.RemoveAt(i); + i--; + } + } + + private static bool TryAdvancePrime(IEnumerator primeEnumerator, ref ulong currentPrime) + { + if (!primeEnumerator.MoveNext()) + { + currentPrime = ulong.MaxValue; + return false; + } + + currentPrime = primeEnumerator.Current; + return true; + } + + private readonly record struct CandidateResult(ulong P, bool IsPerfect, string Csv) + { + public string ToCsv() + { + return Csv; + } + } +} + diff --git a/EvenPerfectScanner.sln b/EvenPerfectScanner.sln index 2321df5..75deba3 100644 --- a/EvenPerfectScanner.sln +++ b/EvenPerfectScanner.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 @@ -10,31 +10,36 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EvenPerfectBitScanner", "Ev EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EvenPerfectBitScanner.Tests", "EvenPerfectBitScanner.Tests\EvenPerfectBitScanner.Tests.csproj", "{65ED9CC9-BB94-4FA5-940E-B84B3EE87DA7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EvenPerfectBitScanner.ResultsParser", "EvenPerfectBitScanner.ResultsParser\EvenPerfectBitScanner.ResultsParser.csproj", "{B07A718C-A71C-410D-A0D7-FA1674CEB38F}" +EndProject Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {FFD557C9-12AE-4D8D-A21A-44CEE197BFB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FFD557C9-12AE-4D8D-A21A-44CEE197BFB6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FFD557C9-12AE-4D8D-A21A-44CEE197BFB6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FFD557C9-12AE-4D8D-A21A-44CEE197BFB6}.Release|Any CPU.Build.0 = Release|Any CPU - {FA4063F7-CB3C-4A66-BE70-4C6535D90197}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FA4063F7-CB3C-4A66-BE70-4C6535D90197}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FA4063F7-CB3C-4A66-BE70-4C6535D90197}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FA4063F7-CB3C-4A66-BE70-4C6535D90197}.Release|Any CPU.Build.0 = Release|Any CPU - {553D1735-214B-4097-B647-D73DB2CB9ADA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {553D1735-214B-4097-B647-D73DB2CB9ADA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {553D1735-214B-4097-B647-D73DB2CB9ADA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {553D1735-214B-4097-B647-D73DB2CB9ADA}.Release|Any CPU.Build.0 = Release|Any CPU - {65ED9CC9-BB94-4FA5-940E-B84B3EE87DA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {65ED9CC9-BB94-4FA5-940E-B84B3EE87DA7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {65ED9CC9-BB94-4FA5-940E-B84B3EE87DA7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {65ED9CC9-BB94-4FA5-940E-B84B3EE87DA7}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FFD557C9-12AE-4D8D-A21A-44CEE197BFB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FFD557C9-12AE-4D8D-A21A-44CEE197BFB6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FFD557C9-12AE-4D8D-A21A-44CEE197BFB6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FFD557C9-12AE-4D8D-A21A-44CEE197BFB6}.Release|Any CPU.Build.0 = Release|Any CPU + {FA4063F7-CB3C-4A66-BE70-4C6535D90197}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA4063F7-CB3C-4A66-BE70-4C6535D90197}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA4063F7-CB3C-4A66-BE70-4C6535D90197}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FA4063F7-CB3C-4A66-BE70-4C6535D90197}.Release|Any CPU.Build.0 = Release|Any CPU + {553D1735-214B-4097-B647-D73DB2CB9ADA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {553D1735-214B-4097-B647-D73DB2CB9ADA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {553D1735-214B-4097-B647-D73DB2CB9ADA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {553D1735-214B-4097-B647-D73DB2CB9ADA}.Release|Any CPU.Build.0 = Release|Any CPU + {65ED9CC9-BB94-4FA5-940E-B84B3EE87DA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {65ED9CC9-BB94-4FA5-940E-B84B3EE87DA7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {65ED9CC9-BB94-4FA5-940E-B84B3EE87DA7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {65ED9CC9-BB94-4FA5-940E-B84B3EE87DA7}.Release|Any CPU.Build.0 = Release|Any CPU + {B07A718C-A71C-410D-A0D7-FA1674CEB38F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B07A718C-A71C-410D-A0D7-FA1674CEB38F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B07A718C-A71C-410D-A0D7-FA1674CEB38F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B07A718C-A71C-410D-A0D7-FA1674CEB38F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection EndGlobal -