diff --git a/CMakeLists.txt b/CMakeLists.txt index 5b78df3..f54b1d1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,10 +22,14 @@ find_package(fmt CONFIG REQUIRED) find_package(nlohmann_json CONFIG REQUIRED) find_package(Boost 1.74.0 REQUIRED CONFIG COMPONENTS system) +# --- Tests (enables ctest for any subproject that calls add_test) --- +enable_testing() + # --- Subprojects --- # Each subdirectory defines its own targets and exposes them by name. # Order matters only when one subdir consumes targets from another: # bin/ links pg_lib and matching_engine_lib, so libraries come first. add_subdirectory(projects/matching_engine) +add_subdirectory(projects/ds) add_subdirectory(pg) add_subdirectory(bin) diff --git a/flake.nix b/flake.nix index 29c0ddb..9fde443 100644 --- a/flake.nix +++ b/flake.nix @@ -26,6 +26,7 @@ boost fmt nlohmann_json + doctest ]; shellHook = '' diff --git a/projects/ds/CMakeLists.txt b/projects/ds/CMakeLists.txt new file mode 100644 index 0000000..452a543 --- /dev/null +++ b/projects/ds/CMakeLists.txt @@ -0,0 +1,26 @@ +# ============================================================================= +# ds — header-only 資料結構練習場 (對應 rust 的 src/ds)。 +# +# - ds_lib 是 INTERFACE library,public headers 放在 include/ds/..., +# 消費端寫 #include ,include 路徑停在 include/。 +# - 每個結構旁邊在 tests/ 放一支 *_test.cpp,全部 GLOB 進單一 ds_tests +# 執行檔(doctest),掛上 ctest → `ctest` 就能一鍵全跑 (≈ cargo test)。 +# - doctest 由 nix flake 提供 (find_package),與其他相依一致。 +# ============================================================================= +cmake_minimum_required(VERSION 3.24) +project(ds) + +add_library(ds_lib INTERFACE) +target_include_directories(ds_lib INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_compile_features(ds_lib INTERFACE cxx_std_20) + +find_package(doctest CONFIG REQUIRED) + +# tests/ 下的 *_test.cpp + 唯一的 doctest main TU → 單一 ds_tests。 +file(GLOB_RECURSE DS_TEST_FILES CONFIGURE_DEPENDS tests/*_test.cpp) + +if(DS_TEST_FILES) + add_executable(ds_tests tests/ds_test_main.cpp ${DS_TEST_FILES}) + target_link_libraries(ds_tests PRIVATE ds_lib doctest::doctest) + add_test(NAME ds_tests COMMAND ds_tests) +endif() diff --git a/projects/ds/README.md b/projects/ds/README.md new file mode 100644 index 0000000..385cc56 --- /dev/null +++ b/projects/ds/README.md @@ -0,0 +1,67 @@ +# ds — 資料結構練習場 + +對應 Rust playground 的 `src/ds/`。header-only 實作 + doctest 測試, +`ctest` 一鍵全跑(≈ `cargo test`)。與 `projects/matching_engine` 同構: +public headers 在 `include/ds/...`,測試在 `tests/`。 + +## 結構 + +``` +projects/ds/ +├── CMakeLists.txt +├── include/ds/ +│ ├── ds.hpp # umbrella header(≈ rust 的 ds/mod.rs) +│ ├── tree/tree.hpp # ✅ BinarySearchTree —— 範例,已實作 +│ ├── graph/graph.hpp # 🚧 骨架,待實作 +│ ├── trie/trie.hpp # 🚧 骨架,待實作 +│ └── list/list.hpp # 🚧 骨架,待實作 +└── tests/ + ├── ds_test_main.cpp # 唯一定義 doctest main 的 TU,別動 + ├── tree_test.cpp # ✅ 對應 BST 的測試 + ├── graph_test.cpp # 🚧 skip() 跳過,待填 + ├── trie_test.cpp # 🚧 + └── list_test.cpp # 🚧 +``` + +消費端一律寫 `#include `(include 路徑停在 `include/`)。 + +## 跑測試 + +在 nix flake 環境裡(`direnv` 進目錄會自動載入,或前綴 `direnv exec .`): + +```bash +cmake -B build -G Ninja # 第一次 / CMakeLists 改過才需要 +cmake --build build --target ds_tests +ctest --test-dir build --output-on-failure +``` + +只跑某個結構: + +```bash +./build/projects/ds/ds_tests --test-case="*BST*" # doctest 過濾語法 +``` + +## 新增一個資料結構 + +1. 寫 `include/ds/xxx/xxx.hpp`(header-only 實作)。 +2. 在 `tests/xxx_test.cpp` 用 doctest 寫 `TEST_CASE` / `CHECK` / `REQUIRE`: + + ```cpp + #include + #include + + TEST_CASE("xxx: 做了什麼") { + ds::Xxx x; + CHECK(x.empty()); + } + ``` + +3. (可選)在 `include/ds/ds.hpp` 加一行 `#include `。 +4. 直接 build + ctest。`tests/*_test.cpp` 用 `GLOB_RECURSE ... CONFIGURE_DEPENDS` + 自動收進來,不用改 CMake。 + +## 骨架說明 + +`graph/` `trie/` `list/` 目前是空殼:header 只有 class 外形 + 預期 API 的 TODO, +測試是 `TEST_CASE(... * doctest::skip())` 先被跳過(所以 ctest 仍是綠的)。 +動手時把實作補進 header、把測試的 `* doctest::skip()` 拿掉再填內容即可。 diff --git a/projects/ds/include/ds/ds.hpp b/projects/ds/include/ds/ds.hpp new file mode 100644 index 0000000..4447806 --- /dev/null +++ b/projects/ds/include/ds/ds.hpp @@ -0,0 +1,10 @@ +#pragma once + +// Umbrella header for the data-structure playground (對應 rust 的 ds/mod.rs)。 +// 每新增一個資料結構,就在這裡 #include 它的 header,外部只要 #include +// 就能拿到全部。測試檔則各自 include 自己那一個即可。 + +#include +#include +#include +#include diff --git a/projects/ds/include/ds/graph/graph.hpp b/projects/ds/include/ds/graph/graph.hpp new file mode 100644 index 0000000..979cfcb --- /dev/null +++ b/projects/ds/include/ds/graph/graph.hpp @@ -0,0 +1,24 @@ +#pragma once + +// 無向圖 (adjacency list)。骨架而已,實作留給你。 +// +// 建議的 API(自己增刪): +// void add_edge(const T& a, const T& b); +// bool has_edge(const T& a, const T& b) const; +// std::vector neighbors(const T& v) const; +// std::size_t num_vertices() const; +// +// 建議的儲存結構:std::unordered_map> adj_; + +namespace ds { + +template +class Graph { +public: + // TODO: 你的 public API + +private: + // TODO: 你的儲存結構 +}; + +} // namespace ds diff --git a/projects/ds/include/ds/list/list.hpp b/projects/ds/include/ds/list/list.hpp new file mode 100644 index 0000000..4400d35 --- /dev/null +++ b/projects/ds/include/ds/list/list.hpp @@ -0,0 +1,26 @@ +#pragma once + +// 單向鏈結串列 (singly linked list)。骨架而已,實作留給你。 +// +// 建議的 API(自己增刪): +// void push_front(const T& value); +// void push_back(const T& value); +// bool empty() const; +// std::size_t size() const; +// std::vector to_vector() const; // 方便寫測試比對 +// +// 建議的節點:struct Node { T value; std::unique_ptr next; }; +// head_ 持有第一個節點,注意 unique_ptr 的所有權轉移。 + +namespace ds { + +template +class List { +public: + // TODO: 你的 public API + +private: + // TODO: 你的 Node 結構與 head_ / size_ +}; + +} // namespace ds diff --git a/projects/ds/include/ds/tree/tree.hpp b/projects/ds/include/ds/tree/tree.hpp new file mode 100644 index 0000000..ee362f9 --- /dev/null +++ b/projects/ds/include/ds/tree/tree.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include +#include + +namespace ds { + +// 一個最小的二元搜尋樹 (BST),header-only,當作 ds/ playground 的範例結構。 +// 重點不在效能,而在「實作 + tests/ 下的 tree_test.cpp + ctest 一鍵跑」的流程。 +template +class BinarySearchTree { +public: + void insert(const T& value) { root_ = insert(std::move(root_), value); } + + bool contains(const T& value) const { + const Node* cur = root_.get(); + while (cur) { + if (value < cur->value) { + cur = cur->left.get(); + } else if (cur->value < value) { + cur = cur->right.get(); + } else { + return true; + } + } + return false; + } + + // 中序走訪 → 由小到大的排序序列。 + std::vector inorder() const { + std::vector out; + inorder(root_.get(), out); + return out; + } + + std::size_t size() const { return size_; } + bool empty() const { return size_ == 0; } + +private: + struct Node { + explicit Node(T v) : value(std::move(v)) {} + T value; + std::unique_ptr left; + std::unique_ptr right; + }; + + std::unique_ptr insert(std::unique_ptr node, const T& value) { + if (!node) { + ++size_; + return std::make_unique(value); + } + if (value < node->value) { + node->left = insert(std::move(node->left), value); + } else if (node->value < value) { + node->right = insert(std::move(node->right), value); + } + // 相等 → 視為已存在,不重複插入。 + return node; + } + + void inorder(const Node* node, std::vector& out) const { + if (!node) return; + inorder(node->left.get(), out); + out.push_back(node->value); + inorder(node->right.get(), out); + } + + std::unique_ptr root_; + std::size_t size_ = 0; +}; + +} // namespace ds diff --git a/projects/ds/include/ds/trie/trie.hpp b/projects/ds/include/ds/trie/trie.hpp new file mode 100644 index 0000000..9c4617e --- /dev/null +++ b/projects/ds/include/ds/trie/trie.hpp @@ -0,0 +1,23 @@ +#pragma once + +// 前綴樹 (Trie),存字串。骨架而已,實作留給你。 +// +// 建議的 API(自己增刪): +// void insert(const std::string& word); +// bool contains(const std::string& word) const; // 完整單字 +// bool starts_with(const std::string& prefix) const; // 任意前綴 +// +// 建議的節點:每個 Node 有 children (例如 std::array, 26> +// 或 std::unordered_map>) 與 is_end 旗標。 + +namespace ds { + +class Trie { +public: + // TODO: 你的 public API + +private: + // TODO: 你的 Node 結構與 root_ +}; + +} // namespace ds diff --git a/projects/ds/tests/ds_test_main.cpp b/projects/ds/tests/ds_test_main.cpp new file mode 100644 index 0000000..51444f2 --- /dev/null +++ b/projects/ds/tests/ds_test_main.cpp @@ -0,0 +1,4 @@ +// 唯一一個定義 doctest main 的 translation unit。其餘 *_test.cpp 只 include +// 寫 TEST_CASE,連結到一起就組成單一的 ds_tests 執行檔。 +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include diff --git a/projects/ds/tests/graph_test.cpp b/projects/ds/tests/graph_test.cpp new file mode 100644 index 0000000..f3cae30 --- /dev/null +++ b/projects/ds/tests/graph_test.cpp @@ -0,0 +1,8 @@ +#include + +#include + +// 拿掉 doctest::skip() 之後就會被 ctest 跑到。先把實作寫好,再回來填測試。 +TEST_CASE("graph: add_edge / has_edge" * doctest::skip()) { + // TODO: 你的測試 +} diff --git a/projects/ds/tests/list_test.cpp b/projects/ds/tests/list_test.cpp new file mode 100644 index 0000000..b4eb4db --- /dev/null +++ b/projects/ds/tests/list_test.cpp @@ -0,0 +1,8 @@ +#include + +#include + +// 拿掉 doctest::skip() 之後就會被 ctest 跑到。先把實作寫好,再回來填測試。 +TEST_CASE("list: push_front / push_back / to_vector" * doctest::skip()) { + // TODO: 你的測試 +} diff --git a/projects/ds/tests/tree_test.cpp b/projects/ds/tests/tree_test.cpp new file mode 100644 index 0000000..96f292a --- /dev/null +++ b/projects/ds/tests/tree_test.cpp @@ -0,0 +1,36 @@ +#include + +#include + +#include + +TEST_CASE("BST: insert 後 contains 找得到") { + ds::BinarySearchTree bst; + CHECK(bst.empty()); + + bst.insert(5); + bst.insert(3); + bst.insert(8); + + CHECK(bst.size() == 3); + CHECK(bst.contains(5)); + CHECK(bst.contains(3)); + CHECK(bst.contains(8)); + CHECK_FALSE(bst.contains(42)); +} + +TEST_CASE("BST: 重複插入不會增加 size") { + ds::BinarySearchTree bst; + bst.insert(1); + bst.insert(1); + bst.insert(1); + CHECK(bst.size() == 1); +} + +TEST_CASE("BST: 中序走訪是排序好的") { + ds::BinarySearchTree bst; + for (int v : {5, 3, 8, 1, 4, 7, 9}) bst.insert(v); + + std::vector expected{1, 3, 4, 5, 7, 8, 9}; + CHECK(bst.inorder() == expected); +} diff --git a/projects/ds/tests/trie_test.cpp b/projects/ds/tests/trie_test.cpp new file mode 100644 index 0000000..cb695cf --- /dev/null +++ b/projects/ds/tests/trie_test.cpp @@ -0,0 +1,8 @@ +#include + +#include + +// 拿掉 doctest::skip() 之後就會被 ctest 跑到。先把實作寫好,再回來填測試。 +TEST_CASE("trie: insert / contains / starts_with" * doctest::skip()) { + // TODO: 你的測試 +} diff --git a/test.py b/test.py new file mode 100644 index 0000000..7118075 --- /dev/null +++ b/test.py @@ -0,0 +1,3 @@ +import orderbook_py +print(orderbook_py.OrderBook()) +