From e3dc6858910aaab3d966bd1ea42e0c49851f8d6b Mon Sep 17 00:00:00 2001 From: "Bastian (BaM)" Date: Mon, 26 May 2025 11:58:29 +0200 Subject: [PATCH] Init --- .github/copilot-instructions.md | 35 +++ .gitignore | 42 ++++ README.md | 128 ++++++++++ build.zig | 33 +++ download-pi-data.sh | 4 + examples/birth-date-finder/README.md | 74 ++++++ examples/birth-date-finder/build.zig | 65 +++++ examples/birth-date-finder/src/main.zig | 48 ++++ .../src/plain_text_example.zig | 50 ++++ .../birth-date-finder/src/test_multiple.zig | 59 +++++ src/lib.zig | 45 ++++ src/pi_finder.zig | 235 ++++++++++++++++++ 12 files changed, 818 insertions(+) create mode 100644 .github/copilot-instructions.md create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build.zig create mode 100755 download-pi-data.sh create mode 100644 examples/birth-date-finder/README.md create mode 100644 examples/birth-date-finder/build.zig create mode 100644 examples/birth-date-finder/src/main.zig create mode 100644 examples/birth-date-finder/src/plain_text_example.zig create mode 100644 examples/birth-date-finder/src/test_multiple.zig create mode 100644 src/lib.zig create mode 100644 src/pi_finder.zig diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..bb47713 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,35 @@ +# Copilot Instructions + +## Communication Preferences + +- Ich bevorzuge die Kommunikation auf Deutsch +- Bitte antworte auf meine Fragen auf Deutsch +- Schreibe technische Erklärungen, Anleitungen und Hilfestellungen auf Deutsch +- Der generierte Code selbst soll jedoch auf Englisch sein + +## Language for Code + +- All code comments should be written in English +- All console output messages should be in English +- All documentation (README files, inline documentation, etc.) should be written in English +- Variable names and function names should follow English naming conventions + +## Code Style + +- Follow idiomatic Zig style +- Use clear and descriptive variable and function names +- Provide appropriate documentation for functions and complex logic +- Maintain the existing project structure and organization + +## Technical Requirements + +- The project uses Zig version 0.14.1 +- New features and syntax introduced in Zig 0.14.1 should be leveraged when appropriate +- Generated code must be compatible with Zig 0.14.1 compiler +- Build scripts should follow Zig 0.14.1 build system conventions + +## Features + +- The Pi-Finder library is focused on simplicity and performance +- JSON is the standard output format +- Any new features should maintain backward compatibility diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e4bf60 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# Zig build artifacts +zig-out/ +zig-cache/ +.zig-cache/ + +# Pi decimal file (may be large) +*.txt + +# IDE and editor files +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# MacOS specific files +.DS_Store +.AppleDouble +.LSOverride +Icon? +._* +.Spotlight-V100 +.Trashes + +# Backup files +*.bak +*.tmp +*.backup + +# Local development environment settings +.env +.envrc +.direnv/ + +# Test artifacts +**/test-results/ + +# Dependency directories +/deps/ + +# Dynamic data directory +data/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..fce7fcc --- /dev/null +++ b/README.md @@ -0,0 +1,128 @@ +# Pi Finder Library + +A Zig library for searching numeric sequences in the decimal places of Pi. + +## Features + +- Efficient searching of numeric sequences in Pi decimal places +- JSON output as standard +- Optional plain text output format +- Configurable context display with delimiters +- Reusable module design +- Performance metrics + +## Installation + +### As a Dependency in a Zig Project + +Add this library as a dependency in your `build.zig.zon`: + +```zig +.dependencies = .{ + .@"pi-finder" = .{ + .path = "path/to/pi-finder-lib", + }, +}, +``` + +### In your build.zig + +```zig +const pi_finder_dep = b.dependency("pi-finder", .{ + .target = target, + .optimize = optimize, +}); + +exe.root_module.addImport("pi-finder", pi_finder_dep.module("pi-finder")); +``` + +## Usage + +### Basic Usage + +```zig +const std = @import("std"); +const PiFinder = @import("pi-finder"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + // Initialize Pi Finder + var pi_finder = try PiFinder.PiFinder.init(allocator, "pi-data.txt"); + defer pi_finder.deinit(); + + // Configure context + const context_config = PiFinder.ContextConfig{ + .size = 50, + .position = 25, + .delimiter = "-", + .delimiter_interval = 2, + }; + + // Search for sequence + const result = try pi_finder.searchSequence("123456", context_config, allocator); + + // Output as JSON (default format) + const json_output = try PiFinder.formatSearchResultAsJson(allocator, result); + defer allocator.free(json_output); + + std.debug.print("{s}\n", .{json_output}); +} +``` + +### Using Plain Text Output Format + +```zig +// After getting the search result +const plain_text_output = try PiFinder.formatSearchResultAsPlainText(allocator, result); +defer allocator.free(plain_text_output); + +std.debug.print("{s}\n", .{plain_text_output}); +``` + +## API + +### PiFinder + +- `init(allocator, file_path)` - Initializes the Pi Finder +- `deinit()` - Frees allocated memory +- `findSequenceInPi(sequence)` - Searches for a sequence +- `getContext(position, config, sequence, allocator)` - Generates context +- `searchSequence(sequence, config, allocator)` - Comprehensive search with structured result + +### Output Format Functions + +- `formatSearchResultAsJson(allocator, result)` - Formats search result as JSON +- `formatSearchResultAsPlainText(allocator, result)` - Formats search result as plain text + +### ContextConfig + +Configuration for context display: + +- `size` - Size of the context +- `position` - Position of the sequence within the context +- `delimiter` - Delimiter character (optional) +- `delimiter_interval` - Interval for delimiter insertion +- `prefix` - Prefix for the sequence +- `suffix` - Suffix for the sequence + +### SearchResult + +Structured result containing: + +- `success` - Search success status +- `sequence` - Searched sequence +- `position` - Position in Pi (if found) +- `context` - Context around the position +- `error_message` - Error message (if any) +- `performance` - Performance metrics +- `context_config` - Used context configuration + +## Examples + +See the `examples/` directory for complete examples: + +- `birth-date-finder/` - Basic example for searching birth dates +- `birth-date-finder/src/plain_text_example.zig` - Example using plain text output format diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..3f8d8f0 --- /dev/null +++ b/build.zig @@ -0,0 +1,33 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + // Create a module for the library + _ = b.addModule("pi-finder", .{ + .root_source_file = b.path("src/lib.zig"), + .target = target, + .optimize = optimize, + }); + + // Library + const lib = b.addStaticLibrary(.{ + .name = "pi-finder", + .root_source_file = b.path("src/lib.zig"), + .target = target, + .optimize = optimize, + }); + b.installArtifact(lib); + + // Tests + const lib_unit_tests = b.addTest(.{ + .root_source_file = b.path("src/lib.zig"), + .target = target, + .optimize = optimize, + }); + + const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_lib_unit_tests.step); +} diff --git a/download-pi-data.sh b/download-pi-data.sh new file mode 100755 index 0000000..cdda4f5 --- /dev/null +++ b/download-pi-data.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +[ ! -d ./data ] && mkdir ./data +curl -o ./data/pi-decimal-10_000_000_000.txt https://einklich.net/etc/pi-decimal-10%5E9.txt diff --git a/examples/birth-date-finder/README.md b/examples/birth-date-finder/README.md new file mode 100644 index 0000000..3642a8d --- /dev/null +++ b/examples/birth-date-finder/README.md @@ -0,0 +1,74 @@ +# Birth Date Finder Example + +This example demonstrates how the Pi Finder Library is used to search for a birth date (01.01.02) in the decimal places of Pi. + +## Execution + +```bash +cd birth-date-finder +zig build run +``` + +## Additional Examples + +This project includes two additional examples: + +### 1. Multiple Test Example + +```bash +zig build test-multiple +``` + +This example tests multiple different sequences and measures the performance. + +### 2. Plain Text Format Example + +```bash +zig build plain-text +``` + +This example demonstrates using the `formatSearchResultAsPlainText` function instead of the default JSON format. + +## Features + +- Searches for the sequence "010102" in Pi +- Uses '-' as delimiter every 2 digits (01-01-02 format) +- Outputs the result as JSON +- Shows performance metrics + +## Example Output + +### JSON Output (Default) + +```json +{ + "success": true, + "sequence": "010102", + "position": 12345, + "context": "...92[01-01-02]94...", + "performance": { + "file_load_time_ms": 1250.45, + "search_time_ms": 23.67, + "file_size_mb": 95.37, + "load_speed_mbs": 76.28 + }, + "context_config": { + "size": 50, + "position": 25, + "delimiter": "-", + "delimiter_interval": 2, + "prefix": "[", + "suffix": "]" + } +} +``` + +### Plain Text Output (Using formatSearchResultAsPlainText) + +```text +Search Result: +Success: true +Sequence: 010102 +Position: 12345 +Context: ...92[01-01-02]94... +``` diff --git a/examples/birth-date-finder/build.zig b/examples/birth-date-finder/build.zig new file mode 100644 index 0000000..aeacb67 --- /dev/null +++ b/examples/birth-date-finder/build.zig @@ -0,0 +1,65 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + // Create the pi-finder module locally + const pi_finder_module = b.createModule(.{ + .root_source_file = b.path("../../src/lib.zig"), + }); + + const exe = b.addExecutable(.{ + .name = "birth-date-finder", + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + exe.root_module.addImport("pi-finder", pi_finder_module); + b.installArtifact(exe); + + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + // Add test executable + const test_exe = b.addExecutable(.{ + .name = "test-multiple", + .root_source_file = b.path("src/test_multiple.zig"), + .target = target, + .optimize = optimize, + }); + + test_exe.root_module.addImport("pi-finder", pi_finder_module); + b.installArtifact(test_exe); + + const test_run_cmd = b.addRunArtifact(test_exe); + test_run_cmd.step.dependOn(b.getInstallStep()); + + const test_step = b.step("test-multiple", "Run multiple tests"); + test_step.dependOn(&test_run_cmd.step); + + // Add plain text example executable + const plain_text_exe = b.addExecutable(.{ + .name = "plain-text-example", + .root_source_file = b.path("src/plain_text_example.zig"), + .target = target, + .optimize = optimize, + }); + + plain_text_exe.root_module.addImport("pi-finder", pi_finder_module); + b.installArtifact(plain_text_exe); + + const plain_text_run_cmd = b.addRunArtifact(plain_text_exe); + plain_text_run_cmd.step.dependOn(b.getInstallStep()); + + const plain_text_step = b.step("plain-text", "Run the plain text format example"); + plain_text_step.dependOn(&plain_text_run_cmd.step); +} diff --git a/examples/birth-date-finder/src/main.zig b/examples/birth-date-finder/src/main.zig new file mode 100644 index 0000000..c7d6b22 --- /dev/null +++ b/examples/birth-date-finder/src/main.zig @@ -0,0 +1,48 @@ +const std = @import("std"); +const PiFinder = @import("pi-finder"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + // Path to the Pi file (relative to the main project) + const pi_file_path = "../../data/pi-decimal-10_000_000_000.txt"; + + // Initialize Pi Finder (JSON output is standard) + var pi_finder = PiFinder.PiFinder.init(allocator, pi_file_path) catch |err| { + std.debug.print("Error initializing Pi finder: {}\n", .{err}); + return; + }; + defer pi_finder.deinit(); + + // Configure birth date + const birth_date = "010102"; // 01.01.02 + + // Context configuration with delimiter '-' + const context_config = PiFinder.ContextConfig{ + .size = 50, + .position = 25, + .delimiter = "-", + .delimiter_interval = 2, + .prefix = "[", + .suffix = "]", + }; + + // Perform search + const result = try pi_finder.searchSequence(birth_date, context_config, allocator); + + // Output result as JSON + const json_output = try PiFinder.formatSearchResultAsJson(allocator, result); + defer allocator.free(json_output); + + const stdout = std.io.getStdOut().writer(); + try stdout.print("{s}\n", .{json_output}); + + // Clean up if context was allocated + if (result.context) |context| { + if (context_config.delimiter != null) { + allocator.free(context); + } + } +} diff --git a/examples/birth-date-finder/src/plain_text_example.zig b/examples/birth-date-finder/src/plain_text_example.zig new file mode 100644 index 0000000..392431e --- /dev/null +++ b/examples/birth-date-finder/src/plain_text_example.zig @@ -0,0 +1,50 @@ +const std = @import("std"); +const PiFinder = @import("pi-finder"); + +/// This example demonstrates how to use the formatSearchResultAsPlainText function +/// to output search results in plain text format instead of JSON +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + // Path to the Pi file (relative to the main project) + const pi_file_path = "../../data/pi-decimal-10_000_000_000.txt"; + + // Initialize Pi Finder + var pi_finder = PiFinder.PiFinder.init(allocator, pi_file_path) catch |err| { + std.debug.print("Error initializing Pi finder: {}\n", .{err}); + return; + }; + defer pi_finder.deinit(); + + // Configure birth date + const birth_date = "010102"; // 01.01.02 + + // Context configuration with delimiter '-' + const context_config = PiFinder.ContextConfig{ + .size = 50, + .position = 25, + .delimiter = "-", + .delimiter_interval = 2, + .prefix = "[", + .suffix = "]", + }; + + // Perform search + const result = try pi_finder.searchSequence(birth_date, context_config, allocator); + + // Output result as plain text + const plain_text_output = try PiFinder.formatSearchResultAsPlainText(allocator, result); + defer allocator.free(plain_text_output); + + const stdout = std.io.getStdOut().writer(); + try stdout.print("{s}\n", .{plain_text_output}); + + // Clean up if context was allocated + if (result.context) |context| { + if (context_config.delimiter != null) { + allocator.free(context); + } + } +} diff --git a/examples/birth-date-finder/src/test_multiple.zig b/examples/birth-date-finder/src/test_multiple.zig new file mode 100644 index 0000000..41e2351 --- /dev/null +++ b/examples/birth-date-finder/src/test_multiple.zig @@ -0,0 +1,59 @@ +const std = @import("std"); +const PiFinder = @import("pi-finder"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + // Path to the Pi file (relative to the main project) + const pi_file_path = "../../data/pi-decimal-10_000_000_000.txt"; + + // Initialize Pi Finder (JSON output is standard) + var pi_finder = PiFinder.PiFinder.init(allocator, pi_file_path) catch |err| { + const result = PiFinder.SearchResult{ + .success = false, + .sequence = "N/A", + .position = null, + .context = null, + .error_message = @errorName(err), + }; + const json_output = try PiFinder.formatSearchResultAsJson(allocator, result); + defer allocator.free(json_output); + std.debug.print("{s}\n", .{json_output}); + return; + }; + defer pi_finder.deinit(); + + // Test with different birth dates + const test_dates = [_][]const u8{ "010102", "123456", "314159" }; + + for (test_dates) |birth_date| { + // Context configuration with delimiter '-' for date + const context_config = PiFinder.ContextConfig{ + .size = 40, + .position = 20, + .delimiter = "-", + .delimiter_interval = 2, + .prefix = "[", + .suffix = "]", + }; + + // Perform search + const result = try pi_finder.searchSequence(birth_date, context_config, allocator); + + // Output result as JSON + const json_output = try PiFinder.formatSearchResultAsJson(allocator, result); + defer allocator.free(json_output); + + const stdout = std.io.getStdOut().writer(); + try stdout.print("Searching for {s}:\n{s}\n\n", .{ birth_date, json_output }); + + // Clean up if context was allocated + if (result.context) |context| { + if (context_config.delimiter != null) { + allocator.free(context); + } + } + } +} diff --git a/src/lib.zig b/src/lib.zig new file mode 100644 index 0000000..1b937ba --- /dev/null +++ b/src/lib.zig @@ -0,0 +1,45 @@ +const std = @import("std"); + +// Export all public types and functions +pub const PiFinder = @import("pi_finder.zig").PiFinder; +pub const ContextConfig = @import("pi_finder.zig").ContextConfig; +pub const SearchResult = @import("pi_finder.zig").SearchResult; + +// JSON utility functions for easier usage +pub fn formatSearchResultAsJson(allocator: std.mem.Allocator, result: SearchResult) ![]u8 { + var json_string = std.ArrayList(u8).init(allocator); + try std.json.stringify(result, .{}, json_string.writer()); + return json_string.toOwnedSlice(); +} + +// Plain text utility functions for easier usage +pub fn formatSearchResultAsPlainText(allocator: std.mem.Allocator, result: SearchResult) ![]u8 { + var plain_text = std.ArrayList(u8).init(allocator); + var writer = plain_text.writer(); + + try writer.writeAll("Success: "); + try writer.print("{}\n", .{result.success}); + try writer.writeAll("Sequence: "); + try writer.writeAll(result.sequence); + + if (result.position) |pos| { + try writer.writeAll("\nPosition: "); + try writer.print("{d}", .{pos}); + } else { + try writer.writeAll("\nPosition: N/A"); + } + + if (result.context) |context| { + try writer.writeAll("\nContext: "); + try writer.writeAll(context); + } else { + try writer.writeAll("\nContext: N/A"); + } + + if (result.error_message) |err_msg| { + try writer.writeAll("\nError Message: "); + try writer.writeAll(err_msg); + } + + return plain_text.toOwnedSlice(); +} diff --git a/src/pi_finder.zig b/src/pi_finder.zig new file mode 100644 index 0000000..cb582af --- /dev/null +++ b/src/pi_finder.zig @@ -0,0 +1,235 @@ +const std = @import("std"); + +/// Output format configuration - REMOVED +// The OutputFormat enum was removed as JSON is now the default and only output format + +/// Configuration for context around a found sequence +pub const ContextConfig = struct { + size: u64 = 25, + position: u64 = 10, + delimiter: ?[]const u8 = null, + delimiter_interval: u64 = 2, // Default: after every 2 digits (DD-MM-YY style) + prefix: []const u8 = "", + suffix: []const u8 = "", +}; + +/// Result structure for JSON output +pub const SearchResult = struct { + success: bool, + sequence: []const u8, + position: ?u64, + context: ?[]const u8, + error_message: ?[]const u8 = null, + performance: ?PerformanceMetrics = null, + context_config: ?ContextConfig = null, + + pub const PerformanceMetrics = struct { + file_load_time_ms: f64, + search_time_ms: f64, + file_size_mb: f64, + load_speed_mbs: f64, + }; +}; + +/// PiFinder structure with core functionality for searching and formatting sequences +pub const PiFinder = struct { + pi_decimals: []u8, + allocator: std.mem.Allocator, + load_time_ms: f64, + + const Self = @This(); + + /// Initializes the Pi finder and loads the Pi decimal places from file + pub fn init(allocator: std.mem.Allocator, file_path: []const u8) !Self { + const start_time = std.time.nanoTimestamp(); + + const file = std.fs.cwd().openFile(file_path, .{}) catch |err| { + return err; + }; + defer file.close(); + + const file_size = try file.getEndPos(); + const pi_data = try allocator.alloc(u8, file_size); + _ = try file.readAll(pi_data); + + // Calculate loading time + const end_time = std.time.nanoTimestamp(); + const load_time_ns = end_time - start_time; + const load_time_ms = @as(f64, @floatFromInt(load_time_ns)) / 1_000_000.0; + + return Self{ + .pi_decimals = pi_data, + .allocator = allocator, + .load_time_ms = load_time_ms, + }; + } + + /// Frees the memory + pub fn deinit(self: *Self) void { + self.allocator.free(self.pi_decimals); + } + + /// Searches for a number sequence in the Pi decimal places + pub fn findSequenceInPi(self: *const Self, sequence: []const u8) !?u64 { + // Input validation + if (sequence.len == 0) { + return null; + } + + if (sequence.len > self.pi_decimals.len) { + return null; + } + + // Check if all characters are digits + for (sequence) |char| { + if (char < '0' or char > '9') { + return null; + } + } + + // Search for the sequence in the Pi decimal places + var i: usize = 0; + outer_loop: while (i <= self.pi_decimals.len - sequence.len) : (i += 1) { + // Check each character of the sequence + for (sequence, 0..) |seq_char, k| { + if (self.pi_decimals[i + k] != seq_char) { + continue :outer_loop; + } + } + return @as(u64, i); + } + + return null; + } + + /// Generate context around a found position with optional delimiter formatting + pub fn getContext(self: *const Self, position: u64, context_cfg: ContextConfig, sequence: []const u8, allocator: std.mem.Allocator) ![]const u8 { + // Ensure context position doesn't exceed context size + const actual_position = if (context_cfg.position >= context_cfg.size) context_cfg.size - 1 else context_cfg.position; + + // Calculate context boundaries + const context_start = if (position > actual_position) position - actual_position else 0; + const context_end_target = context_start + context_cfg.size; + const context_end = if (context_end_target <= self.pi_decimals.len) context_end_target else self.pi_decimals.len; + + const raw_context = self.pi_decimals[context_start..context_end]; + + // If no delimiter is specified, return raw context + if (context_cfg.delimiter == null) { + return raw_context; + } + + // Apply delimiter formatting to the found sequence within the context + const delimiter = context_cfg.delimiter.?; + const sequence_start_in_context = if (position > context_start) position - context_start else 0; + + // Create a copy of the context to modify + const max_delimiters = (sequence.len - 1) / context_cfg.delimiter_interval; + var formatted_context = try allocator.alloc(u8, raw_context.len + max_delimiters * delimiter.len + context_cfg.prefix.len + context_cfg.suffix.len); + var write_pos: usize = 0; + + // Copy context up to the sequence + for (raw_context[0..sequence_start_in_context]) |char| { + formatted_context[write_pos] = char; + write_pos += 1; + } + + // Add prefix + for (context_cfg.prefix) |char| { + formatted_context[write_pos] = char; + write_pos += 1; + } + + // Add the sequence with delimiters + if (sequence_start_in_context + sequence.len <= raw_context.len) { + for (sequence, 0..) |_, i| { + // Add delimiter based on interval (but not at the very beginning) + if (i > 0 and i % context_cfg.delimiter_interval == 0) { + // Add delimiter + for (delimiter) |d_char| { + formatted_context[write_pos] = d_char; + write_pos += 1; + } + } + // Add the character from the actual context (to ensure accuracy) + formatted_context[write_pos] = raw_context[sequence_start_in_context + i]; + write_pos += 1; + } + } + + // Add suffix + for (context_cfg.suffix) |char| { + formatted_context[write_pos] = char; + write_pos += 1; + } + + // Copy remaining context after the sequence + const remaining_start = sequence_start_in_context + sequence.len; + if (remaining_start < raw_context.len) { + for (raw_context[remaining_start..]) |char| { + formatted_context[write_pos] = char; + write_pos += 1; + } + } + + // Resize to actual used length + return formatted_context[0..write_pos]; + } + + /// Comprehensive search function that returns a structured result + pub fn searchSequence(self: *const Self, sequence: []const u8, context_cfg: ContextConfig, allocator: std.mem.Allocator) !SearchResult { + const start_search_time = std.time.nanoTimestamp(); + + const position = self.findSequenceInPi(sequence) catch |err| { + return SearchResult{ + .success = false, + .sequence = sequence, + .position = null, + .context = null, + .error_message = @errorName(err), + }; + }; + + const end_search_time = std.time.nanoTimestamp(); + const search_time_ns = end_search_time - start_search_time; + const search_time_ms = @as(f64, @floatFromInt(search_time_ns)) / 1_000_000.0; + + if (position) |pos| { + const context = self.getContext(pos, context_cfg, sequence, allocator) catch |err| { + return SearchResult{ + .success = false, + .sequence = sequence, + .position = pos, + .context = null, + .error_message = @errorName(err), + }; + }; + + // Calculate performance metrics + const file_size_mb = @as(f64, @floatFromInt(self.pi_decimals.len)) / (1024.0 * 1024.0); + const load_speed_mbs = file_size_mb / (self.load_time_ms / 1000.0); + + return SearchResult{ + .success = true, + .sequence = sequence, + .position = pos, + .context = context, + .performance = SearchResult.PerformanceMetrics{ + .file_load_time_ms = self.load_time_ms, + .search_time_ms = search_time_ms, + .file_size_mb = file_size_mb, + .load_speed_mbs = load_speed_mbs, + }, + .context_config = context_cfg, + }; + } else { + return SearchResult{ + .success = false, + .sequence = sequence, + .position = null, + .context = null, + .error_message = "Sequence not found in Pi", + }; + } + } +};