From 15393a32204b648d8f46b2ffeb5e8b05f5b023ea Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Mon, 11 Aug 2025 21:43:08 +0200 Subject: [PATCH 1/2] The base commit --- .editorconfig | 21 +-- .github/workflows/docs.yml | 2 +- .github/workflows/lints.yml | 2 +- .github/workflows/tests.yml | 2 +- .pre-commit-config.yaml | 6 +- CODE_OF_CONDUCT.md | 2 +- CONTRIBUTING.md | 32 ++-- Makefile | 24 ++- README.md | 58 +++--- benches/.gitkeep | 0 benches/README.md | 87 +++++++++ benches/b1_btree_map.zig | 103 +++++++++++ benches/b2_sorted_set.zig | 102 +++++++++++ benches/b3_red_black_tree.zig | 138 +++++++++++++++ benches/b4_skip_list.zig | 103 +++++++++++ benches/b5_trie.zig | 167 ++++++++++++++++++ benches/b6_cartesian_tree.zig | 130 ++++++++++++++ build.zig | 119 +++++++------ build.zig.zon | 10 +- examples/README.md | 13 +- examples/{btree_map.zig => e1_btree_map.zig} | 0 .../{sorted_set.zig => e2_sorted_set.zig} | 0 ...d_black_tree.zig => e3_red_black_tree.zig} | 0 examples/{skip_list.zig => e4_skip_list.zig} | 0 examples/{trie.zig => e5_trie.zig} | 2 +- ...rtesian_tree.zig => e6_cartesian_tree.zig} | 0 logo.svg | 108 +++++------ pyproject.toml | 7 +- src/ordered/cartesian_tree.zig | 8 +- src/ordered/red_black_tree.zig | 12 +- src/ordered/sorted_set.zig | 6 +- src/ordered/trie.zig | 20 +-- test_arraylist.zig | 14 ++ 33 files changed, 1089 insertions(+), 209 deletions(-) delete mode 100644 benches/.gitkeep create mode 100644 benches/README.md create mode 100644 benches/b1_btree_map.zig create mode 100644 benches/b2_sorted_set.zig create mode 100644 benches/b3_red_black_tree.zig create mode 100644 benches/b4_skip_list.zig create mode 100644 benches/b5_trie.zig create mode 100644 benches/b6_cartesian_tree.zig rename examples/{btree_map.zig => e1_btree_map.zig} (100%) rename examples/{sorted_set.zig => e2_sorted_set.zig} (100%) rename examples/{red_black_tree.zig => e3_red_black_tree.zig} (100%) rename examples/{skip_list.zig => e4_skip_list.zig} (100%) rename examples/{trie.zig => e5_trie.zig} (97%) rename examples/{cartesian_tree.zig => e6_cartesian_tree.zig} (100%) create mode 100644 test_arraylist.zig diff --git a/.editorconfig b/.editorconfig index 9ee9a38..cf71165 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,32 +1,25 @@ -# Top-most EditorConfig file root = true -# Global settings (applicable to all files unless overridden) [*] -charset = utf-8 # Default character encoding -end_of_line = lf # Use LF for line endings (Unix-style) -indent_style = space # Use spaces for indentation -indent_size = 4 # Default indentation size -insert_final_newline = true # Make sure files end with a newline -trim_trailing_whitespace = true # Remove trailing whitespace +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true -# Zig files [*.zig] max_line_length = 100 -# Markdown files [*.md] max_line_length = 120 -trim_trailing_whitespace = false # Don't remove trailing whitespace in Markdown files +trim_trailing_whitespace = false -# Bash scripts [*.sh] indent_size = 2 -# YAML files [*.{yml,yaml}] indent_size = 2 -# Python files [*.py] max_line_length = 100 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index bd15684..7edda17 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -20,7 +20,7 @@ jobs: - name: Install Zig uses: goto-bus-stop/setup-zig@v2 with: - version: '0.14.1' + version: '0.15.1' - name: Install System Dependencies run: | diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index d08a97d..13bd36f 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -27,7 +27,7 @@ jobs: - name: Install Zig uses: goto-bus-stop/setup-zig@v2 with: - version: '0.14.1' + version: '0.15.1' - name: Install Dependencies run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3b5d45b..8c3989d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,7 +27,7 @@ jobs: - name: Install Zig uses: goto-bus-stop/setup-zig@v2 with: - version: '0.14.1' + version: '0.15.1' - name: Install Dependencies run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 177c140..98ec238 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,21 +22,21 @@ repos: - repo: local hooks: - id: format - name: Format the code + name: Format Code entry: make format language: system pass_filenames: false stages: [ pre-commit ] - id: lint - name: Check code style + name: Check Code Style entry: make lint language: system pass_filenames: false stages: [ pre-commit ] - id: test - name: Run the tests + name: Run Test entry: make test language: system pass_filenames: false diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 8e67f5c..54d08d1 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,3 +1,3 @@ -# Code of Conduct +## Code of Conduct We adhere to the [Contributor Covenant](https://www.contributor-covenant.org/version/2/1/code_of_conduct/) version 2.1. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aa6be24..afe7029 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,24 +1,24 @@ -# Contribution Guidelines +## Contribution Guidelines Thank you for considering contributing to this project! Contributions are always welcome and appreciated. -## How to Contribute +### How to Contribute -Please check the [issue tracker](https://github.com/habedi/ordered/issues) to see if there is an issue you +Please check the [issue tracker](https://github.com/CogitatorTech/ordered/issues) to see if there is an issue you would like to work on or if it has already been resolved. -### Reporting Bugs +#### Reporting Bugs -1. Open an issue on the [issue tracker](https://github.com/habedi/ordered/issues). +1. Open an issue on the [issue tracker](https://github.com/CogitatorTech/ordered/issues). 2. Include information such as steps to reproduce the observed behavior and relevant logs or screenshots. -### Suggesting Features +#### Suggesting Features -1. Open an issue on the [issue tracker](https://github.com/habedi/ordered/issues). +1. Open an issue on the [issue tracker](https://github.com/CogitatorTech/ordered/issues). 2. Provide details about the feature, its purpose, and potential implementation ideas. -## Submitting Pull Requests +### Submitting Pull Requests - Ensure all tests pass before submitting a pull request. - Write a clear description of the changes you made and the reasons behind them. @@ -26,9 +26,9 @@ would like to work on or if it has already been resolved. > [!IMPORTANT] > It's assumed that by submitting a pull request, you agree to license your contributions under the project's license. -## Development Workflow +### Development Workflow -### Prerequisites +#### Prerequisites Install GNU Make on your system if it's not already installed. @@ -39,22 +39,22 @@ sudo apt-get install make - Use the `make install-deps` command to install the development dependencies. -### Code Style +#### Code Style - Use the `make format` command to format the code. -### Running Tests +#### Running Tests - Use the `make test` command to run the tests. -### Running Linters +#### Running Linters - Use the `make lint` command to run the linters. -### See Available Commands +#### See Available Commands - Run `make help` to see all available commands for managing different tasks. -## Code of Conduct +### Code of Conduct -We adhere to the [Contributor Covenant](https://www.contributor-covenant.org/version/2/1/code_of_conduct/) version 2.1. +We adhere to the project's [Code of Conduct](CODE_OF_CONDUCT.md). diff --git a/Makefile b/Makefile index aa5b130..8b4f788 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,13 @@ # ################################################################################ # # Configuration and Variables # ################################################################################ -ZIG ?= $(shell which zig || echo ~/.local/share/zig/0.14.1/zig) +ZIG ?= $(shell which zig || echo ~/.local/share/zig/0.15.1/zig) BUILD_TYPE ?= Debug BUILD_OPTS = -Doptimize=$(BUILD_TYPE) JOBS ?= $(shell nproc || echo 2) SRC_DIR := src EXAMPLES_DIR := examples +BENCHMARKS_DIR:= benches BUILD_DIR := zig-out CACHE_DIR := .zig-cache BINARY_NAME := example @@ -18,6 +19,10 @@ JUNK_FILES := *.o *.obj *.dSYM *.dll *.so *.dylib *.a *.lib *.pdb temp/ EXAMPLES := $(patsubst %.zig,%,$(notdir $(wildcard examples/*.zig))) EXAMPLE ?= all +# Automatically find all benchmark names +BENCHMARKS := $(patsubst %.zig,%,$(notdir $(wildcard benches/*.zig))) +BENCHMARK ?= all + SHELL := /usr/bin/env bash .SHELLFLAGS := -eu -o pipefail -c @@ -25,7 +30,7 @@ SHELL := /usr/bin/env bash # Targets ################################################################################ -.PHONY: all help build rebuild run test release clean lint format docs serve-docs install-deps setup-hooks test-hooks +.PHONY: all help build rebuild run bench test release clean lint format docs serve-docs install-deps setup-hooks test-hooks .DEFAULT_GOAL := help help: ## Show the help messages for all targets @@ -43,7 +48,7 @@ build: ## Build project (e.g. 'make build BUILD_TYPE=ReleaseSmall' or 'make buil rebuild: clean build ## clean and build -run: ## Run an example (e.g. 'make run EXAMPLE=sorted_set' or 'make run' to run all examples) +run: ## Run an example (like 'make run EXAMPLE=e1_btree_map' or 'make run' to run all examples) @if [ "$(EXAMPLE)" = "all" ]; then \ echo "--> Running all examples..."; \ for ex in $(EXAMPLES); do \ @@ -56,6 +61,19 @@ run: ## Run an example (e.g. 'make run EXAMPLE=sorted_set' or 'make run' to run $(ZIG) build run-$(EXAMPLE) $(BUILD_OPTS); \ fi +bench: ## Run a benchmark (like 'make bench BENCHMARK=b1_btree_map' or 'make run' to run all benchmarks) + @if [ "$(BENCHMARK)" = "all" ]; then \ + echo "--> Running all benchmarks..."; \ + for ex in $(BENCHMARKS); do \ + echo ""; \ + echo "--> Running '$$ex'"; \ + $(ZIG) build bench-$$ex $(BUILD_OPTS); \ + done; \ + else \ + echo "--> Running benchmark: $(BENCHMARK)"; \ + $(ZIG) build bench-$(BENCHMARK) $(BUILD_OPTS); \ + fi + test: ## Run tests @echo "Running tests..." @$(ZIG) build test $(BUILD_OPTS) -j$(JOBS) $(TEST_FLAGS) diff --git a/README.md b/README.md index 7070e5c..11235b0 100644 --- a/README.md +++ b/README.md @@ -6,34 +6,50 @@

Ordered

-[![Tests](https://img.shields.io/github/actions/workflow/status/habedi/ordered/tests.yml?label=tests&style=flat&labelColor=282c34&logo=github)](https://github.com/habedi/ordered/actions/workflows/tests.yml) -[![CodeFactor](https://img.shields.io/codefactor/grade/github/habedi/ordered?label=code%20quality&style=flat&labelColor=282c34&logo=codefactor)](https://www.codefactor.io/repository/github/habedi/ordered) -[![Zig Version](https://img.shields.io/badge/Zig-0.14.1-orange?logo=zig&labelColor=282c34)](https://ziglang.org/download/) -[![Docs](https://img.shields.io/github/v/tag/habedi/ordered?label=docs&color=blue&style=flat&labelColor=282c34&logo=read-the-docs)](https://habedi.github.io/ordered/) -[![Release](https://img.shields.io/github/release/habedi/ordered.svg?label=release&style=flat&labelColor=282c34&logo=github)](https://github.com/habedi/ordered/releases/latest) -[![License](https://img.shields.io/badge/license-MIT-007ec6?label=license&style=flat&labelColor=282c34&logo=open-source-initiative)](https://github.com/habedi/ordered/blob/main/LICENSE) +[![Tests](https://img.shields.io/github/actions/workflow/status/CogitatorTech/ordered/tests.yml?label=tests&style=flat&labelColor=282c34&logo=github)](https://github.com/CogitatorTech/ordered/actions/workflows/tests.yml) +[![CodeFactor](https://img.shields.io/codefactor/grade/github/CogitatorTech/ordered?label=code%20quality&style=flat&labelColor=282c34&logo=codefactor)](https://www.codefactor.io/repository/github/CogitatorTech/ordered) +[![Zig Version](https://img.shields.io/badge/Zig-0.15.1-orange?logo=zig&labelColor=282c34)](https://ziglang.org/download/) +[![Docs](https://img.shields.io/badge/docs-view-blue?style=flat&labelColor=282c34&logo=read-the-docs)](https://CogitatorTech.github.io/ordered/) +[![Examples](https://img.shields.io/badge/examples-view-green?style=flat&labelColor=282c34&logo=zig)](https://github.com/CogitatorTech/ordered/tree/main/examples) +[![Release](https://img.shields.io/github/release/CogitatorTech/ordered.svg?label=release&style=flat&labelColor=282c34&logo=github)](https://github.com/CogitatorTech/ordered/releases/latest) +[![License](https://img.shields.io/badge/license-MIT-007ec6?label=license&style=flat&labelColor=282c34&logo=open-source-initiative)](https://github.com/CogitatorTech/ordered/blob/main/LICENSE) -A Zig library of common data structures that keep data in order +A collection of data structures that keep data in order --- -Ordered Zig library includes implementations of popular data structures including B-tree, skip list, trie, and -red-black tree. +Ordered library includes fast and efficient implementations of popular data structures including +B-tree, skip list, trie, and red-black tree for Zig programming language. -### Features +### Supported Data Structures -- Implementations for common data structures that maintain the order of keys: - - [`BTreeMap`](src/btree_map.zig): A balanced tree map that maintains order of keys. - - [`OrderedSet`](src/sorted_set.zig): A set with ordered elements. - - [`SkipList`](src/skip_list.zig): A probabilistic data structure that allows fast search, insertion, and deletion. - - [`Trie`](src/trie.zig): A prefix tree for fast retrieval of keys with common prefixes. - - [`RedBlackTree`](src/red_black_tree.zig): A self-balancing binary search tree that maintains order of keys. +Currently supported data structures include: + +- [B-tree](src/ordered/btree_map.zig): a balanced n-array tree that maintains the order of keys. +- [Sorted set](src/ordered/sorted_set.zig): a set with ordered elements based on keys. +- [Skip list](src/ordered/skip_list.zig): a probabilistic data structure that maintains sorted order using multiple linked lists. +- [Trie](src/ordered/trie.zig): a prefix tree that supports efficient retrieval of keys with common prefixes. +- [Red-black tree](src/ordered/red_black_tree.zig): A self-balancing binary search tree that maintains the order of keys +- [Cartesian tree](src/ordered/cartesian_tree.zig): A binary tree that maintains both a binary search tree property on keys and a heap property on priorities. + +| # | Data Structure | Build Complexity | Memory Complexity | Search Complexity | +|---|----------------|------------------|-------------------|----------------------| +| 1 | B-tree | $O(\log n)$ | $O(n)$ | $O(\log n)$ | +| 2 | Cartesian tree | $O(\log n)$\* | $O(n)$ | $O(\log n)$\* | +| 3 | Red-black tree | $O(\log n)$ | $O(n)$ | $O(\log n)$ | +| 4 | Skip list | $O(\log n)$\* | $O(n)$ | $O(\log n)$\* | +| 5 | Sorted set | $O(n)$ | $O(n)$ | $O(\log n)$ | +| 6 | Trie | $O(m)$ | $O(n \cdot m)$ | $O(m)$ | + +- $n$: number of stored elements +- $m$: maximum length of a key +- \*: average case complexity > [!IMPORTANT] -> Zig-DbC is in early development, so bugs and breaking API changes are expected. -> Please use the [issues page](https://github.com/habedi/zig-dbc/issues) to report bugs or request features. +> Ordered is in early development, so bugs and breaking API changes are expected. +> Please use the [issues page](https://github.com/CogitatorTech/ordered/issues) to report bugs or request features. --- @@ -45,7 +61,7 @@ To be added. ### Documentation -You can find the API documentation for the latest release of Ordered [here](https://habedi.github.io/ordered/). +You can find the API documentation for the latest release of Ordered [here](https://CogitatorTech.github.io/ordered/). Alternatively, you can use the `make docs` command to generate the documentation for the current version of Ordered. This will generate HTML documentation in the `docs/api` directory, which you can serve locally with `make serve-docs` @@ -53,7 +69,7 @@ and view in a web browser. ### Examples -Check out the [examples](examples/) directory for example usages of Ordered. +Check out the [examples](examples) directory for example usages of Ordered. --- @@ -67,4 +83,4 @@ Ordered is licensed under the MIT License (see [LICENSE](LICENSE)). ### Acknowledgements -* The logo is from [SVG Repo](https://www.svgrepo.com/svg/469537/zig-zag-left-right-arrow). +* The logo is from [SVG Repo](https://www.svgrepo.com/svg/469537/zig-zag-left-right-arrow) with some modifications. diff --git a/benches/.gitkeep b/benches/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/benches/README.md b/benches/README.md new file mode 100644 index 0000000..f0f5b8f --- /dev/null +++ b/benches/README.md @@ -0,0 +1,87 @@ +# Benchmarks + +This directory contains performance benchmarks for all data structures in the `ordered` library. + +## Running Benchmarks + +Each benchmark can be run using the following command pattern: + +```bash +zig build bench- +``` + +### Available Benchmarks + +- **BTreeMap**: `zig build bench-btree_map_bench` +- **SortedSet**: `zig build bench-sorted_set_bench` +- **RedBlackTree**: `zig build bench-red_black_tree_bench` +- **SkipList**: `zig build bench-skip_list_bench` +- **Trie**: `zig build bench-trie_bench` +- **CartesianTree**: `zig build bench-cartesian_tree_bench` + +## What Each Benchmark Tests + +### BTreeMap Benchmark +- **Insert**: Sequential insertion of integers +- **Lookup**: Finding all inserted keys +- **Delete**: Removing all keys + +### SortedSet Benchmark +- **Add**: Adding elements while maintaining sorted order +- **Contains**: Checking if elements exist +- **Remove**: Removing elements from the set + +### RedBlackTree Benchmark +- **Insert**: Inserting nodes with self-balancing +- **Find**: Searching for nodes +- **Remove**: Deleting nodes while maintaining balance +- **Iterator**: In-order traversal performance + +### SkipList Benchmark +- **Put**: Inserting key-value pairs with probabilistic levels +- **Get**: Retrieving values by key +- **Delete**: Removing key-value pairs + +### Trie Benchmark +- **Put**: Inserting strings with associated values +- **Get**: Retrieving values by string key +- **Contains**: Checking if strings exist +- **Prefix Search**: Finding all keys with a common prefix + +### CartesianTree Benchmark +- **Put**: Inserting key-value pairs with random priorities +- **Get**: Retrieving values by key +- **Remove**: Deleting nodes +- **Iterator**: In-order traversal performance + +## Benchmark Sizes + +Each benchmark tests with multiple dataset sizes: +- Small: 1,000 items +- Medium: 10,000 items +- Large: 50,000 - 100,000 items (varies by data structure) + +## Build Configuration + +Benchmarks are compiled with `ReleaseFast` optimization mode for accurate performance measurements. + +## Example Output + +``` +=== BTreeMap Benchmark === + +Insert 1000 items: 0.42 ms (420 ns/op) +Lookup 1000 items: 0.18 ms (180 ns/op, found: 1000) +Delete 1000 items: 0.35 ms (350 ns/op) + +Insert 10000 items: 5.23 ms (523 ns/op) +Lookup 10000 items: 2.10 ms (210 ns/op, found: 10000) +Delete 10000 items: 4.15 ms (415 ns/op) +``` + +## Notes + +- All benchmarks use a simple integer or string key type for consistency +- Times are reported in both total milliseconds and nanoseconds per operation +- Memory allocations use `GeneralPurposeAllocator` for production-like behavior + diff --git a/benches/b1_btree_map.zig b/benches/b1_btree_map.zig new file mode 100644 index 0000000..3177505 --- /dev/null +++ b/benches/b1_btree_map.zig @@ -0,0 +1,103 @@ +const std = @import("std"); +const ordered = @import("ordered"); +const Timer = std.time.Timer; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + std.debug.print("=== BTreeMap Benchmark ===\n\n", .{}); + + const sizes = [_]usize{ 1000, 10_000, 100_000 }; + + inline for (sizes) |size| { + try benchmarkInsert(allocator, size); + try benchmarkLookup(allocator, size); + try benchmarkDelete(allocator, size); + std.debug.print("\n", .{}); + } +} + +fn i32Compare(lhs: i32, rhs: i32) std.math.Order { + return std.math.order(lhs, rhs); +} + +fn benchmarkInsert(allocator: std.mem.Allocator, size: usize) !void { + var map = ordered.BTreeMap(i32, i32, i32Compare, 16).init(allocator); + defer map.deinit(); + + var timer = try Timer.start(); + const start = timer.lap(); + + var i: i32 = 0; + while (i < size) : (i += 1) { + try map.put(i, i * 2); + } + + const elapsed = timer.read() - start; + const ns_per_op = elapsed / size; + + std.debug.print("Insert {} items: {d:.2} ms ({d} ns/op)\n", .{ + size, + @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, + ns_per_op, + }); +} + +fn benchmarkLookup(allocator: std.mem.Allocator, size: usize) !void { + var map = ordered.BTreeMap(i32, i32, i32Compare, 16).init(allocator); + defer map.deinit(); + + var i: i32 = 0; + while (i < size) : (i += 1) { + try map.put(i, i * 2); + } + + var timer = try Timer.start(); + const start = timer.lap(); + + i = 0; + var found: usize = 0; + while (i < size) : (i += 1) { + if (map.get(i) != null) found += 1; + } + + const elapsed = timer.read() - start; + const ns_per_op = elapsed / size; + + std.debug.print("Lookup {} items: {d:.2} ms ({d} ns/op, found: {})\n", .{ + size, + @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, + ns_per_op, + found, + }); +} + +fn benchmarkDelete(allocator: std.mem.Allocator, size: usize) !void { + var map = ordered.BTreeMap(i32, i32, i32Compare, 16).init(allocator); + defer map.deinit(); + + var i: i32 = 0; + while (i < size) : (i += 1) { + try map.put(i, i * 2); + } + + var timer = try Timer.start(); + const start = timer.lap(); + + i = 0; + while (i < size) : (i += 1) { + _ = map.remove(i); + } + + const elapsed = timer.read() - start; + const ns_per_op = elapsed / size; + + std.debug.print("Delete {} items: {d:.2} ms ({d} ns/op)\n", .{ + size, + @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, + ns_per_op, + }); +} + diff --git a/benches/b2_sorted_set.zig b/benches/b2_sorted_set.zig new file mode 100644 index 0000000..6905a18 --- /dev/null +++ b/benches/b2_sorted_set.zig @@ -0,0 +1,102 @@ +const std = @import("std"); +const ordered = @import("ordered"); +const Timer = std.time.Timer; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + std.debug.print("=== SortedSet Benchmark ===\n\n", .{}); + + const sizes = [_]usize{ 1000, 10_000, 50_000 }; + + inline for (sizes) |size| { + try benchmarkAdd(allocator, size); + try benchmarkContains(allocator, size); + try benchmarkRemove(allocator, size); + std.debug.print("\n", .{}); + } +} + +fn i32Compare(lhs: i32, rhs: i32) std.math.Order { + return std.math.order(lhs, rhs); +} + +fn benchmarkAdd(allocator: std.mem.Allocator, size: usize) !void { + var set = ordered.SortedSet(i32, i32Compare).init(allocator); + defer set.deinit(); + + var timer = try Timer.start(); + const start = timer.lap(); + + var i: i32 = 0; + while (i < size) : (i += 1) { + try set.add(i); + } + + const elapsed = timer.read() - start; + const ns_per_op = elapsed / size; + + std.debug.print("Add {} items: {d:.2} ms ({d} ns/op)\n", .{ + size, + @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, + ns_per_op, + }); +} + +fn benchmarkContains(allocator: std.mem.Allocator, size: usize) !void { + var set = ordered.SortedSet(i32, i32Compare).init(allocator); + defer set.deinit(); + + var i: i32 = 0; + while (i < size) : (i += 1) { + try set.add(i); + } + + var timer = try Timer.start(); + const start = timer.lap(); + + i = 0; + var found: usize = 0; + while (i < size) : (i += 1) { + if (set.contains(i)) found += 1; + } + + const elapsed = timer.read() - start; + const ns_per_op = elapsed / size; + + std.debug.print("Contains {} items: {d:.2} ms ({d} ns/op, found: {})\n", .{ + size, + @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, + ns_per_op, + found, + }); +} + +fn benchmarkRemove(allocator: std.mem.Allocator, size: usize) !void { + var set = ordered.SortedSet(i32, i32Compare).init(allocator); + defer set.deinit(); + + var i: i32 = 0; + while (i < size) : (i += 1) { + try set.add(i); + } + + var timer = try Timer.start(); + const start = timer.lap(); + + while (set.items.items.len > 0) { + _ = set.remove(0); + } + + const elapsed = timer.read() - start; + const ns_per_op = elapsed / size; + + std.debug.print("Remove {} items: {d:.2} ms ({d} ns/op)\n", .{ + size, + @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, + ns_per_op, + }); +} + diff --git a/benches/b3_red_black_tree.zig b/benches/b3_red_black_tree.zig new file mode 100644 index 0000000..26ffde3 --- /dev/null +++ b/benches/b3_red_black_tree.zig @@ -0,0 +1,138 @@ +const std = @import("std"); +const ordered = @import("ordered"); +const Timer = std.time.Timer; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + std.debug.print("=== RedBlackTree Benchmark ===\n\n", .{}); + + const sizes = [_]usize{ 1000, 10_000, 100_000 }; + + inline for (sizes) |size| { + try benchmarkInsert(allocator, size); + try benchmarkFind(allocator, size); + try benchmarkRemove(allocator, size); + try benchmarkIterator(allocator, size); + std.debug.print("\n", .{}); + } +} + +const Context = struct { + pub fn lessThan(self: @This(), a: i32, b: i32) bool { + _ = self; + return a < b; + } +}; + +fn benchmarkInsert(allocator: std.mem.Allocator, size: usize) !void { + var tree = ordered.RedBlackTree(i32, Context).init(allocator, Context{}); + defer tree.deinit(); + + var timer = try Timer.start(); + const start = timer.lap(); + + var i: i32 = 0; + while (i < size) : (i += 1) { + try tree.insert(i); + } + + const elapsed = timer.read() - start; + const ns_per_op = elapsed / size; + + std.debug.print("Insert {} items: {d:.2} ms ({d} ns/op)\n", .{ + size, + @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, + ns_per_op, + }); +} + +fn benchmarkFind(allocator: std.mem.Allocator, size: usize) !void { + var tree = ordered.RedBlackTree(i32, Context).init(allocator, Context{}); + defer tree.deinit(); + + var i: i32 = 0; + while (i < size) : (i += 1) { + try tree.insert(i); + } + + var timer = try Timer.start(); + const start = timer.lap(); + + i = 0; + var found: usize = 0; + while (i < size) : (i += 1) { + if (tree.find(i) != null) found += 1; + } + + const elapsed = timer.read() - start; + const ns_per_op = elapsed / size; + + std.debug.print("Find {} items: {d:.2} ms ({d} ns/op, found: {})\n", .{ + size, + @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, + ns_per_op, + found, + }); +} + +fn benchmarkRemove(allocator: std.mem.Allocator, size: usize) !void { + var tree = ordered.RedBlackTree(i32, Context).init(allocator, Context{}); + defer tree.deinit(); + + var i: i32 = 0; + while (i < size) : (i += 1) { + try tree.insert(i); + } + + var timer = try Timer.start(); + const start = timer.lap(); + + i = 0; + while (i < size) : (i += 1) { + _ = tree.remove(i); + } + + const elapsed = timer.read() - start; + const ns_per_op = elapsed / size; + + std.debug.print("Remove {} items: {d:.2} ms ({d} ns/op)\n", .{ + size, + @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, + ns_per_op, + }); +} + +fn benchmarkIterator(allocator: std.mem.Allocator, size: usize) !void { + var tree = ordered.RedBlackTree(i32, Context).init(allocator, Context{}); + defer tree.deinit(); + + var i: i32 = 0; + while (i < size) : (i += 1) { + try tree.insert(i); + } + + var timer = try Timer.start(); + const start = timer.lap(); + + var iter = try tree.iterator(); + defer iter.deinit(); + + var count: usize = 0; + while (try iter.next()) |_| { + count += 1; + } + + const elapsed = timer.read() - start; + const ns_per_op = elapsed / size; + + std.debug.print("Iterator {} items: {d:.2} ms ({d} ns/op, count: {})\n", .{ + size, + @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, + ns_per_op, + count, + }); +} + diff --git a/benches/b4_skip_list.zig b/benches/b4_skip_list.zig new file mode 100644 index 0000000..8cb53d7 --- /dev/null +++ b/benches/b4_skip_list.zig @@ -0,0 +1,103 @@ +const std = @import("std"); +const ordered = @import("ordered"); +const Timer = std.time.Timer; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + std.debug.print("=== SkipList Benchmark ===\n\n", .{}); + + const sizes = [_]usize{ 1000, 10_000, 100_000 }; + + inline for (sizes) |size| { + try benchmarkPut(allocator, size); + try benchmarkGet(allocator, size); + try benchmarkDelete(allocator, size); + std.debug.print("\n", .{}); + } +} + +fn i32Compare(lhs: i32, rhs: i32) std.math.Order { + return std.math.order(lhs, rhs); +} + +fn benchmarkPut(allocator: std.mem.Allocator, size: usize) !void { + var list = try ordered.SkipList(i32, i32, i32Compare, 16).init(allocator); + defer list.deinit(); + + var timer = try Timer.start(); + const start = timer.lap(); + + var i: i32 = 0; + while (i < size) : (i += 1) { + try list.put(i, i * 2); + } + + const elapsed = timer.read() - start; + const ns_per_op = elapsed / size; + + std.debug.print("Put {} items: {d:.2} ms ({d} ns/op)\n", .{ + size, + @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, + ns_per_op, + }); +} + +fn benchmarkGet(allocator: std.mem.Allocator, size: usize) !void { + var list = try ordered.SkipList(i32, i32, i32Compare, 16).init(allocator); + defer list.deinit(); + + var i: i32 = 0; + while (i < size) : (i += 1) { + try list.put(i, i * 2); + } + + var timer = try Timer.start(); + const start = timer.lap(); + + i = 0; + var found: usize = 0; + while (i < size) : (i += 1) { + if (list.get(i) != null) found += 1; + } + + const elapsed = timer.read() - start; + const ns_per_op = elapsed / size; + + std.debug.print("Get {} items: {d:.2} ms ({d} ns/op, found: {})\n", .{ + size, + @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, + ns_per_op, + found, + }); +} + +fn benchmarkDelete(allocator: std.mem.Allocator, size: usize) !void { + var list = try ordered.SkipList(i32, i32, i32Compare, 16).init(allocator); + defer list.deinit(); + + var i: i32 = 0; + while (i < size) : (i += 1) { + try list.put(i, i * 2); + } + + var timer = try Timer.start(); + const start = timer.lap(); + + i = 0; + while (i < size) : (i += 1) { + _ = list.delete(i); + } + + const elapsed = timer.read() - start; + const ns_per_op = elapsed / size; + + std.debug.print("Delete {} items: {d:.2} ms ({d} ns/op)\n", .{ + size, + @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, + ns_per_op, + }); +} + diff --git a/benches/b5_trie.zig b/benches/b5_trie.zig new file mode 100644 index 0000000..e8fbb34 --- /dev/null +++ b/benches/b5_trie.zig @@ -0,0 +1,167 @@ +const std = @import("std"); +const ordered = @import("ordered"); +const Timer = std.time.Timer; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + std.debug.print("=== Trie Benchmark ===\n\n", .{}); + + const sizes = [_]usize{ 1000, 10_000, 50_000 }; + + inline for (sizes) |size| { + try benchmarkPut(allocator, size); + try benchmarkGet(allocator, size); + try benchmarkContains(allocator, size); + try benchmarkPrefixSearch(allocator, size); + std.debug.print("\n", .{}); + } +} + +fn generateKey(allocator: std.mem.Allocator, i: usize) ![]u8 { + return std.fmt.allocPrint(allocator, "key_{d:0>8}", .{i}); +} + +fn benchmarkPut(allocator: std.mem.Allocator, size: usize) !void { + var trie = try ordered.Trie(i32).init(allocator); + defer trie.deinit(); + + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + const arena_alloc = arena.allocator(); + + var timer = try Timer.start(); + const start = timer.lap(); + + var i: usize = 0; + while (i < size) : (i += 1) { + const key = try generateKey(arena_alloc, i); + try trie.put(key, @intCast(i)); + } + + const elapsed = timer.read() - start; + const ns_per_op = elapsed / size; + + std.debug.print("Put {} items: {d:.2} ms ({d} ns/op)\n", .{ + size, + @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, + ns_per_op, + }); +} + +fn benchmarkGet(allocator: std.mem.Allocator, size: usize) !void { + var trie = try ordered.Trie(i32).init(allocator); + defer trie.deinit(); + + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + const arena_alloc = arena.allocator(); + + var i: usize = 0; + while (i < size) : (i += 1) { + const key = try generateKey(arena_alloc, i); + try trie.put(key, @intCast(i)); + } + + var timer = try Timer.start(); + const start = timer.lap(); + + i = 0; + var found: usize = 0; + while (i < size) : (i += 1) { + const key = try generateKey(arena_alloc, i); + if (trie.get(key) != null) found += 1; + } + + const elapsed = timer.read() - start; + const ns_per_op = elapsed / size; + + std.debug.print("Get {} items: {d:.2} ms ({d} ns/op, found: {})\n", .{ + size, + @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, + ns_per_op, + found, + }); +} + +fn benchmarkContains(allocator: std.mem.Allocator, size: usize) !void { + var trie = try ordered.Trie(i32).init(allocator); + defer trie.deinit(); + + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + const arena_alloc = arena.allocator(); + + var i: usize = 0; + while (i < size) : (i += 1) { + const key = try generateKey(arena_alloc, i); + try trie.put(key, @intCast(i)); + } + + var timer = try Timer.start(); + const start = timer.lap(); + + i = 0; + var found: usize = 0; + while (i < size) : (i += 1) { + const key = try generateKey(arena_alloc, i); + if (trie.contains(key)) found += 1; + } + + const elapsed = timer.read() - start; + const ns_per_op = elapsed / size; + + std.debug.print("Contains {} items: {d:.2} ms ({d} ns/op, found: {})\n", .{ + size, + @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, + ns_per_op, + found, + }); +} + +fn benchmarkPrefixSearch(allocator: std.mem.Allocator, size: usize) !void { + var trie = try ordered.Trie(i32).init(allocator); + defer trie.deinit(); + + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + const arena_alloc = arena.allocator(); + + var i: usize = 0; + while (i < size) : (i += 1) { + const key = try generateKey(arena_alloc, i); + try trie.put(key, @intCast(i)); + } + + var timer = try Timer.start(); + const start = timer.lap(); + + // Search for common prefixes + const num_searches = @min(100, size / 10); + var total_found: usize = 0; + + i = 0; + while (i < num_searches) : (i += 1) { + var keys = try trie.keysWithPrefix(allocator, "key_"); + defer { + for (keys.items) |key| { + allocator.free(key); + } + keys.deinit(allocator); + } + total_found += keys.items.len; + } + + const elapsed = timer.read() - start; + const ns_per_op = elapsed / num_searches; + + std.debug.print("Prefix search {d} times (avg {d} matches): {d:.2} ms ({d} ns/op)\n", .{ + num_searches, + total_found / num_searches, + @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, + ns_per_op, + }); +} + diff --git a/benches/b6_cartesian_tree.zig b/benches/b6_cartesian_tree.zig new file mode 100644 index 0000000..377f8a0 --- /dev/null +++ b/benches/b6_cartesian_tree.zig @@ -0,0 +1,130 @@ +const std = @import("std"); +const ordered = @import("ordered"); +const Timer = std.time.Timer; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + std.debug.print("=== CartesianTree Benchmark ===\n\n", .{}); + + const sizes = [_]usize{ 1000, 10_000, 100_000 }; + + inline for (sizes) |size| { + try benchmarkPut(allocator, size); + try benchmarkGet(allocator, size); + try benchmarkRemove(allocator, size); + try benchmarkIterator(allocator, size); + std.debug.print("\n", .{}); + } +} + +fn benchmarkPut(allocator: std.mem.Allocator, size: usize) !void { + var tree = ordered.CartesianTree(i32, i32).init(allocator); + defer tree.deinit(); + + var timer = try Timer.start(); + const start = timer.lap(); + + var i: i32 = 0; + while (i < size) : (i += 1) { + try tree.put(i, i * 2); + } + + const elapsed = timer.read() - start; + const ns_per_op = elapsed / size; + + std.debug.print("Put {} items: {d:.2} ms ({d} ns/op)\n", .{ + size, + @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, + ns_per_op, + }); +} + +fn benchmarkGet(allocator: std.mem.Allocator, size: usize) !void { + var tree = ordered.CartesianTree(i32, i32).init(allocator); + defer tree.deinit(); + + var i: i32 = 0; + while (i < size) : (i += 1) { + try tree.put(i, i * 2); + } + + var timer = try Timer.start(); + const start = timer.lap(); + + i = 0; + var found: usize = 0; + while (i < size) : (i += 1) { + if (tree.get(i)) |_| found += 1; + } + + const elapsed = timer.read() - start; + const ns_per_op = elapsed / size; + + std.debug.print("Get {} items: {d:.2} ms ({d} ns/op, found: {})\n", .{ + size, + @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, + ns_per_op, + found, + }); +} + +fn benchmarkRemove(allocator: std.mem.Allocator, size: usize) !void { + var tree = ordered.CartesianTree(i32, i32).init(allocator); + defer tree.deinit(); + + var i: i32 = 0; + while (i < size) : (i += 1) { + try tree.put(i, i * 2); + } + + var timer = try Timer.start(); + const start = timer.lap(); + + i = 0; + while (i < size) : (i += 1) { + _ = tree.remove(i); + } + + const elapsed = timer.read() - start; + const ns_per_op = elapsed / size; + + std.debug.print("Remove {} items: {d:.2} ms ({d} ns/op)\n", .{ + size, + @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, + ns_per_op, + }); +} + +fn benchmarkIterator(allocator: std.mem.Allocator, size: usize) !void { + var tree = ordered.CartesianTree(i32, i32).init(allocator); + defer tree.deinit(); + + var i: i32 = 0; + while (i < size) : (i += 1) { + try tree.put(i, i * 2); + } + + var timer = try Timer.start(); + const start = timer.lap(); + + var iter = tree.iterator(allocator); + defer iter.deinit(); + + var count: usize = 0; + while (iter.next()) |_| { + count += 1; + } + + const elapsed = timer.read() - start; + const ns_per_op = elapsed / size; + + std.debug.print("Iterator {} items: {d:.2} ms ({d} ns/op, count: {})\n", .{ + size, + @as(f64, @floatFromInt(elapsed)) / 1_000_000.0, + ns_per_op, + count, + }); +} diff --git a/build.zig b/build.zig index 1cc40b2..ea51958 100644 --- a/build.zig +++ b/build.zig @@ -4,85 +4,98 @@ const fs = std.fs; pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - - // --- Library Setup --- const lib_source = b.path("src/lib.zig"); - - const lib = b.addStaticLibrary(.{ - .name = "ordered", + const lib_module = b.addModule("ordered", .{ .root_source_file = lib_source, .target = target, .optimize = optimize, }); + const lib = b.addLibrary(.{ + .name = "ordered", + .root_module = lib_module, + }); b.installArtifact(lib); - - const lib_module = lib.root_module; - - // Add dependencies - const zig_dbc_dep = b.dependency("dbc", .{}); - const zig_dbc_module = zig_dbc_dep.artifact("dbc").root_module; - lib_module.addImport("dbc", zig_dbc_module); - - // --- Docs Setup --- const docs_step = b.step("docs", "Generate API documentation"); const doc_install_path = "docs/api"; - const gen_docs_cmd = b.addSystemCommand(&[_][]const u8{ b.graph.zig_exe, "build-lib", "src/lib.zig", "-femit-docs=" ++ doc_install_path, }); - const mkdir_cmd = b.addSystemCommand(&[_][]const u8{ "mkdir", "-p", doc_install_path, }); gen_docs_cmd.step.dependOn(&mkdir_cmd.step); - docs_step.dependOn(&gen_docs_cmd.step); - - // --- Test Setup --- const lib_unit_tests = b.addTest(.{ - .root_source_file = lib_source, - .target = target, - .optimize = optimize, + .root_module = lib_module, }); - - // Add dbc import to tests as well - lib_unit_tests.root_module.addImport("dbc", zig_dbc_module); - 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); - // --- Example Setup --- + // Build examples const examples_path = "examples"; - var examples_dir = fs.cwd().openDir(examples_path, .{ .iterate = true }) catch |err| { - if (err == error.FileNotFound) return; - @panic("Can't open 'examples' directory"); - }; - defer examples_dir.close(); - - var dir_iter = examples_dir.iterate(); - while (dir_iter.next() catch @panic("Failed to iterate examples")) |entry| { - if (!std.mem.endsWith(u8, entry.name, ".zig")) continue; - - const exe_name = fs.path.stem(entry.name); - const exe_path = b.fmt("{s}/{s}", .{ examples_path, entry.name }); - - const exe = b.addExecutable(.{ - .name = exe_name, - .root_source_file = b.path(exe_path), - .target = target, - .optimize = optimize, - }); - exe.root_module.addImport("ordered", lib_module); - b.installArtifact(exe); + examples_blk: { + var examples_dir = fs.cwd().openDir(examples_path, .{ .iterate = true }) catch |err| { + if (err == error.FileNotFound or err == error.NotDir) break :examples_blk; + @panic("Can't open 'examples' directory"); + }; + defer examples_dir.close(); + var dir_iter = examples_dir.iterate(); + while (dir_iter.next() catch @panic("Failed to iterate examples")) |entry| { + if (!std.mem.endsWith(u8, entry.name, ".zig")) continue; + const exe_name = fs.path.stem(entry.name); + const exe_path = b.fmt("{s}/{s}", .{ examples_path, entry.name }); + const exe_module = b.createModule(.{ + .root_source_file = b.path(exe_path), + .target = target, + .optimize = optimize, + }); + exe_module.addImport("ordered", lib_module); + const exe = b.addExecutable(.{ + .name = exe_name, + .root_module = exe_module, + }); + b.installArtifact(exe); + const run_cmd = b.addRunArtifact(exe); + const run_step_name = b.fmt("run-{s}", .{exe_name}); + const run_step_desc = b.fmt("Run the {s} example", .{exe_name}); + const run_step = b.step(run_step_name, run_step_desc); + run_step.dependOn(&run_cmd.step); + } + } - const run_cmd = b.addRunArtifact(exe); - const run_step_name = b.fmt("run-{s}", .{exe_name}); - const run_step_desc = b.fmt("Run the {s} example", .{exe_name}); - const run_step = b.step(run_step_name, run_step_desc); - run_step.dependOn(&run_cmd.step); + // Build benchmarks + const benches_path = "benches"; + benches_blk: { + var benches_dir = fs.cwd().openDir(benches_path, .{ .iterate = true }) catch |err| { + if (err == error.FileNotFound or err == error.NotDir) break :benches_blk; + @panic("Can't open 'benches' directory"); + }; + defer benches_dir.close(); + var dir_iter = benches_dir.iterate(); + while (dir_iter.next() catch @panic("Failed to iterate benches")) |entry| { + if (!std.mem.endsWith(u8, entry.name, ".zig")) continue; + const bench_name = fs.path.stem(entry.name); + const bench_path = b.fmt("{s}/{s}", .{ benches_path, entry.name }); + const bench_module = b.createModule(.{ + .root_source_file = b.path(bench_path), + .target = target, + .optimize = .ReleaseFast, // Use ReleaseFast for benchmarks + }); + bench_module.addImport("ordered", lib_module); + const bench_exe = b.addExecutable(.{ + .name = bench_name, + .root_module = bench_module, + }); + b.installArtifact(bench_exe); + const run_bench_cmd = b.addRunArtifact(bench_exe); + const bench_step_name = b.fmt("bench-{s}", .{bench_name}); + const bench_step_desc = b.fmt("Run the {s} benchmark", .{bench_name}); + const bench_step = b.step(bench_step_name, bench_step_desc); + bench_step.dependOn(&run_bench_cmd.step); + } } } diff --git a/build.zig.zon b/build.zig.zon index 6d2af56..015df48 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,14 +1,8 @@ .{ .name = .ordered, - .version = "0.1.0-a1", + .version = "0.1.0-alpha.2", .fingerprint = 0xc3121f99b0352e1b, // Changing this has security and trust implications. - .minimum_zig_version = "0.14.1", - .dependencies = .{ - .dbc = .{ - .url = "https://github.com/habedi/zig-dbc/archive/v0.1.0.tar.gz", - .hash = "zig_dbc-0.1.0-VjRHfLVvAABdMwGT4jSQksMbvSUpWPsSG41qhUAmCJuk", - }, - }, + .minimum_zig_version = "0.15.1", .paths = .{ "build.zig", "build.zig.zon", diff --git a/examples/README.md b/examples/README.md index 59ab688..fc3efd4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,7 +1,10 @@ ## Ordered Examples -| # | File | Description | -|---|----------------------------------------------|----------------------------------------------------------------------------| -| 1 | [e1_bounded_queue.zig](e1_bounded_queue.zig) | An example that shows contracts on a `BoundedQueue` data structure | -| 2 | [e2_file_parser.zig](e2_file_parser.zig) | An example of using contracts to build a robust file parser | -| 3 | [e3_linked_list.zig](e3_linked_list.zig) | An example that uses contracts to guarantee the integrity of a linked list | +| # | File | Description | +|---|------------------------------------------------|-----------------------------------------------------| +| 1 | [e1_btree_map.zig](e1_btree_map.zig) | An example using the `BTreeMap` data structure | +| 2 | [e2_sorted_set.zig](e2_sorted_set.zig) | An example using the `SortedSet` data structure | +| 3 | [e3_red_black_tree.zig](e3_red_black_tree.zig) | An example using the `RedBlackTree` data structure | +| 4 | [e4_skip_list.zig](e4_skip_list.zig) | An example using the `SkipList` data structure | +| 5 | [e5_trie.zig](e5_trie.zig) | An example using the `Trie` data structure | +| 6 | [e6_cartesian_tree.zig](e6_cartesian_tree.zig) | An example using the `CartesianTree` data structure | diff --git a/examples/btree_map.zig b/examples/e1_btree_map.zig similarity index 100% rename from examples/btree_map.zig rename to examples/e1_btree_map.zig diff --git a/examples/sorted_set.zig b/examples/e2_sorted_set.zig similarity index 100% rename from examples/sorted_set.zig rename to examples/e2_sorted_set.zig diff --git a/examples/red_black_tree.zig b/examples/e3_red_black_tree.zig similarity index 100% rename from examples/red_black_tree.zig rename to examples/e3_red_black_tree.zig diff --git a/examples/skip_list.zig b/examples/e4_skip_list.zig similarity index 100% rename from examples/skip_list.zig rename to examples/e4_skip_list.zig diff --git a/examples/trie.zig b/examples/e5_trie.zig similarity index 97% rename from examples/trie.zig rename to examples/e5_trie.zig index 3f6843b..b086970 100644 --- a/examples/trie.zig +++ b/examples/e5_trie.zig @@ -28,7 +28,7 @@ pub fn main() !void { for (keys.items) |key| { allocator.free(key); } - keys.deinit(); + keys.deinit(allocator); } std.debug.print("Keys with prefix 'car': ", .{}); diff --git a/examples/cartesian_tree.zig b/examples/e6_cartesian_tree.zig similarity index 100% rename from examples/cartesian_tree.zig rename to examples/e6_cartesian_tree.zig diff --git a/logo.svg b/logo.svg index d5b2766..db99182 100644 --- a/logo.svg +++ b/logo.svg @@ -2,58 +2,58 @@ - - - - - - + fill="#000000" + width="800px" + height="800px" + viewBox="0 0 24 24" + id="zig-zag-left-right-arrow" + data-name="Flat Line" + class="icon flat-line" + version="1.1" + sodipodi:docname="logo.svg" + inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + + + + + + diff --git a/pyproject.toml b/pyproject.toml index e1f37b7..a877ee1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,7 @@ [project] name = "ordered" version = "0.1.0" -description = "Python environment for Ordered" -readme = "README.md" -license = { text = "MIT" } -authors = [ - { name = "Hassan Abedi", email = "hassan.abedi.t@gmail.com" } -] +description = "The Python environment for the Ordered project" requires-python = ">=3.10,<4.0" dependencies = [ diff --git a/src/ordered/cartesian_tree.zig b/src/ordered/cartesian_tree.zig index 6c1c04b..0eab7db 100644 --- a/src/ordered/cartesian_tree.zig +++ b/src/ordered/cartesian_tree.zig @@ -218,23 +218,25 @@ pub fn CartesianTree(comptime K: type, comptime V: type) type { /// Iterator for in-order traversal pub const Iterator = struct { stack: std.ArrayList(*Node), + allocator: Allocator, pub fn init(allocator: Allocator, root: ?*Node) Iterator { var it = Iterator{ - .stack = std.ArrayList(*Node).init(allocator), + .stack = .{}, + .allocator = allocator, }; it.pushLeft(root); return it; } pub fn deinit(self: *Iterator) void { - self.stack.deinit(); + self.stack.deinit(self.allocator); } fn pushLeft(self: *Iterator, node: ?*Node) void { var current = node; while (current) |n| { - self.stack.append(n) catch return; // Handle potential allocation failure + self.stack.append(self.allocator, n) catch return; // Handle potential allocation failure current = n.left; } } diff --git a/src/ordered/red_black_tree.zig b/src/ordered/red_black_tree.zig index 4315ef1..3b814ed 100644 --- a/src/ordered/red_black_tree.zig +++ b/src/ordered/red_black_tree.zig @@ -395,16 +395,18 @@ pub fn RedBlackTree(comptime T: type, comptime Context: type) type { /// Iterator for in-order traversal pub const Iterator = struct { stack: std.ArrayList(*Node), + allocator: Allocator, pub fn init(allocator: Allocator, root: ?*Node) !Iterator { var it = Iterator{ - .stack = std.ArrayList(*Node).init(allocator), + .stack = .{}, + .allocator = allocator, }; // Initialize stack with leftmost path var node = root; while (node) |n| { - try it.stack.append(n); + try it.stack.append(allocator, n); node = n.left; } @@ -412,18 +414,18 @@ pub fn RedBlackTree(comptime T: type, comptime Context: type) type { } pub fn deinit(self: *Iterator) void { - self.stack.deinit(); + self.stack.deinit(self.allocator); } pub fn next(self: *Iterator) !?*Node { if (self.stack.items.len == 0) return null; - const node = self.stack.pop(); + const node: *Node = self.stack.pop().?; // Add right subtree to stack var current = node.right; while (current) |n| { - try self.stack.append(n); + try self.stack.append(self.allocator, n); current = n.left; } diff --git a/src/ordered/sorted_set.zig b/src/ordered/sorted_set.zig index d4abd77..8b8c7ec 100644 --- a/src/ordered/sorted_set.zig +++ b/src/ordered/sorted_set.zig @@ -16,13 +16,13 @@ pub fn SortedSet( pub fn init(allocator: std.mem.Allocator) Self { return .{ - .items = std.ArrayList(T).init(allocator), + .items = .{}, .allocator = allocator, }; } pub fn deinit(self: *Self) void { - self.items.deinit(); + self.items.deinit(self.allocator); } fn compareFn(key: T, item: T) std.math.Order { @@ -32,7 +32,7 @@ pub fn SortedSet( /// Adds a value to the vector, maintaining sort order. pub fn add(self: *Self, value: T) !void { const index = std.sort.lowerBound(T, self.items.items, value, compareFn); - try self.items.insert(index, value); + try self.items.insert(self.allocator, index, value); } /// Removes an element at a given index. diff --git a/src/ordered/trie.zig b/src/ordered/trie.zig index 38a2d7b..addb8ef 100644 --- a/src/ordered/trie.zig +++ b/src/ordered/trie.zig @@ -154,7 +154,7 @@ pub fn Trie(comptime V: type) type { } pub fn keysWithPrefix(self: *const Self, allocator: std.mem.Allocator, prefix: []const u8) !std.ArrayList([]u8) { - var results = std.ArrayList([]u8).init(allocator); + var results: std.ArrayList([]u8) = .{}; const prefix_node = self.findNode(prefix) orelse return results; try self.collectKeys(allocator, prefix_node, &results, prefix); @@ -165,7 +165,7 @@ pub fn Trie(comptime V: type) type { fn collectKeys(self: *const Self, allocator: std.mem.Allocator, node: *const TrieNode, results: *std.ArrayList([]u8), current_key: []const u8) !void { if (node.is_end) { const key_copy = try allocator.dupe(u8, current_key); - try results.append(key_copy); + try results.append(allocator, key_copy); } var iter = node.children.iterator(); @@ -194,8 +194,8 @@ pub fn Trie(comptime V: type) type { }; fn init(allocator: std.mem.Allocator, root: *const TrieNode) !Iterator { - var stack = std.ArrayList(IteratorFrame).init(allocator); - try stack.append(IteratorFrame{ + var stack: std.ArrayList(IteratorFrame) = .{}; + try stack.append(allocator, IteratorFrame{ .node = root, .child_iter = root.children.iterator(), .visited_self = false, @@ -204,13 +204,13 @@ pub fn Trie(comptime V: type) type { return Iterator{ .stack = stack, .allocator = allocator, - .current_key = std.ArrayList(u8).init(allocator), + .current_key = .{}, }; } fn deinit(self: *Iterator) void { - self.stack.deinit(); - self.current_key.deinit(); + self.stack.deinit(self.allocator); + self.current_key.deinit(self.allocator); } pub fn next(self: *Iterator) ?struct { key: []const u8, value: V } { @@ -226,9 +226,9 @@ pub fn Trie(comptime V: type) type { const char = entry.key_ptr.*; const child = entry.value_ptr.*; - self.current_key.append(char) catch return null; + self.current_key.append(self.allocator, char) catch return null; - self.stack.append(IteratorFrame{ + self.stack.append(self.allocator, IteratorFrame{ .node = child, .child_iter = child.children.iterator(), .visited_self = false, @@ -288,7 +288,7 @@ test "Trie: prefix operations" { for (keys.items) |key| { allocator.free(key); } - keys.deinit(); + keys.deinit(allocator); } try std.testing.expectEqual(@as(usize, 3), keys.items.len); diff --git a/test_arraylist.zig b/test_arraylist.zig new file mode 100644 index 0000000..dd53156 --- /dev/null +++ b/test_arraylist.zig @@ -0,0 +1,14 @@ +const std = @import("std"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + var list = std.ArrayList(i32).init(allocator); + defer list.deinit(); + + try list.append(42); + std.debug.print("Success! ArrayList works.\n", .{}); +} + From 00bd063373b47ef0688035d00791647a0de892f7 Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Wed, 8 Oct 2025 22:07:15 +0200 Subject: [PATCH 2/2] Add benchmarks --- .github/workflows/benches.yml | 36 ++++++ .github/workflows/docs.yml | 3 +- .github/workflows/lints.yml | 6 +- .github/workflows/tests.yml | 4 +- README.md | 21 ++-- benches/README.md | 36 +++--- benches/b1_btree_map.zig | 1 - benches/b2_sorted_set.zig | 1 - benches/b3_red_black_tree.zig | 1 - benches/b4_skip_list.zig | 1 - benches/b5_trie.zig | 1 - src/ordered/btree_map.zig | 169 ++++++++++++++++++++++++++- src/ordered/cartesian_tree.zig | 202 ++++++++++++++++++++++++++------ src/ordered/red_black_tree.zig | 195 +++++++++++++++++++++++++++++++ src/ordered/skip_list.zig | 146 ++++++++++++++++++++++-- src/ordered/sorted_set.zig | 92 +++++++++++++++ src/ordered/trie.zig | 203 +++++++++++++++++++++++++++++---- test_arraylist.zig | 14 --- 18 files changed, 1002 insertions(+), 130 deletions(-) create mode 100644 .github/workflows/benches.yml delete mode 100644 test_arraylist.zig diff --git a/.github/workflows/benches.yml b/.github/workflows/benches.yml new file mode 100644 index 0000000..6f21c91 --- /dev/null +++ b/.github/workflows/benches.yml @@ -0,0 +1,36 @@ +name: Run Benchmarks + +on: + workflow_dispatch: + push: + branches: + - main + tags: + - 'v*' + pull_request: + branches: + - main + +permissions: + contents: read + +jobs: + benchmarks: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Install Zig + uses: goto-bus-stop/setup-zig@v2 + with: + version: '0.15.1' + + - name: Install Dependencies + run: | + sudo apt-get update + sudo apt-get install -y make + + - name: Run the Benchmarks + run: make bench diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 7edda17..916ed06 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,12 +1,11 @@ name: Publish API Documentation on: + workflow_dispatch: push: tags: - 'v*' - workflow_dispatch: - permissions: contents: write diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index 13bd36f..e57bae1 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -1,18 +1,14 @@ name: Run Linter Checks on: + workflow_dispatch: push: - branches: - - main tags: - 'v*' - pull_request: branches: - main - workflow_dispatch: - permissions: contents: read diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8c3989d..712b7c8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,18 +1,16 @@ name: Run Tests on: + workflow_dispatch: push: branches: - main tags: - 'v*' - pull_request: branches: - main - workflow_dispatch: - permissions: contents: read diff --git a/README.md b/README.md index 11235b0..a55c66f 100644 --- a/README.md +++ b/README.md @@ -7,32 +7,33 @@

Ordered

[![Tests](https://img.shields.io/github/actions/workflow/status/CogitatorTech/ordered/tests.yml?label=tests&style=flat&labelColor=282c34&logo=github)](https://github.com/CogitatorTech/ordered/actions/workflows/tests.yml) -[![CodeFactor](https://img.shields.io/codefactor/grade/github/CogitatorTech/ordered?label=code%20quality&style=flat&labelColor=282c34&logo=codefactor)](https://www.codefactor.io/repository/github/CogitatorTech/ordered) -[![Zig Version](https://img.shields.io/badge/Zig-0.15.1-orange?logo=zig&labelColor=282c34)](https://ziglang.org/download/) +[![Benchmarks](https://img.shields.io/github/actions/workflow/status/CogitatorTech/ordered/benches.yml?label=benches&style=flat&labelColor=282c34&logo=github)](https://github.com/CogitatorTech/ordered/actions/workflows/benches.yml) +[![CodeFactor](https://img.shields.io/codefactor/grade/github/CogitatorTech/ordered?label=quality&style=flat&labelColor=282c34&logo=codefactor)](https://www.codefactor.io/repository/github/CogitatorTech/ordered) [![Docs](https://img.shields.io/badge/docs-view-blue?style=flat&labelColor=282c34&logo=read-the-docs)](https://CogitatorTech.github.io/ordered/) [![Examples](https://img.shields.io/badge/examples-view-green?style=flat&labelColor=282c34&logo=zig)](https://github.com/CogitatorTech/ordered/tree/main/examples) +[![Zig Version](https://img.shields.io/badge/Zig-0.15.1-orange?logo=zig&labelColor=282c34)](https://ziglang.org/download/) [![Release](https://img.shields.io/github/release/CogitatorTech/ordered.svg?label=release&style=flat&labelColor=282c34&logo=github)](https://github.com/CogitatorTech/ordered/releases/latest) [![License](https://img.shields.io/badge/license-MIT-007ec6?label=license&style=flat&labelColor=282c34&logo=open-source-initiative)](https://github.com/CogitatorTech/ordered/blob/main/LICENSE) -A collection of data structures that keep data in order +A collection of data structures that keep data in order in pure Zig --- -Ordered library includes fast and efficient implementations of popular data structures including +Ordered is a Zig library that provides fast and efficient implementations of various popular data structures including B-tree, skip list, trie, and red-black tree for Zig programming language. ### Supported Data Structures Currently supported data structures include: -- [B-tree](src/ordered/btree_map.zig): a balanced n-array tree that maintains the order of keys. -- [Sorted set](src/ordered/sorted_set.zig): a set with ordered elements based on keys. -- [Skip list](src/ordered/skip_list.zig): a probabilistic data structure that maintains sorted order using multiple linked lists. -- [Trie](src/ordered/trie.zig): a prefix tree that supports efficient retrieval of keys with common prefixes. -- [Red-black tree](src/ordered/red_black_tree.zig): A self-balancing binary search tree that maintains the order of keys -- [Cartesian tree](src/ordered/cartesian_tree.zig): A binary tree that maintains both a binary search tree property on keys and a heap property on priorities. +- [B-tree](src/ordered/btree_map.zig): A self-balancing search tree where nodes can have many children. +- [Sorted Set](src/ordered/sorted_set.zig): A data structure that stores a collection of unique elements in a consistently sorted order. +- [Skip List](src/ordered/skip_list.zig): A probabilistic data structure that uses multiple linked lists to create "express lanes" for fast, tree-like search. +- [Trie](src/ordered/trie.zig): A tree where paths from the root represent prefixes which makes it extremely fast for tasks like text autocomplete. +- [Red-black Tree](src/ordered/red_black_tree.zig): A self-balancing binary search tree that uses node colors to guarantee efficient operations. +- [Cartesian Tree](src/ordered/cartesian_tree.zig): A binary tree that uniquely combines a binary search tree property for its keys with a heap** property for its values. | # | Data Structure | Build Complexity | Memory Complexity | Search Complexity | |---|----------------|------------------|-------------------|----------------------| diff --git a/benches/README.md b/benches/README.md index f0f5b8f..ebbb48f 100644 --- a/benches/README.md +++ b/benches/README.md @@ -1,8 +1,8 @@ -# Benchmarks +## Benchmarks -This directory contains performance benchmarks for all data structures in the `ordered` library. +This directory contains benchmarks for the data structures in the `ordered` library. -## Running Benchmarks +### Running Benchmarks Each benchmark can be run using the following command pattern: @@ -10,7 +10,7 @@ Each benchmark can be run using the following command pattern: zig build bench- ``` -### Available Benchmarks +#### Available Benchmarks - **BTreeMap**: `zig build bench-btree_map_bench` - **SortedSet**: `zig build bench-sorted_set_bench` @@ -19,53 +19,53 @@ zig build bench- - **Trie**: `zig build bench-trie_bench` - **CartesianTree**: `zig build bench-cartesian_tree_bench` -## What Each Benchmark Tests +### What Each Benchmark Tests -### BTreeMap Benchmark +#### BTreeMap Benchmark - **Insert**: Sequential insertion of integers - **Lookup**: Finding all inserted keys - **Delete**: Removing all keys -### SortedSet Benchmark -- **Add**: Adding elements while maintaining sorted order +#### SortedSet Benchmark +- **Add**: Adding elements while maintaining a sorted order - **Contains**: Checking if elements exist - **Remove**: Removing elements from the set -### RedBlackTree Benchmark +#### RedBlackTree Benchmark - **Insert**: Inserting nodes with self-balancing - **Find**: Searching for nodes - **Remove**: Deleting nodes while maintaining balance - **Iterator**: In-order traversal performance -### SkipList Benchmark +#### SkipList Benchmark - **Put**: Inserting key-value pairs with probabilistic levels - **Get**: Retrieving values by key - **Delete**: Removing key-value pairs -### Trie Benchmark +#### Trie Benchmark - **Put**: Inserting strings with associated values - **Get**: Retrieving values by string key - **Contains**: Checking if strings exist - **Prefix Search**: Finding all keys with a common prefix -### CartesianTree Benchmark +#### CartesianTree Benchmark - **Put**: Inserting key-value pairs with random priorities - **Get**: Retrieving values by key - **Remove**: Deleting nodes - **Iterator**: In-order traversal performance -## Benchmark Sizes +### Benchmark Sizes Each benchmark tests with multiple dataset sizes: - Small: 1,000 items - Medium: 10,000 items - Large: 50,000 - 100,000 items (varies by data structure) -## Build Configuration +### Build Configuration Benchmarks are compiled with `ReleaseFast` optimization mode for accurate performance measurements. -## Example Output +### Example Output ``` === BTreeMap Benchmark === @@ -79,9 +79,9 @@ Lookup 10000 items: 2.10 ms (210 ns/op, found: 10000) Delete 10000 items: 4.15 ms (415 ns/op) ``` -## Notes +### Notes - All benchmarks use a simple integer or string key type for consistency - Times are reported in both total milliseconds and nanoseconds per operation -- Memory allocations use `GeneralPurposeAllocator` for production-like behavior - +- Memory allocations use `GeneralPurposeAllocator` for simulating a more realistic memory usage +- Results may vary based on a hardware and system load diff --git a/benches/b1_btree_map.zig b/benches/b1_btree_map.zig index 3177505..c7757dd 100644 --- a/benches/b1_btree_map.zig +++ b/benches/b1_btree_map.zig @@ -100,4 +100,3 @@ fn benchmarkDelete(allocator: std.mem.Allocator, size: usize) !void { ns_per_op, }); } - diff --git a/benches/b2_sorted_set.zig b/benches/b2_sorted_set.zig index 6905a18..ece558f 100644 --- a/benches/b2_sorted_set.zig +++ b/benches/b2_sorted_set.zig @@ -99,4 +99,3 @@ fn benchmarkRemove(allocator: std.mem.Allocator, size: usize) !void { ns_per_op, }); } - diff --git a/benches/b3_red_black_tree.zig b/benches/b3_red_black_tree.zig index 26ffde3..d571e63 100644 --- a/benches/b3_red_black_tree.zig +++ b/benches/b3_red_black_tree.zig @@ -135,4 +135,3 @@ fn benchmarkIterator(allocator: std.mem.Allocator, size: usize) !void { count, }); } - diff --git a/benches/b4_skip_list.zig b/benches/b4_skip_list.zig index 8cb53d7..a534f9c 100644 --- a/benches/b4_skip_list.zig +++ b/benches/b4_skip_list.zig @@ -100,4 +100,3 @@ fn benchmarkDelete(allocator: std.mem.Allocator, size: usize) !void { ns_per_op, }); } - diff --git a/benches/b5_trie.zig b/benches/b5_trie.zig index e8fbb34..deb0c14 100644 --- a/benches/b5_trie.zig +++ b/benches/b5_trie.zig @@ -164,4 +164,3 @@ fn benchmarkPrefixSearch(allocator: std.mem.Allocator, size: usize) !void { ns_per_op, }); } - diff --git a/src/ordered/btree_map.zig b/src/ordered/btree_map.zig index 7693dc7..58a4634 100644 --- a/src/ordered/btree_map.zig +++ b/src/ordered/btree_map.zig @@ -79,9 +79,10 @@ pub fn BTreeMap( /// Inserts a key-value pair. If the key exists, the value is updated. pub fn put(self: *Self, key: K, value: V) !void { + // Check if key exists and just update the value in place if (self.get(key) != null) { _ = self.remove(key); - self.len += 1; + // Don't increment len here, it will be incremented below } var root_node = if (self.root) |r| r else { @@ -113,22 +114,28 @@ pub fn BTreeMap( new_sibling.is_leaf = child.is_leaf; const t = MIN_KEYS; - new_sibling.len = t; + // Calculate how many keys go to the right sibling + // Full node has BRANCHING_FACTOR - 1 keys + // Left child keeps t keys, parent gets 1, right sibling gets the rest + const right_keys = BRANCHING_FACTOR - 1 - t - 1; + new_sibling.len = right_keys; + // Copy the right half of keys to the new sibling var j: u16 = 0; - while (j < t) : (j += 1) { + while (j < right_keys) : (j += 1) { new_sibling.keys[j] = child.keys[j + t + 1]; new_sibling.values[j] = child.values[j + t + 1]; } if (!child.is_leaf) { j = 0; - while (j < t + 1) : (j += 1) { + while (j <= right_keys) : (j += 1) { new_sibling.children[j] = child.children[j + t + 1]; } } child.len = t; + // Insert new sibling into parent j = parent.len; while (j > index) : (j -= 1) { parent.children[j + 1] = parent.children[j]; @@ -400,3 +407,157 @@ test "BTreeMap: put, get, and delete" { try str_map.put("b", 2); try std.testing.expectEqual(2, str_map.get("b").?.*); } + +test "BTreeMap: empty map operations" { + const allocator = std.testing.allocator; + var map = BTreeMap(i32, i32, i32Compare, 4).init(allocator); + defer map.deinit(); + + try std.testing.expect(map.get(42) == null); + try std.testing.expectEqual(@as(usize, 0), map.len); + try std.testing.expect(map.remove(42) == null); +} + +test "BTreeMap: single element operations" { + const allocator = std.testing.allocator; + var map = BTreeMap(i32, []const u8, i32Compare, 4).init(allocator); + defer map.deinit(); + + try map.put(42, "answer"); + try std.testing.expectEqual(@as(usize, 1), map.len); + try std.testing.expectEqualStrings("answer", map.get(42).?.*); + + const removed = map.remove(42); + try std.testing.expect(removed != null); + try std.testing.expectEqualStrings("answer", removed.?); + try std.testing.expectEqual(@as(usize, 0), map.len); +} + +test "BTreeMap: update existing keys" { + const allocator = std.testing.allocator; + var map = BTreeMap(i32, i32, i32Compare, 4).init(allocator); + defer map.deinit(); + + try map.put(10, 100); + try map.put(20, 200); + try map.put(10, 999); // Update + + try std.testing.expectEqual(@as(usize, 2), map.len); + try std.testing.expectEqual(@as(i32, 999), map.get(10).?.*); +} + +test "BTreeMap: sequential insertion" { + const allocator = std.testing.allocator; + var map = BTreeMap(i32, i32, i32Compare, 5).init(allocator); + defer map.deinit(); + + var i: i32 = 0; + while (i < 50) : (i += 1) { + try map.put(i, i * 2); + } + + try std.testing.expectEqual(@as(usize, 50), map.len); + + i = 0; + while (i < 50) : (i += 1) { + try std.testing.expectEqual(i * 2, map.get(i).?.*); + } +} + +test "BTreeMap: reverse insertion" { + const allocator = std.testing.allocator; + var map = BTreeMap(i32, i32, i32Compare, 5).init(allocator); + defer map.deinit(); + + var i: i32 = 50; + while (i > 0) : (i -= 1) { + try map.put(i, i * 3); + } + + try std.testing.expectEqual(@as(usize, 50), map.len); + + i = 1; + while (i <= 50) : (i += 1) { + try std.testing.expectEqual(i * 3, map.get(i).?.*); + } +} + +test "BTreeMap: random operations" { + const allocator = std.testing.allocator; + var map = BTreeMap(i32, i32, i32Compare, 4).init(allocator); + defer map.deinit(); + + const values = [_]i32{ 15, 3, 27, 8, 42, 1, 19, 33, 11, 25 }; + for (values) |val| { + try map.put(val, val * 10); + } + + try std.testing.expectEqual(@as(usize, 10), map.len); + + for (values) |val| { + try std.testing.expectEqual(val * 10, map.get(val).?.*); + } + + // Remove some + _ = map.remove(15); + _ = map.remove(27); + _ = map.remove(1); + + try std.testing.expectEqual(@as(usize, 7), map.len); + try std.testing.expect(map.get(15) == null); + try std.testing.expect(map.get(27) == null); + try std.testing.expect(map.get(1) == null); +} + +test "BTreeMap: remove all elements" { + const allocator = std.testing.allocator; + var map = BTreeMap(i32, i32, i32Compare, 4).init(allocator); + defer map.deinit(); + + try map.put(1, 1); + try map.put(2, 2); + try map.put(3, 3); + try map.put(4, 4); + try map.put(5, 5); + + _ = map.remove(1); + _ = map.remove(2); + _ = map.remove(3); + _ = map.remove(4); + _ = map.remove(5); + + try std.testing.expectEqual(@as(usize, 0), map.len); + try std.testing.expect(map.get(3) == null); +} + +test "BTreeMap: minimum branching factor" { + const allocator = std.testing.allocator; + var map = BTreeMap(i32, i32, i32Compare, 3).init(allocator); + defer map.deinit(); + + var i: i32 = 0; + while (i < 20) : (i += 1) { + try map.put(i, i); + } + + try std.testing.expectEqual(@as(usize, 20), map.len); + + i = 0; + while (i < 20) : (i += 1) { + try std.testing.expectEqual(i, map.get(i).?.*); + } +} + +test "BTreeMap: negative keys" { + const allocator = std.testing.allocator; + var map = BTreeMap(i32, i32, i32Compare, 4).init(allocator); + defer map.deinit(); + + try map.put(-10, 10); + try map.put(-5, 5); + try map.put(0, 0); + try map.put(5, -5); + + try std.testing.expectEqual(@as(i32, 10), map.get(-10).?.*); + try std.testing.expectEqual(@as(i32, -5), map.get(5).?.*); +} diff --git a/src/ordered/cartesian_tree.zig b/src/ordered/cartesian_tree.zig index 0eab7db..03eb555 100644 --- a/src/ordered/cartesian_tree.zig +++ b/src/ordered/cartesian_tree.zig @@ -281,62 +281,190 @@ test "CartesianTree basic operations" { // Test get try testing.expectEqualStrings("five", tree.get(5).?); try testing.expectEqualStrings("three", tree.get(3).?); - try testing.expectEqualStrings("seven", tree.get(7).?); - try testing.expectEqualStrings("one", tree.get(1).?); try testing.expect(tree.get(99) == null); // Test contains try testing.expect(tree.contains(5)); - try testing.expect(tree.contains(3)); try testing.expect(!tree.contains(99)); - // Test removal + // Test remove try testing.expect(tree.remove(3)); + try testing.expectEqual(@as(usize, 3), tree.count()); try testing.expect(!tree.contains(3)); +} + +test "CartesianTree: empty tree operations" { + var tree = CartesianTree(i32, i32).init(testing.allocator); + defer tree.deinit(); + + try testing.expect(tree.isEmpty()); + try testing.expectEqual(@as(usize, 0), tree.count()); + try testing.expect(tree.get(42) == null); + try testing.expect(!tree.contains(42)); + try testing.expect(!tree.remove(42)); +} + +test "CartesianTree: single element" { + var tree = CartesianTree(i32, i32).init(testing.allocator); + defer tree.deinit(); + + try tree.putWithPriority(42, 100, 50); + try testing.expectEqual(@as(usize, 1), tree.count()); + try testing.expectEqual(@as(i32, 100), tree.get(42).?); + + const removed = tree.remove(42); + try testing.expect(removed); + try testing.expect(tree.isEmpty()); + try testing.expect(tree.root == null); +} + +test "CartesianTree: priority ordering" { + var tree = CartesianTree(i32, i32).init(testing.allocator); + defer tree.deinit(); + + // Higher priority should be closer to root + try tree.putWithPriority(10, 1, 100); // Highest priority + try tree.putWithPriority(5, 2, 50); + try tree.putWithPriority(15, 3, 75); + + try testing.expectEqual(@as(u32, 100), tree.root.?.priority); + try testing.expectEqual(@as(i32, 10), tree.root.?.key); +} + +test "CartesianTree: update existing key with different priority" { + var tree = CartesianTree(i32, i32).init(testing.allocator); + defer tree.deinit(); + + try tree.putWithPriority(10, 100, 50); + try tree.putWithPriority(10, 200, 75); + + try testing.expectEqual(@as(usize, 1), tree.count()); + try testing.expectEqual(@as(i32, 200), tree.get(10).?); +} + +test "CartesianTree: random priorities with put" { + var tree = CartesianTree(i32, i32).init(testing.allocator); + defer tree.deinit(); + + // Using put which generates random priorities + try tree.put(1, 1); + try tree.put(2, 2); + try tree.put(3, 3); + try testing.expectEqual(@as(usize, 3), tree.count()); - try testing.expect(!tree.remove(99)); + try testing.expectEqual(@as(i32, 1), tree.get(1).?); + try testing.expectEqual(@as(i32, 2), tree.get(2).?); + try testing.expectEqual(@as(i32, 3), tree.get(3).?); } -test "CartesianTree iterator" { +test "CartesianTree: sequential keys" { var tree = CartesianTree(i32, i32).init(testing.allocator); defer tree.deinit(); - // Insert values with specific priorities to control structure - try tree.putWithPriority(5, 50, 10); - try tree.putWithPriority(3, 30, 5); - try tree.putWithPriority(7, 70, 15); - try tree.putWithPriority(1, 10, 3); - try tree.putWithPriority(9, 90, 8); - - var it = tree.iterator(testing.allocator); - defer it.deinit(); - - // Should iterate in sorted key order - const expected_keys = [_]i32{ 1, 3, 5, 7, 9 }; - const expected_values = [_]i32{ 10, 30, 50, 70, 90 }; - - var i: usize = 0; - while (it.next()) |entry| { - try testing.expectEqual(expected_keys[i], entry.key); - try testing.expectEqual(expected_values[i], entry.value); - i += 1; + var i: i32 = 0; + while (i < 20) : (i += 1) { + try tree.putWithPriority(i, i * 2, @intCast(i)); + } + + try testing.expectEqual(@as(usize, 20), tree.count()); + + i = 0; + while (i < 20) : (i += 1) { + try testing.expectEqual(i * 2, tree.get(i).?); } - try testing.expectEqual(@as(usize, 5), i); } -test "CartesianTree heap property" { - // Test that heap property is maintained (higher priority nodes are ancestors) +test "CartesianTree: remove non-existent key" { var tree = CartesianTree(i32, i32).init(testing.allocator); defer tree.deinit(); - try tree.putWithPriority(5, 50, 100); // Root (highest priority) - try tree.putWithPriority(3, 30, 80); // Left subtree - try tree.putWithPriority(7, 70, 90); // Right subtree - try tree.putWithPriority(1, 10, 60); // Left-left - try tree.putWithPriority(9, 90, 70); // Right-right + try tree.putWithPriority(10, 10, 10); + try tree.putWithPriority(20, 20, 20); - // Verify structure maintains heap property - // Root should have highest priority - try testing.expectEqual(@as(u32, 100), tree.root.?.priority); - try testing.expectEqual(@as(i32, 5), tree.root.?.key); + const removed = tree.remove(15); + try testing.expect(!removed); + try testing.expectEqual(@as(usize, 2), tree.count()); +} + +test "CartesianTree: remove all elements" { + var tree = CartesianTree(i32, i32).init(testing.allocator); + defer tree.deinit(); + + try tree.putWithPriority(1, 1, 1); + try tree.putWithPriority(2, 2, 2); + try tree.putWithPriority(3, 3, 3); + + try testing.expect(tree.remove(1)); + try testing.expect(tree.remove(2)); + try testing.expect(tree.remove(3)); + + try testing.expect(tree.isEmpty()); + try testing.expect(tree.get(2) == null); +} + +test "CartesianTree: negative keys" { + var tree = CartesianTree(i32, i32).init(testing.allocator); + defer tree.deinit(); + + try tree.putWithPriority(-10, 10, 100); + try tree.putWithPriority(-5, 5, 50); + try tree.putWithPriority(0, 0, 75); + try tree.putWithPriority(5, -5, 25); + + try testing.expectEqual(@as(i32, 10), tree.get(-10).?); + try testing.expectEqual(@as(i32, -5), tree.get(5).?); +} + +test "CartesianTree: iterator traversal" { + var tree = CartesianTree(i32, i32).init(testing.allocator); + defer tree.deinit(); + + try tree.putWithPriority(30, 30, 30); + try tree.putWithPriority(10, 10, 10); + try tree.putWithPriority(20, 20, 20); + try tree.putWithPriority(5, 5, 5); + + var iter = tree.iterator(testing.allocator); + defer iter.deinit(); + + // Should iterate in sorted key order (BST property) + const expected = [_]i32{ 5, 10, 20, 30 }; + var idx: usize = 0; + + while (iter.next()) |entry| : (idx += 1) { + try testing.expectEqual(expected[idx], entry.key); + } + try testing.expectEqual(@as(usize, 4), idx); +} + +test "CartesianTree: large dataset" { + var tree = CartesianTree(i32, i32).init(testing.allocator); + defer tree.deinit(); + + var i: i32 = 0; + while (i < 50) : (i += 1) { + try tree.putWithPriority(i, i * 3, @intCast(i * 2)); + } + + try testing.expectEqual(@as(usize, 50), tree.count()); + + i = 0; + while (i < 50) : (i += 1) { + try testing.expectEqual(i * 3, tree.get(i).?); + } +} + +test "CartesianTree: same priorities different keys" { + var tree = CartesianTree(i32, i32).init(testing.allocator); + defer tree.deinit(); + + // When priorities are equal, BST property still maintained by key + try tree.putWithPriority(10, 1, 50); + try tree.putWithPriority(5, 2, 50); + try tree.putWithPriority(15, 3, 50); + + try testing.expectEqual(@as(usize, 3), tree.count()); + try testing.expect(tree.contains(5)); + try testing.expect(tree.contains(10)); + try testing.expect(tree.contains(15)); } diff --git a/src/ordered/red_black_tree.zig b/src/ordered/red_black_tree.zig index 3b814ed..77e9605 100644 --- a/src/ordered/red_black_tree.zig +++ b/src/ordered/red_black_tree.zig @@ -453,3 +453,198 @@ pub fn DefaultContext(comptime T: type) type { pub fn RedBlackTreeManaged(comptime T: type) type { return RedBlackTree(T, DefaultContext(T)); } + +test "RedBlackTree: basic operations" { + const allocator = std.testing.allocator; + var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + defer tree.deinit(); + + try tree.insert(10); + try tree.insert(20); + try tree.insert(5); + + try std.testing.expectEqual(@as(usize, 3), tree.count()); + try std.testing.expect(tree.contains(10)); + try std.testing.expect(tree.contains(5)); + try std.testing.expect(!tree.contains(99)); +} + +test "RedBlackTree: empty tree operations" { + const allocator = std.testing.allocator; + var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + defer tree.deinit(); + + try std.testing.expect(!tree.contains(42)); + try std.testing.expectEqual(@as(usize, 0), tree.count()); + try std.testing.expect(!tree.remove(42)); +} + +test "RedBlackTree: single element" { + const allocator = std.testing.allocator; + var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + defer tree.deinit(); + + try tree.insert(42); + try std.testing.expectEqual(@as(usize, 1), tree.count()); + try std.testing.expect(tree.contains(42)); + try std.testing.expect(tree.root.?.color == .black); + + const removed = tree.remove(42); + try std.testing.expect(removed); + try std.testing.expectEqual(@as(usize, 0), tree.count()); + try std.testing.expect(tree.root == null); +} + +test "RedBlackTree: duplicate insertions" { + const allocator = std.testing.allocator; + var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + defer tree.deinit(); + + try tree.insert(10); + try tree.insert(10); + try tree.insert(10); + + // Duplicates update existing nodes + try std.testing.expectEqual(@as(usize, 1), tree.count()); +} + +test "RedBlackTree: sequential insertion" { + const allocator = std.testing.allocator; + var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + defer tree.deinit(); + + var i: i32 = 0; + while (i < 50) : (i += 1) { + try tree.insert(i); + } + + try std.testing.expectEqual(@as(usize, 50), tree.count()); + try std.testing.expect(tree.root.?.color == .black); + + i = 0; + while (i < 50) : (i += 1) { + try std.testing.expect(tree.contains(i)); + } +} + +test "RedBlackTree: reverse insertion" { + const allocator = std.testing.allocator; + var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + defer tree.deinit(); + + var i: i32 = 50; + while (i > 0) : (i -= 1) { + try tree.insert(i); + } + + try std.testing.expectEqual(@as(usize, 50), tree.count()); + try std.testing.expect(tree.root.?.color == .black); +} + +test "RedBlackTree: remove from middle" { + const allocator = std.testing.allocator; + var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + defer tree.deinit(); + + try tree.insert(10); + try tree.insert(5); + try tree.insert(15); + try tree.insert(3); + try tree.insert(7); + + const removed = tree.remove(5); + try std.testing.expect(removed); + try std.testing.expectEqual(@as(usize, 4), tree.count()); + try std.testing.expect(!tree.contains(5)); + try std.testing.expect(tree.contains(3)); + try std.testing.expect(tree.contains(7)); +} + +test "RedBlackTree: remove root" { + const allocator = std.testing.allocator; + var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + defer tree.deinit(); + + try tree.insert(10); + try tree.insert(5); + try tree.insert(15); + + const removed = tree.remove(10); + try std.testing.expect(removed); + try std.testing.expectEqual(@as(usize, 2), tree.count()); + try std.testing.expect(tree.root.?.color == .black); +} + +test "RedBlackTree: minimum and maximum" { + const allocator = std.testing.allocator; + var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + defer tree.deinit(); + + try tree.insert(10); + try tree.insert(5); + try tree.insert(15); + try tree.insert(3); + try tree.insert(20); + + const min = tree.minimum(null); + const max = tree.maximum(null); + + try std.testing.expect(min != null); + try std.testing.expect(max != null); + try std.testing.expectEqual(@as(i32, 3), min.?.data); + try std.testing.expectEqual(@as(i32, 20), max.?.data); +} + +test "RedBlackTree: iterator empty tree" { + const allocator = std.testing.allocator; + var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + defer tree.deinit(); + + var iter = try tree.iterator(); + defer iter.deinit(); + + const node = try iter.next(); + try std.testing.expect(node == null); +} + +test "RedBlackTree: clear" { + const allocator = std.testing.allocator; + var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + defer tree.deinit(); + + try tree.insert(1); + try tree.insert(2); + try tree.insert(3); + + tree.clear(); + try std.testing.expectEqual(@as(usize, 0), tree.count()); + try std.testing.expect(tree.root == null); +} + +test "RedBlackTree: negative numbers" { + const allocator = std.testing.allocator; + var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + defer tree.deinit(); + + try tree.insert(-10); + try tree.insert(-5); + try tree.insert(0); + try tree.insert(5); + + try std.testing.expectEqual(@as(usize, 4), tree.count()); + try std.testing.expect(tree.contains(-10)); + try std.testing.expect(tree.contains(0)); +} + +test "RedBlackTree: find returns correct node" { + const allocator = std.testing.allocator; + var tree = RedBlackTree(i32, DefaultContext(i32)).init(allocator, .{}); + defer tree.deinit(); + + try tree.insert(10); + try tree.insert(20); + + const node = tree.find(10); + try std.testing.expect(node != null); + try std.testing.expectEqual(@as(i32, 10), node.?.data); +} diff --git a/src/ordered/skip_list.zig b/src/ordered/skip_list.zig index b7142c2..8f00e89 100644 --- a/src/ordered/skip_list.zig +++ b/src/ordered/skip_list.zig @@ -311,21 +311,145 @@ test "SkipList: string keys" { var list = try SkipList([]const u8, i32, strCompare, 16).init(allocator); defer list.deinit(); - try list.put("banana", 2); try list.put("apple", 1); + try list.put("banana", 2); try list.put("cherry", 3); - try std.testing.expectEqual(@as(i32, 1), list.get("apple").?.*); try std.testing.expectEqual(@as(i32, 2), list.get("banana").?.*); - try std.testing.expectEqual(@as(i32, 3), list.get("cherry").?.*); + try std.testing.expect(list.contains("apple")); + try std.testing.expect(!list.contains("date")); +} + +test "SkipList: empty list operations" { + const allocator = std.testing.allocator; + var list = try SkipList(i32, i32, i32Compare, 16).init(allocator); + defer list.deinit(); + + try std.testing.expect(list.get(42) == null); + try std.testing.expectEqual(@as(usize, 0), list.len); + try std.testing.expect(list.delete(42) == null); + try std.testing.expect(!list.contains(42)); +} + +test "SkipList: single element" { + const allocator = std.testing.allocator; + var list = try SkipList(i32, i32, i32Compare, 16).init(allocator); + defer list.deinit(); + + try list.put(42, 100); + try std.testing.expectEqual(@as(usize, 1), list.len); + try std.testing.expectEqual(@as(i32, 100), list.get(42).?.*); + + const deleted = list.delete(42); + try std.testing.expectEqual(@as(i32, 100), deleted.?); + try std.testing.expectEqual(@as(usize, 0), list.len); +} + +test "SkipList: large dataset sequential" { + const allocator = std.testing.allocator; + var list = try SkipList(i32, i32, i32Compare, 16).init(allocator); + defer list.deinit(); + + var i: i32 = 0; + while (i < 100) : (i += 1) { + try list.put(i, i * 2); + } + + try std.testing.expectEqual(@as(usize, 100), list.len); + + i = 0; + while (i < 100) : (i += 1) { + try std.testing.expectEqual(i * 2, list.get(i).?.*); + } +} + +test "SkipList: reverse insertion" { + const allocator = std.testing.allocator; + var list = try SkipList(i32, i32, i32Compare, 16).init(allocator); + defer list.deinit(); + + var i: i32 = 50; + while (i > 0) : (i -= 1) { + try list.put(i, i); + } - // Test iteration maintains lexicographic order + try std.testing.expectEqual(@as(usize, 50), list.len); + + // Verify iteration is sorted var iter = list.iterator(); - const first = iter.next().?; - try std.testing.expectEqualStrings("apple", first.key); - const second = iter.next().?; - try std.testing.expectEqualStrings("banana", second.key); - const third = iter.next().?; - try std.testing.expectEqualStrings("cherry", third.key); - try std.testing.expect(iter.next() == null); + var prev: i32 = 0; + while (iter.next()) |entry| { + try std.testing.expect(entry.key > prev); + prev = entry.key; + } +} + +test "SkipList: delete non-existent" { + const allocator = std.testing.allocator; + var list = try SkipList(i32, i32, i32Compare, 16).init(allocator); + defer list.deinit(); + + try list.put(10, 10); + try list.put(20, 20); + + const deleted = list.delete(15); + try std.testing.expect(deleted == null); + try std.testing.expectEqual(@as(usize, 2), list.len); +} + +test "SkipList: delete all elements" { + const allocator = std.testing.allocator; + var list = try SkipList(i32, i32, i32Compare, 16).init(allocator); + defer list.deinit(); + + try list.put(1, 1); + try list.put(2, 2); + try list.put(3, 3); + + _ = list.delete(1); + _ = list.delete(2); + _ = list.delete(3); + + try std.testing.expectEqual(@as(usize, 0), list.len); + try std.testing.expect(list.get(2) == null); +} + +test "SkipList: negative keys" { + const allocator = std.testing.allocator; + var list = try SkipList(i32, i32, i32Compare, 16).init(allocator); + defer list.deinit(); + + try list.put(-10, 10); + try list.put(-5, 5); + try list.put(0, 0); + try list.put(5, -5); + + try std.testing.expectEqual(@as(i32, 10), list.get(-10).?.*); + try std.testing.expectEqual(@as(i32, -5), list.get(5).?.*); +} + +test "SkipList: getPtr mutation" { + const allocator = std.testing.allocator; + var list = try SkipList(i32, i32, i32Compare, 16).init(allocator); + defer list.deinit(); + + try list.put(10, 100); + + const ptr = list.getPtr(10); + try std.testing.expect(ptr != null); + ptr.?.* = 999; + + try std.testing.expectEqual(@as(i32, 999), list.get(10).?.*); +} + +test "SkipList: minimum max level" { + const allocator = std.testing.allocator; + var list = try SkipList(i32, i32, i32Compare, 1).init(allocator); + defer list.deinit(); + + try list.put(1, 1); + try list.put(2, 2); + try list.put(3, 3); + + try std.testing.expectEqual(@as(i32, 2), list.get(2).?.*); } diff --git a/src/ordered/sorted_set.zig b/src/ordered/sorted_set.zig index 8b8c7ec..4435ed7 100644 --- a/src/ordered/sorted_set.zig +++ b/src/ordered/sorted_set.zig @@ -73,3 +73,95 @@ test "SortedSet basic functionality" { _ = vec.remove(1); // Remove 75 try std.testing.expectEqualSlices(i32, &.{ 50, 100 }, vec.items.items); } + +test "SortedSet: empty set operations" { + const allocator = std.testing.allocator; + var vec = SortedSet(i32, i32Compare).init(allocator); + defer vec.deinit(); + + try std.testing.expect(!vec.contains(42)); + try std.testing.expectEqual(@as(?usize, null), vec.findIndex(42)); + try std.testing.expectEqual(@as(usize, 0), vec.items.items.len); +} + +test "SortedSet: single element" { + const allocator = std.testing.allocator; + var vec = SortedSet(i32, i32Compare).init(allocator); + defer vec.deinit(); + + try vec.add(42); + try std.testing.expect(vec.contains(42)); + try std.testing.expectEqual(@as(usize, 1), vec.items.items.len); + + const removed = vec.remove(0); + try std.testing.expectEqual(@as(i32, 42), removed); + try std.testing.expectEqual(@as(usize, 0), vec.items.items.len); +} + +test "SortedSet: duplicate values" { + const allocator = std.testing.allocator; + var vec = SortedSet(i32, i32Compare).init(allocator); + defer vec.deinit(); + + try vec.add(10); + try vec.add(10); + try vec.add(10); + + // Duplicates are allowed in this implementation + try std.testing.expectEqual(@as(usize, 3), vec.items.items.len); +} + +test "SortedSet: negative numbers" { + const allocator = std.testing.allocator; + var vec = SortedSet(i32, i32Compare).init(allocator); + defer vec.deinit(); + + try vec.add(-5); + try vec.add(-10); + try vec.add(0); + try vec.add(5); + + try std.testing.expectEqualSlices(i32, &.{ -10, -5, 0, 5 }, vec.items.items); +} + +test "SortedSet: large dataset" { + const allocator = std.testing.allocator; + var vec = SortedSet(i32, i32Compare).init(allocator); + defer vec.deinit(); + + // Insert in reverse order + var i: i32 = 100; + while (i >= 0) : (i -= 1) { + try vec.add(i); + } + + // Verify sorted + try std.testing.expectEqual(@as(usize, 101), vec.items.items.len); + for (vec.items.items, 0..) |val, idx| { + try std.testing.expectEqual(@as(i32, @intCast(idx)), val); + } +} + +test "SortedSet: remove boundary cases" { + const allocator = std.testing.allocator; + var vec = SortedSet(i32, i32Compare).init(allocator); + defer vec.deinit(); + + try vec.add(1); + try vec.add(2); + try vec.add(3); + try vec.add(4); + try vec.add(5); + + // Remove first + _ = vec.remove(0); + try std.testing.expectEqualSlices(i32, &.{ 2, 3, 4, 5 }, vec.items.items); + + // Remove last + _ = vec.remove(3); + try std.testing.expectEqualSlices(i32, &.{ 2, 3, 4 }, vec.items.items); + + // Remove middle + _ = vec.remove(1); + try std.testing.expectEqualSlices(i32, &.{ 2, 4 }, vec.items.items); +} diff --git a/src/ordered/trie.zig b/src/ordered/trie.zig index addb8ef..17e538d 100644 --- a/src/ordered/trie.zig +++ b/src/ordered/trie.zig @@ -252,25 +252,59 @@ pub fn Trie(comptime V: type) type { test "Trie: basic operations" { const allocator = std.testing.allocator; - var trie = try Trie([]const u8).init(allocator); + var trie = try Trie(i32).init(allocator); defer trie.deinit(); - try trie.put("cat", "feline"); - try trie.put("car", "vehicle"); - try trie.put("card", "playing card"); + try trie.put("hello", 1); + try trie.put("world", 2); + try trie.put("help", 3); try std.testing.expectEqual(@as(usize, 3), trie.len); - try std.testing.expectEqualStrings("feline", trie.get("cat").?.*); - try std.testing.expectEqualStrings("vehicle", trie.get("car").?.*); - try std.testing.expect(trie.get("ca") == null); + try std.testing.expectEqual(@as(i32, 1), trie.get("hello").?.*); + try std.testing.expectEqual(@as(i32, 3), trie.get("help").?.*); + try std.testing.expect(trie.get("bye") == null); +} - const deleted = trie.delete("car"); - try std.testing.expectEqualStrings("vehicle", deleted.?); - try std.testing.expect(trie.get("car") == null); - try std.testing.expectEqual(@as(usize, 2), trie.len); +test "Trie: empty trie operations" { + const allocator = std.testing.allocator; + var trie = try Trie(i32).init(allocator); + defer trie.deinit(); + + try std.testing.expect(trie.get("key") == null); + try std.testing.expectEqual(@as(usize, 0), trie.len); + try std.testing.expect(!trie.contains("key")); + try std.testing.expect(!trie.hasPrefix("pre")); } -test "Trie: prefix operations" { +test "Trie: single character keys" { + const allocator = std.testing.allocator; + var trie = try Trie(i32).init(allocator); + defer trie.deinit(); + + try trie.put("a", 1); + try trie.put("b", 2); + try trie.put("c", 3); + + try std.testing.expectEqual(@as(i32, 2), trie.get("b").?.*); + try std.testing.expect(trie.contains("a")); + try std.testing.expect(!trie.contains("d")); +} + +test "Trie: empty string key" { + const allocator = std.testing.allocator; + var trie = try Trie(i32).init(allocator); + defer trie.deinit(); + + try trie.put("", 42); + try std.testing.expectEqual(@as(usize, 1), trie.len); + try std.testing.expectEqual(@as(i32, 42), trie.get("").?.*); + + const deleted = trie.delete(""); + try std.testing.expectEqual(@as(i32, 42), deleted.?); + try std.testing.expectEqual(@as(usize, 0), trie.len); +} + +test "Trie: overlapping prefixes" { const allocator = std.testing.allocator; var trie = try Trie(i32).init(allocator); defer trie.deinit(); @@ -278,18 +312,145 @@ test "Trie: prefix operations" { try trie.put("test", 1); try trie.put("testing", 2); try trie.put("tester", 3); + try trie.put("tested", 4); + try std.testing.expectEqual(@as(usize, 4), trie.len); + try std.testing.expectEqual(@as(i32, 1), trie.get("test").?.*); + try std.testing.expectEqual(@as(i32, 2), trie.get("testing").?.*); + try std.testing.expect(trie.hasPrefix("tes")); try std.testing.expect(trie.hasPrefix("test")); - try std.testing.expect(trie.hasPrefix("te")); - try std.testing.expect(!trie.hasPrefix("xyz")); +} - var keys = try trie.keysWithPrefix(allocator, "test"); - defer { - for (keys.items) |key| { - allocator.free(key); - } - keys.deinit(allocator); +test "Trie: delete with shared prefixes" { + const allocator = std.testing.allocator; + var trie = try Trie(i32).init(allocator); + defer trie.deinit(); + + try trie.put("car", 1); + try trie.put("card", 2); + try trie.put("care", 3); + + const deleted = trie.delete("card"); + try std.testing.expectEqual(@as(i32, 2), deleted.?); + try std.testing.expectEqual(@as(usize, 2), trie.len); + try std.testing.expect(!trie.contains("card")); + try std.testing.expect(trie.contains("car")); + try std.testing.expect(trie.contains("care")); +} + +test "Trie: delete non-existent key" { + const allocator = std.testing.allocator; + var trie = try Trie(i32).init(allocator); + defer trie.deinit(); + + try trie.put("hello", 1); + + const deleted = trie.delete("world"); + try std.testing.expect(deleted == null); + try std.testing.expectEqual(@as(usize, 1), trie.len); +} + +test "Trie: delete prefix that is not a key" { + const allocator = std.testing.allocator; + var trie = try Trie(i32).init(allocator); + defer trie.deinit(); + + try trie.put("testing", 1); + + const deleted = trie.delete("test"); + try std.testing.expect(deleted == null); + try std.testing.expectEqual(@as(usize, 1), trie.len); + try std.testing.expect(trie.contains("testing")); +} + +test "Trie: update existing key" { + const allocator = std.testing.allocator; + var trie = try Trie(i32).init(allocator); + defer trie.deinit(); + + try trie.put("key", 100); + try std.testing.expectEqual(@as(usize, 1), trie.len); + + try trie.put("key", 200); + try std.testing.expectEqual(@as(usize, 1), trie.len); + try std.testing.expectEqual(@as(i32, 200), trie.get("key").?.*); +} + +test "Trie: hasPrefix with exact match" { + const allocator = std.testing.allocator; + var trie = try Trie(i32).init(allocator); + defer trie.deinit(); + + try trie.put("hello", 1); + + try std.testing.expect(trie.hasPrefix("hello")); + try std.testing.expect(trie.hasPrefix("hel")); + try std.testing.expect(trie.hasPrefix("h")); + try std.testing.expect(!trie.hasPrefix("helloo")); +} + +test "Trie: getPtr mutation" { + const allocator = std.testing.allocator; + var trie = try Trie(i32).init(allocator); + defer trie.deinit(); + + try trie.put("key", 100); + + const ptr = trie.getPtr("key"); + try std.testing.expect(ptr != null); + ptr.?.* = 999; + + try std.testing.expectEqual(@as(i32, 999), trie.get("key").?.*); +} + +test "Trie: many keys" { + const allocator = std.testing.allocator; + var trie = try Trie(i32).init(allocator); + defer trie.deinit(); + + const keys = [_][]const u8{ + "apple", "application", "apply", "banana", "band", + "can", "cancel", "cat", "dog", "door", + }; + + for (keys, 0..) |key, i| { + try trie.put(key, @intCast(i)); } - try std.testing.expectEqual(@as(usize, 3), keys.items.len); + try std.testing.expectEqual(@as(usize, 10), trie.len); + + for (keys, 0..) |key, i| { + try std.testing.expectEqual(@as(i32, @intCast(i)), trie.get(key).?.*); + } +} + +test "Trie: delete all keys" { + const allocator = std.testing.allocator; + var trie = try Trie(i32).init(allocator); + defer trie.deinit(); + + try trie.put("a", 1); + try trie.put("b", 2); + try trie.put("c", 3); + + _ = trie.delete("a"); + _ = trie.delete("b"); + _ = trie.delete("c"); + + try std.testing.expectEqual(@as(usize, 0), trie.len); + try std.testing.expect(!trie.hasPrefix("a")); +} + +test "Trie: special characters" { + const allocator = std.testing.allocator; + var trie = try Trie(i32).init(allocator); + defer trie.deinit(); + + try trie.put("hello-world", 1); + try trie.put("test_case", 2); + try trie.put("foo.bar", 3); + + try std.testing.expectEqual(@as(i32, 1), trie.get("hello-world").?.*); + try std.testing.expectEqual(@as(i32, 2), trie.get("test_case").?.*); + try std.testing.expectEqual(@as(i32, 3), trie.get("foo.bar").?.*); } diff --git a/test_arraylist.zig b/test_arraylist.zig deleted file mode 100644 index dd53156..0000000 --- a/test_arraylist.zig +++ /dev/null @@ -1,14 +0,0 @@ -const std = @import("std"); - -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); - - var list = std.ArrayList(i32).init(allocator); - defer list.deinit(); - - try list.append(42); - std.debug.print("Success! ArrayList works.\n", .{}); -} -