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
-