diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..1e9629c --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,6 @@ +* @lrleon +Examples/** @lrleon +Tests/** @lrleon +docs/** @lrleon +bin/** @lrleon +.github/** @lrleon diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..f7a1755 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,554 @@ +# GitHub Copilot Review Instructions for Aleph-w + +## Project Context +Aleph-w is a comprehensive C++20 library for graph algorithms and data structures. All code reviews must ensure correctness, performance, maintainability, and adherence to C++20 standards and best practices. + +--- + +## 1. C++ Language Standards & Best Practices + +### C++20 Standard (Primary) +- **MUST**: This project targets **C++20** as the primary standard +- **MUST**: Use `auto` for type deduction, range-based for loops, `nullptr`, `constexpr`, `override`, `final` +- **MUST**: Prefer `std::unique_ptr`/`std::shared_ptr` over raw pointers for ownership +- **MUST**: Use structured bindings for tuple/pair decomposition +- **MUST**: Use `std::optional` instead of sentinel values or nullable pointers where semantically clear +- **MUST**: Use `if constexpr` for compile-time branching in templates +- **MUST**: Use concepts to constrain template parameters (see section below) +- **SHOULD**: Use `std::span` for non-owning views of contiguous sequences +- **SHOULD**: Consider `std::variant` for type-safe unions +- **SHOULD**: Use fold expressions for variadic template expansion +- **SHOULD**: Use three-way comparison (spaceship operator `<=>`) for comparable types +- **SHOULD**: Use designated initializers for aggregate initialization clarity +- **NICE**: Use `std::format` for type-safe string formatting (prefer over printf/streams) +- **NICE**: Use `consteval` for functions that must be evaluated at compile-time +- **NICE**: Use `constinit` for static/thread_local variables requiring constant initialization + +### C++20 Concepts +- **MUST**: Use concepts to constrain template parameters for better error messages and documentation +- **MUST**: Prefer standard library concepts: `std::integral`, `std::floating_point`, `std::ranges::range`, `std::invocable`, etc. +- **SHOULD**: Define custom concepts for domain-specific constraints (e.g., `Graph`, `Node`, `WeightFunction`) +- **SHOULD**: Use `requires` clauses for complex constraints +- **NICE**: Use abbreviated function templates with concept constraints: `void foo(std::integral auto x)` + +**Example:** +```cpp +template +requires std::integral> +auto sum(const R& range) { ... } +``` + +### C++20 Ranges +- **SHOULD**: Use ranges library for composable, lazy algorithm pipelines +- **SHOULD**: Prefer `std::ranges::` algorithms over classic `std::` algorithms for better composability +- **SHOULD**: Use views (`std::views::filter`, `std::views::transform`, `std::views::take`, etc.) for non-owning, lazy transformations +- **NICE**: Chain range adaptors with pipe syntax: `container | views::filter(...) | views::transform(...)` + +**Example:** +```cpp +auto even_squares = numbers + | std::views::filter([](int n) { return n % 2 == 0; }) + | std::views::transform([](int n) { return n * n; }); +``` + +### C++20 Coroutines (if applicable) +- **SHOULD**: Consider coroutines for asynchronous graph traversals or generators +- **SHOULD**: Use `co_await`, `co_yield`, `co_return` appropriately +- **NICE**: Implement custom generators for lazy graph traversals + +### C++20 Modules (future consideration) +- **NICE**: Consider migrating to modules when toolchain support is stable +- **NICE**: Modules improve compile times and eliminate header-only issues + +### RAII and Resource Management +- **MUST**: All resources (memory, file handles, locks) must follow RAII principles +- **MUST**: Constructors acquire resources, destructors release them +- **MUST**: Use standard smart pointers or custom RAII wrappers +- **MUST**: Never call `delete` manually; use smart pointers +- **MUST**: Ensure exception safety (basic, strong, or no-throw guarantee) +- **SHOULD**: Prefer stack allocation over heap when possible +- **SHOULD**: Document ownership semantics in comments + +### Const-Correctness +- **MUST**: Mark all non-modifying member functions as `const` +- **MUST**: Use `const` references for input parameters that won't be modified +- **MUST**: Use `const` for variables that don't change after initialization +- **SHOULD**: Prefer `const` iterators when not modifying container elements +- **SHOULD**: Use `constexpr` for compile-time constants and functions +- **SHOULD**: Use `consteval` (C++20) for functions that *must* be compile-time evaluated +- **SHOULD**: Use `constinit` (C++20) for static/thread_local variables requiring compile-time initialization +- **NICE**: Consider `mutable` for internal caching in `const` methods (document why) + +### Move Semantics and Copy Elision +- **MUST**: Implement move constructors and move assignment operators for heavy objects +- **MUST**: Mark move operations as `noexcept` when possible +- **SHOULD**: Use `std::move` when transferring ownership +- **SHOULD**: Return by value to enable copy elision (NRVO/RVO) +- **SHOULD**: Avoid unnecessary copies; use references or move semantics +- **NICE**: Consider rule of five (or zero) for resource-managing classes + +--- + +## 2. Algorithms & Complexity Analysis + +### Algorithmic Correctness +- **MUST**: Verify algorithm correctness for edge cases: empty inputs, single element, duplicates +- **MUST**: Check boundary conditions and off-by-one errors +- **MUST**: Ensure graph algorithms handle disconnected graphs, self-loops, and multi-edges +- **MUST**: Verify termination conditions in loops and recursive algorithms +- **SHOULD**: Consider numerical stability for floating-point arithmetic +- **SHOULD**: Validate input preconditions (e.g., non-negative weights for Dijkstra) + +### Time and Space Complexity +- **MUST**: Document time complexity using Big-O notation in Doxygen comments +- **MUST**: Document space complexity for recursive algorithms (call stack depth) +- **MUST**: Flag algorithms with worse than expected complexity (e.g., O(n²) when O(n log n) is achievable) +- **SHOULD**: Prefer O(log n) lookups (maps/sets) over O(n) scans when appropriate +- **SHOULD**: Avoid unnecessary copies that increase complexity by a factor +- **SHOULD**: Consider amortized complexity for data structures with occasional expensive operations + +### Common Algorithm Classes +- **Graph Traversal**: BFS, DFS, topological sort + - Verify visited set handling + - Check cycle detection logic + - Ensure proper parent/predecessor tracking +- **Shortest Paths**: Dijkstra, Bellman-Ford, Floyd-Warshall, A* + - Verify priority queue usage (min-heap) + - Check for negative cycle handling + - Ensure path reconstruction correctness +- **Minimum Spanning Trees**: Kruskal, Prim + - Verify union-find implementation (path compression, union by rank) + - Check edge weight comparison and sorting +- **Network Flow**: Ford-Fulkerson, Edmonds-Karp, Dinic + - Verify residual graph updates + - Check for augmenting path termination +- **Dynamic Programming**: Check memoization, subproblem definition, and recurrence relations +- **Greedy Algorithms**: Verify correctness of greedy choice property + +### Data Structures +- **Trees**: BST, AVL, Red-Black, B-trees + - Verify balance invariants + - Check rotation logic + - Ensure parent pointer updates (if used) +- **Heaps**: Binary heap, Fibonacci heap, binomial heap + - Verify heap property maintenance + - Check merge/decrease-key operations +- **Hash Tables**: Check collision handling (chaining, open addressing) +- **Union-Find**: Ensure path compression and union by rank/size +- **Graphs**: Adjacency list vs. matrix tradeoffs; directed vs. undirected handling + +--- + +## 3. Performance & Optimization + +### Performance Considerations +- **MUST**: Avoid premature optimization; prioritize correctness and readability first +- **MUST**: Use profiling data to identify bottlenecks before optimizing +- **SHOULD**: Reserve capacity for vectors/strings when final size is known +- **SHOULD**: Use `emplace_back` instead of `push_back` to avoid copies +- **SHOULD**: Prefer `++i` over `i++` for iterators (avoid temporary) +- **SHOULD**: Avoid repeated lookups; cache iterators/references +- **SHOULD**: Use `std::unordered_map`/`std::unordered_set` when ordering is not needed +- **NICE**: Consider cache locality for performance-critical inner loops +- **NICE**: Use `constexpr` to move computations to compile-time + +### Memory Efficiency +- **MUST**: Avoid memory leaks; verify with sanitizers (ASan, LSan) +- **MUST**: Use appropriate container sizes (reserve, shrink_to_fit) +- **SHOULD**: Consider space-time tradeoffs (memoization vs. recomputation) +- **SHOULD**: Use bit-packing or compact representations for large datasets +- **NICE**: Profile memory usage for large-scale graph algorithms + +--- + +## 4. Thread Safety & Concurrency + +### Concurrency Primitives +- **MUST**: Use `std::mutex`, `std::lock_guard`, `std::unique_lock` for synchronization +- **MUST**: Avoid data races; verify with ThreadSanitizer (TSan) +- **MUST**: Document thread-safety guarantees in class/function comments +- **MUST**: Use `std::atomic` for lock-free shared state (document memory ordering) +- **SHOULD**: Prefer RAII lock guards over manual lock/unlock +- **SHOULD**: Minimize critical section duration +- **SHOULD**: Avoid deadlocks using lock ordering or `std::scoped_lock` (C++17) +- **NICE**: Consider reader-writer locks (`std::shared_mutex`) for read-heavy workloads + +### Parallel Algorithms +- **MUST**: Verify absence of race conditions in parallel graph traversals +- **MUST**: Ensure proper synchronization for shared data structures +- **SHOULD**: Consider work-stealing or task-based parallelism for load balancing +- **SHOULD**: Document whether algorithms are thread-safe, reentrant, or require external synchronization + +--- + +## 5. Templates & Generic Programming + +### Template Design (C++20 Focus) +- **MUST**: Use **concepts** (C++20) to constrain template parameters; avoid SFINAE when possible +- **MUST**: Provide clear error messages for template instantiation failures (concepts help here) +- **SHOULD**: Use type traits (`std::is_same`, `std::type_identity`, etc.) for compile-time dispatch when concepts are insufficient +- **SHOULD**: Prefer templates over macros for type-safe generic code +- **SHOULD**: Document template parameter requirements in Doxygen comments (concepts serve as self-documentation) +- **SHOULD**: Use abbreviated function templates with concepts: `void foo(std::integral auto x)` +- **NICE**: Use variadic templates for flexible interfaces + +### Template Metaprogramming (C++20 Simplifications) +- **SHOULD**: Keep metaprogramming readable; avoid overly complex TMP +- **MUST**: Use `constexpr`/`consteval` functions instead of TMP when possible (C++20 makes this much more powerful) +- **SHOULD**: Use concepts to replace complex SFINAE-based metaprogramming +- **NICE**: Consider template specialization for type-specific optimizations when concepts are insufficient + +--- + +## 6. STL and Standard Library Usage + +### Aleph Containers vs. Standard Library (CRITICAL RULE) +- **MUST**: **ALWAYS use Aleph containers when an equivalent exists. Standard library containers are ONLY permitted when no Aleph equivalent is available.** + - This is a strict library policy, not a suggestion or preference + - Aleph containers are optimized for graph algorithms and provide domain-specific functionality + +#### Mandatory Aleph Container Usage (DO NOT use STL equivalents) +- **MUST**: Use these Aleph containers instead of their STL counterparts: + - `DynList` → **NEVER** `std::list` or `std::vector` (dynamic list with rich functional API) + - `DynSetTree` → **NEVER** `std::set` (ordered set with graph-specific operations) + - `DynMapTree` → **NEVER** `std::map` (ordered map) + - `OLhashTable` → **NEVER** `std::unordered_map` (open addressing linear hash table, best hash table implementation) + - `ArrayQueue`, `DynListQueue` → **NEVER** `std::queue` + - `ArrayStack`, `DynListStack` → **NEVER** `std::stack` + - Aleph heap implementations → **NEVER** `std::priority_queue` + - `SkipList` → probabilistic data structure (no direct STL equivalent) + +#### Hash Tables: Prefer OLhashTable +- **MUST**: Use `OLhashTable` as the default hash table implementation + - OLhashTable is the preferred Aleph hash table (open addressing, linear probing) + - Other Aleph hash tables exist but OLhashTable is recommended for general use + - **NEVER** use `std::unordered_map` or `std::unordered_set` when a hash table is needed + +#### ONLY Acceptable Use of Standard Library Containers +- **MUST**: Use standard library containers **ONLY** when: + 1. **No equivalent Aleph container exists** for the required data structure + 2. **Interfacing with external libraries** that explicitly require STL types as part of their API contract (e.g., third-party library function signatures) + 3. **`std::vector` as return type for functional programming operations** where: + - The number of elements is **known a priori** (can be calculated before insertion) + - Memory can be **pre-reserved** using `reserve()` before populating + - Compactness and memory layout efficiency are critical + - Example: `map()`, `filter()`, `zip()`, or similar functional operations where output size is deterministic + - **MUST**: Always call `reserve(expected_size)` before insertions when using this exception + +- **IMPORTANT**: The following are **NOT** valid reasons to use STL containers: + - ❌ "STL might be faster" (use Aleph containers regardless) + - ❌ "STL is more familiar" (learn Aleph containers) + - ❌ "STL has better documentation" (consult Aleph headers and examples) + - ❌ "Easier to integrate with std algorithms" (use Aleph containers and convert if absolutely necessary) + - ❌ "Memory efficiency concerns" (use Aleph containers unless this is the functional programming return case above) + +- **SHOULD**: Familiarize with Aleph container APIs before suggesting STL alternatives +- **SHOULD**: Check header files (`htlist.H`, `tpl_dynSetTree.H`, `tpl_dynMapTree.H`, etc.) for available Aleph containers + +### Standard Library Container Selection (when Aleph containers are not applicable) +- **MUST**: Choose appropriate containers based on access patterns and complexity requirements + - `std::vector`: random access, cache-friendly, dynamic array + - `std::deque`: double-ended queue, stable references + - `std::list`: stable iterators, frequent insertions/deletions + - `std::map`/`std::set`: ordered, O(log n) operations + - `std::unordered_map`/`std::unordered_set`: unordered, O(1) average operations +- **SHOULD**: Prefer `std::array` over C-style arrays when size is known at compile-time +- **SHOULD**: Use `std::priority_queue` for heaps only if Aleph heaps are not suitable + +### Algorithm Usage (C++20 Ranges Preferred) +- **MUST**: Use algorithms over hand-rolled loops for readability and correctness +- **SHOULD**: Prefer `std::ranges::` algorithms (`std::ranges::sort`, `std::ranges::find`, etc.) over classic `std::` algorithms +- **SHOULD**: Use range views for composable, lazy transformations +- **SHOULD**: Use range-based for loops for simple iterations +- **SHOULD**: Consider parallel algorithms with execution policies (`std::execution::par`, `std::execution::par_unseq`) for performance-critical code +- **NICE**: Chain range adaptors for expressive, functional-style code + +--- + +## 7. Error Handling + +### Aleph Exception Macros (CRITICAL RULE) +- **MUST**: **ALWAYS use Aleph exception macros from `ah-errors.H` instead of raw `throw` statements** + - Aleph macros automatically include file location (`file:line`) in error messages + - Provides consistent error reporting across the library + - Supports stream-style error message composition + +- **MUST**: Use these Aleph exception macros (defined in `ah-errors.H`): + - `ah_domain_error_if(condition)` → for domain/logic errors (replaces `throw std::domain_error`) + - `ah_runtime_error_if(condition)` → for runtime errors (replaces `throw std::runtime_error`) + - `ah_range_error_if(condition)` → for out-of-range errors (replaces `throw std::range_error`) + - `ah_invalid_argument_if(condition)` → for invalid arguments (replaces `throw std::invalid_argument`) + - Unconditional versions: `ah_domain_error()`, `ah_runtime_error()`, `ah_range_error()`, `ah_invalid_argument()` + - `_unless` variants: `ah_domain_error_unless(condition)`, etc. (inverted logic) + - `ah_fatal_error()` → for unrecoverable fatal errors + +- **Example (Aleph style - CORRECT)**: + ```cpp + ah_domain_error_if(x < 0) << "sqrt requires non-negative value, got " << x; + ah_range_error_if(index >= size) << "Index " << index << " out of range [0, " << size << ")"; + ah_runtime_error_unless(file.is_open()) << "Failed to open file: " << filename; + ``` + +- **Example (Raw throw - INCORRECT, DO NOT USE)**: + ```cpp + if (x < 0) // ❌ DO NOT DO THIS + throw std::domain_error("sqrt requires non-negative value"); + ``` + +### Exception Safety +- **MUST**: Document exception safety guarantees (no-throw, strong, basic) +- **MUST**: Use RAII to ensure cleanup on exception +- **MUST**: Catch exceptions by const reference: `catch (const std::exception& e)` +- **SHOULD**: Mark functions as `noexcept` when they guarantee no exceptions +- **NICE**: Avoid exceptions in destructors and move operations + +### Assertions and Preconditions +- **MUST**: Use assertions (`assert`, custom `ah-errors.H` macros) for preconditions +- **MUST**: Validate external inputs before processing +- **SHOULD**: Document preconditions in Doxygen `@pre` tags +- **SHOULD**: Fail fast with clear error messages + +--- + +## 8. Code Quality & Maintainability + +### Readability +- **MUST**: Use descriptive variable and function names +- **MUST**: Keep functions short and focused (single responsibility) +- **MUST**: Avoid deep nesting (max 3-4 levels); extract functions or use early returns +- **SHOULD**: Prefer explicit over implicit (avoid type conversions that hide intent) +- **SHOULD**: Avoid magic numbers; use named constants or enums +- **SHOULD**: Use whitespace and formatting consistently + +### Code Duplication +- **MUST**: Refactor duplicated code into reusable functions or templates +- **SHOULD**: Use inheritance or composition to share behavior +- **SHOULD**: Avoid copy-paste programming + +### Cyclomatic Complexity +- **MUST**: Keep cyclomatic complexity low (< 10 per function ideally) +- **SHOULD**: Break complex functions into smaller helpers +- **SHOULD**: Use lookup tables or polymorphism instead of long switch/if chains + +--- + +## 9. Documentation (Doxygen) + +### Required Documentation +- **MUST**: All headers must have a Doxygen file-level comment with: + - `@file` directive + - Brief description (`@brief`) + - MIT license header + - Author and date if applicable +- **MUST**: All public classes must have `@brief` and detailed description +- **MUST**: **If a new public method or class is added, it MUST be fully documented with Doxygen.** +- **MUST**: All public member functions must have: + - `@brief` description + - `@param` for each parameter + - `@return` if function returns a value + - `@throws` if function can throw exceptions + - Time/space complexity in `@note` or description +- **MUST**: Document preconditions (`@pre`) and postconditions (`@post`) when relevant +- **SHOULD**: Document thread-safety guarantees +- **SHOULD**: Provide usage examples in `@code` blocks for complex APIs + +### Documentation Quality +- **MUST**: Doxygen must generate no warnings for changed files +- **MUST**: Use proper English grammar and punctuation +- **SHOULD**: Keep descriptions concise but complete +- **SHOULD**: Cross-reference related classes/functions with `@see` + +--- + +## 10. Testing Requirements + +### Test Coverage +- **MUST**: **If a new public method or class is added, a corresponding unit test MUST be created.** + - The test must verify the method's contract (preconditions, postconditions, return values). + - The test must be added to the appropriate test file in `Tests/` (e.g., `Tests/ClassName_test.cc`). +- **MUST**: If headers are modified, corresponding tests must be added or updated +- **MUST**: Tests must cover edge cases: empty inputs, single element, large inputs +- **MUST**: Tests must verify algorithmic correctness, not just absence of crashes +- **SHOULD**: Include performance regression tests for critical algorithms +- **SHOULD**: Use property-based testing or randomized tests for complex algorithms + +### Test Quality +- **MUST**: Tests must be deterministic and repeatable +- **MUST**: Tests must have clear failure messages +- **SHOULD**: Use GTest framework conventions (ASSERT_*, EXPECT_*) +- **SHOULD**: Organize tests by functionality (setup, action, assertion) + +--- + +## 11. License & Legal + +### MIT License +- **MUST**: All `.H` header files must include the MIT license header at the top +- **MUST**: License header must include copyright notice +- **SHOULD**: Keep license text consistent with `LICENSE` file + +--- + +## 12. Code Review Checklist + +When reviewing a pull request, verify the following: + +### Correctness +- [ ] Algorithm logic is correct for all cases (empty, single, typical, edge) +- [ ] No off-by-one errors or boundary issues +- [ ] Proper handling of nullptr, invalid inputs, and error conditions +- [ ] No undefined behavior (uninitialized variables, dangling pointers, data races) + +### Performance +- [ ] Time complexity is as expected and documented +- [ ] No unnecessary copies or allocations in hot paths +- [ ] Appropriate data structures and algorithms selected + +### C++ Best Practices +- [ ] RAII for all resource management +- [ ] Const-correctness throughout +- [ ] Move semantics used where beneficial +- [ ] Smart pointers for ownership, raw pointers only for observation +- [ ] Exception-safe code with proper RAII cleanup + +### Concurrency (if applicable) +- [ ] No data races (verify with TSan) +- [ ] Proper synchronization primitives used +- [ ] Deadlock-free lock ordering +- [ ] Thread-safety documented + +### Documentation +- [ ] All public APIs documented with Doxygen +- [ ] New methods/classes fully documented +- [ ] Complexity analysis included +- [ ] Preconditions and postconditions documented +- [ ] No Doxygen warnings for changed files +- [ ] MIT license header present in all `.H` files + +### Testing +- [ ] Tests added/updated for modified headers +- [ ] New public methods have corresponding unit tests +- [ ] Edge cases covered in tests +- [ ] Tests pass locally and in CI + +### Style & Maintainability +- [ ] Code follows project conventions +- [ ] Descriptive names, no magic numbers +- [ ] Low cyclomatic complexity +- [ ] No code duplication +- [ ] Clear separation of concerns + +--- + +## 13. Common Issues to Flag + +### Critical Issues (Block Merge) +- Memory leaks or undefined behavior +- Data races or deadlocks +- Incorrect algorithm logic or complexity +- Missing Doxygen documentation for new or modified code +- Missing MIT license header in `.H` files +- Test failures or missing tests for header changes/new features + +### Important Issues (Should Fix) +- Non-const member functions that should be const +- Raw pointers where smart pointers should be used +- Missing move semantics for heavy objects +- Inefficient algorithms or data structures +- Missing `noexcept` on move operations +- Poor error handling or exception safety + +### Style Issues (Nice to Fix) +- Long functions that should be split +- Magic numbers instead of named constants +- Inconsistent naming conventions +- Overly complex nested logic +- Code duplication + +--- + +## 14. C++20 Feature Checklist + +When reviewing code, actively look for opportunities to use C++20 features: + +### Concepts +- [ ] Template parameters constrained with concepts instead of SFINAE +- [ ] Standard library concepts used (`std::integral`, `std::ranges::range`, etc.) +- [ ] Custom concepts defined for domain constraints + +### Ranges +- [ ] `std::ranges::` algorithms used instead of classic algorithms where appropriate +- [ ] Range views used for lazy transformations +- [ ] Pipe syntax for composable range operations + +### Modern Language Features +- [ ] Three-way comparison (`<=>`) for comparable types +- [ ] Designated initializers for clarity in aggregate initialization +- [ ] `std::span` for non-owning sequence views +- [ ] `std::format` for type-safe formatting +- [ ] `consteval`/`constinit` where appropriate + +### Performance +- [ ] Parallel execution policies considered for large datasets +- [ ] Range views used to avoid intermediate containers + +--- + +## 15. Review Tone & Format + +- Be constructive and specific in feedback +- Reference line numbers and code snippets +- Suggest concrete improvements with examples +- Explain *why* a change improves the code (performance, safety, maintainability) +- Acknowledge good practices and clever solutions +- Use severity labels: **CRITICAL**, **IMPORTANT**, **NICE**, **QUESTION** + +--- + +## Examples of Good Feedback + +### Example 1: Algorithm Complexity +``` +**IMPORTANT**: Line 42: This nested loop results in O(n²) complexity. +Consider using std::unordered_set for O(1) lookups, reducing to O(n). + +Suggested change: +std::unordered_set seen; +for (const auto& item : items) { + if (seen.count(item)) return true; + seen.insert(item); +} +``` + +### Example 2: Const-Correctness +``` +**IMPORTANT**: Line 87: Method `get_size()` doesn't modify state; mark as const. + +Suggested change: +-size_t get_size() { return size; } ++size_t get_size() const { return size; } +``` + +### Example 3: Documentation +``` +**CRITICAL**: Missing Doxygen documentation for public method `find_shortest_path`. + +Add: +/** @brief Finds the shortest path between source and target using Dijkstra's algorithm. + * @param source Starting node + * @param target Destination node + * @return Vector of nodes forming the path, or empty if no path exists + * @throws std::invalid_argument if source or target not in graph + * @note Time complexity: O((V + E) log V), Space: O(V) + */ +``` + +--- + +## Priority Levels + +- **MUST**: Mandatory requirement; blocking issue if violated +- **SHOULD**: Strongly recommended; may block merge for repeated violations +- **NICE**: Optional improvement; enhances code quality but not required + +Focus on **MUST** items first, then **SHOULD**, then **NICE** to keep reviews efficient. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..cc7d323 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,555 @@ +name: CI + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-test: + name: build-and-test (${{ matrix.os }}, ${{ matrix.compiler }}, ${{ matrix.build_type }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + + strategy: + fail-fast: false + matrix: + os: [ubuntu-22.04, ubuntu-24.04] + compiler: [gcc, clang] + build_type: [Debug, Release] + + env: + CCACHE_DIR: ${{ github.workspace }}/.ccache + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install base system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + cmake ninja-build pkg-config ccache git ruby-full \ + libgmp-dev libmpfr-dev libgsl-dev libx11-dev \ + libtclap-dev \ + nlohmann-json3-dev \ + libgtest-dev + + - name: Install GCC toolchain + if: matrix.compiler == 'gcc' + run: | + sudo apt-get install -y g++ + + - name: Install Clang toolchain + if: matrix.compiler == 'clang' + run: | + sudo apt-get install -y clang g++ + + - name: Configure compiler + run: | + if [ "${{ matrix.compiler }}" = "clang" ]; then + echo "CC=clang" >> "$GITHUB_ENV" + echo "CXX=clang++" >> "$GITHUB_ENV" + else + echo "CC=gcc" >> "$GITHUB_ENV" + echo "CXX=g++" >> "$GITHUB_ENV" + fi + + - name: Detect gcc-install-dir for Clang + run: | + if [ "${{ matrix.compiler }}" = "clang" ]; then + GCC_INSTALL_DIR="$( + ls -d /usr/lib/gcc/*/* 2>/dev/null | while read -r d; do + ver="$(basename "$d")" + if [ -d "/usr/include/c++/$ver" ]; then + echo "$d" + fi + done | sort -V | tail -n 1 + )" + + if [ -z "$GCC_INSTALL_DIR" ]; then + echo "Unable to detect a usable gcc-install-dir for clang" + exit 1 + fi + + if "${CXX:-clang++}" --help 2>/dev/null | grep -q -- '--gcc-install-dir'; then + echo "CLANG_GCC_CMAKE_ARG=-DUCONV_CLANG_GCC_INSTALL_DIR=$GCC_INSTALL_DIR" >> "$GITHUB_ENV" + echo "Using clang --gcc-install-dir: $GCC_INSTALL_DIR" + else + echo "CLANG_GCC_CMAKE_ARG=" >> "$GITHUB_ENV" + echo "Clang does not expose --gcc-install-dir; continuing without explicit GCC hint" + fi + else + echo "CLANG_GCC_CMAKE_ARG=" >> "$GITHUB_ENV" + fi + + - name: Debug Clang toolchain hint + if: matrix.compiler == 'clang' + run: | + echo "clang binary: $(command -v "${CXX:-clang++}")" + "${CXX:-clang++}" --version | head -n 2 + echo "CLANG_GCC_CMAKE_ARG='${CLANG_GCC_CMAKE_ARG}'" + + - name: Checkout Aleph-w dependency + run: | + git clone --depth 1 https://github.com/lrleon/Aleph-w.git deps/Aleph-w + + - name: Build Aleph static library + run: | + cmake -S deps/Aleph-w -B deps/Aleph-w/build -G Ninja \ + -DCMAKE_C_COMPILER="${CC}" \ + -DCMAKE_CXX_COMPILER="${CXX}" \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTS=OFF \ + -DBUILD_EXAMPLES=OFF + cmake --build deps/Aleph-w/build --target Aleph --parallel + ALEPH_LIB="$(find deps/Aleph-w/build -name libAleph.a | head -n 1)" + test -n "$ALEPH_LIB" + cp "$ALEPH_LIB" deps/Aleph-w/libAleph.a + ls -l deps/Aleph-w/libAleph.a + echo "ALEPHW_DIR=${GITHUB_WORKSPACE}/deps/Aleph-w" >> "$GITHUB_ENV" + + - name: Cache ccache + uses: actions/cache@v4 + with: + path: ${{ env.CCACHE_DIR }} + key: ${{ runner.os }}-${{ matrix.os }}-${{ matrix.compiler }}-${{ matrix.build_type }}-ccache-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-${{ matrix.os }}-${{ matrix.compiler }}-${{ matrix.build_type }}-ccache- + ${{ runner.os }}-${{ matrix.os }}-${{ matrix.compiler }}-ccache- + ${{ runner.os }}-${{ matrix.os }}-ccache- + + - name: Configure + run: | + rm -rf build + cmake -S . -B build -G Ninja \ + -DCMAKE_C_COMPILER="${CC}" \ + -DCMAKE_CXX_COMPILER="${CXX}" \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \ + -DALEPHW="${ALEPHW_DIR}" \ + -DJSON_PATH=/usr \ + -DENABLE_SANITIZERS=OFF \ + -DBUILD_DOCS=OFF \ + ${CLANG_GCC_CMAKE_ARG} + + - name: Build + run: | + cmake --build build --parallel + + - name: Test + env: + ASAN_OPTIONS: detect_leaks=0 + run: | + ctest --test-dir build --output-on-failure --parallel 2 --timeout 600 + + - name: Upload test logs on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: ctest-logs-${{ matrix.os }}-${{ matrix.compiler }}-${{ matrix.build_type }} + path: | + build/Testing/Temporary/LastTest.log + build/CMakeCache.txt + + sanitizers: + name: sanitizers (ubuntu-24.04, ${{ matrix.compiler }}, Debug) + runs-on: ubuntu-24.04 + timeout-minutes: 45 + + strategy: + fail-fast: false + matrix: + compiler: [gcc, clang] + + env: + CCACHE_DIR: ${{ github.workspace }}/.ccache + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + cmake ninja-build pkg-config ccache git ruby-full \ + libgmp-dev libmpfr-dev libgsl-dev libx11-dev \ + libtclap-dev \ + nlohmann-json3-dev \ + libgtest-dev + + - name: Install GCC toolchain + if: matrix.compiler == 'gcc' + run: | + sudo apt-get install -y g++ + + - name: Install Clang toolchain + if: matrix.compiler == 'clang' + run: | + sudo apt-get install -y clang g++ + + - name: Configure compiler + run: | + if [ "${{ matrix.compiler }}" = "clang" ]; then + echo "CC=clang" >> "$GITHUB_ENV" + echo "CXX=clang++" >> "$GITHUB_ENV" + else + echo "CC=gcc" >> "$GITHUB_ENV" + echo "CXX=g++" >> "$GITHUB_ENV" + fi + + - name: Detect gcc-install-dir for Clang + run: | + if [ "${{ matrix.compiler }}" = "clang" ]; then + GCC_INSTALL_DIR="$( + ls -d /usr/lib/gcc/*/* 2>/dev/null | while read -r d; do + ver="$(basename "$d")" + if [ -d "/usr/include/c++/$ver" ]; then + echo "$d" + fi + done | sort -V | tail -n 1 + )" + + if [ -z "$GCC_INSTALL_DIR" ]; then + echo "Unable to detect a usable gcc-install-dir for clang" + exit 1 + fi + + if "${CXX:-clang++}" --help 2>/dev/null | grep -q -- '--gcc-install-dir'; then + echo "CLANG_GCC_CMAKE_ARG=-DUCONV_CLANG_GCC_INSTALL_DIR=$GCC_INSTALL_DIR" >> "$GITHUB_ENV" + echo "Using clang --gcc-install-dir: $GCC_INSTALL_DIR" + else + echo "CLANG_GCC_CMAKE_ARG=" >> "$GITHUB_ENV" + echo "Clang does not expose --gcc-install-dir; continuing without explicit GCC hint" + fi + else + echo "CLANG_GCC_CMAKE_ARG=" >> "$GITHUB_ENV" + fi + + - name: Debug Clang toolchain hint + if: matrix.compiler == 'clang' + run: | + echo "clang binary: $(command -v "${CXX:-clang++}")" + "${CXX:-clang++}" --version | head -n 2 + echo "CLANG_GCC_CMAKE_ARG='${CLANG_GCC_CMAKE_ARG}'" + + - name: Checkout Aleph-w dependency + run: | + git clone --depth 1 https://github.com/lrleon/Aleph-w.git deps/Aleph-w + + - name: Build Aleph static library + run: | + cmake -S deps/Aleph-w -B deps/Aleph-w/build -G Ninja \ + -DCMAKE_C_COMPILER="${CC}" \ + -DCMAKE_CXX_COMPILER="${CXX}" \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTS=OFF \ + -DBUILD_EXAMPLES=OFF + cmake --build deps/Aleph-w/build --target Aleph --parallel + ALEPH_LIB="$(find deps/Aleph-w/build -name libAleph.a | head -n 1)" + test -n "$ALEPH_LIB" + cp "$ALEPH_LIB" deps/Aleph-w/libAleph.a + ls -l deps/Aleph-w/libAleph.a + echo "ALEPHW_DIR=${GITHUB_WORKSPACE}/deps/Aleph-w" >> "$GITHUB_ENV" + + - name: Cache ccache + uses: actions/cache@v4 + with: + path: ${{ env.CCACHE_DIR }} + key: ${{ runner.os }}-sanitizers-${{ matrix.compiler }}-ccache-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-sanitizers-${{ matrix.compiler }}-ccache- + ${{ runner.os }}-sanitizers-ccache- + + - name: Configure + run: | + rm -rf build-asan + cmake -S . -B build-asan -G Ninja \ + -DCMAKE_C_COMPILER="${CC}" \ + -DCMAKE_CXX_COMPILER="${CXX}" \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DCMAKE_BUILD_TYPE=Debug \ + -DALEPHW="${ALEPHW_DIR}" \ + -DJSON_PATH=/usr \ + -DENABLE_SANITIZERS=ON \ + -DBUILD_DOCS=OFF \ + ${CLANG_GCC_CMAKE_ARG} + + - name: Build + run: | + cmake --build build-asan --parallel + + - name: Test + env: + ASAN_OPTIONS: detect_leaks=1:halt_on_error=1 + UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1 + run: | + ctest --test-dir build-asan --output-on-failure --parallel 2 --timeout 600 + + - name: Upload test logs on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: ctest-logs-sanitizers-${{ matrix.compiler }} + path: | + build-asan/Testing/Temporary/LastTest.log + build-asan/CMakeCache.txt + + coverage-gate: + name: coverage-gate (ubuntu-24.04, gcc, Debug) + runs-on: ubuntu-24.04 + timeout-minutes: 45 + + env: + CCACHE_DIR: ${{ github.workspace }}/.ccache + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + cmake ninja-build pkg-config ccache git ruby-full \ + gcovr \ + libgmp-dev libmpfr-dev libgsl-dev libx11-dev \ + libtclap-dev \ + nlohmann-json3-dev \ + libgtest-dev \ + g++ + + - name: Checkout Aleph-w dependency + run: | + git clone --depth 1 https://github.com/lrleon/Aleph-w.git deps/Aleph-w + + - name: Build Aleph static library + run: | + cmake -S deps/Aleph-w -B deps/Aleph-w/build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTS=OFF \ + -DBUILD_EXAMPLES=OFF + cmake --build deps/Aleph-w/build --target Aleph --parallel + ALEPH_LIB="$(find deps/Aleph-w/build -name libAleph.a | head -n 1)" + test -n "$ALEPH_LIB" + cp "$ALEPH_LIB" deps/Aleph-w/libAleph.a + ls -l deps/Aleph-w/libAleph.a + echo "ALEPHW_DIR=${GITHUB_WORKSPACE}/deps/Aleph-w" >> "$GITHUB_ENV" + + - name: Cache ccache + uses: actions/cache@v4 + with: + path: ${{ env.CCACHE_DIR }} + key: ${{ runner.os }}-coverage-gate-ccache-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-coverage-gate-ccache- + ${{ runner.os }}-ccache- + + - name: Configure + run: | + rm -rf build-cov + cmake -S . -B build-cov -G Ninja \ + -DCMAKE_C_COMPILER=gcc \ + -DCMAKE_CXX_COMPILER=g++ \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_C_FLAGS="--coverage -O0 -g -DNDEBUG" \ + -DCMAKE_CXX_FLAGS="--coverage -O0 -g -DNDEBUG" \ + -DCMAKE_EXE_LINKER_FLAGS="--coverage" \ + -DCMAKE_SHARED_LINKER_FLAGS="--coverage" \ + -DALEPHW="${ALEPHW_DIR}" \ + -DJSON_PATH=/usr \ + -DENABLE_SANITIZERS=OFF \ + -DBUILD_DOCS=OFF \ + -DUCONV_ENABLE_FUZZ_SMOKE_TESTS=OFF + + - name: Build + run: | + cmake --build build-cov --parallel + + - name: Test + run: | + ctest --test-dir build-cov --output-on-failure --parallel 2 --timeout 600 + + - name: Run examples for coverage + run: | + ./build-cov/Examples/basic_conversion + ./build-cov/Examples/physics + ./build-cov/Examples/dynamic_units + + - name: Coverage gate (uconv core) + run: | + gcovr -r . build-cov \ + --filter 'include/uconv[.]H$' \ + --filter 'lib/uconv[.]cc$' \ + --exclude-throw-branches \ + --exclude-unreachable-branches \ + --txt build-cov/coverage-summary.txt \ + --txt-summary \ + --txt-metric branch \ + --print-summary \ + --fail-under-line 80 \ + --fail-under-branch 75 + + gcovr -r . build-cov \ + --filter 'include/uconv[.]H$' \ + --filter 'lib/uconv[.]cc$' \ + --exclude-throw-branches \ + --exclude-unreachable-branches \ + --xml-pretty \ + --output build-cov/coverage.xml + + - name: Upload coverage artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-gate-artifacts + path: | + build-cov/coverage-summary.txt + build-cov/coverage.xml + + docs-check: + name: docs-check (Doxygen warnings gate) + runs-on: ubuntu-24.04 + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + cmake ninja-build git ruby-full \ + doxygen graphviz g++ \ + libgmp-dev libmpfr-dev libgsl-dev libx11-dev \ + nlohmann-json3-dev \ + libgtest-dev + + - name: Checkout Aleph-w dependency + run: | + git clone --depth 1 https://github.com/lrleon/Aleph-w.git deps/Aleph-w + + - name: Build Aleph static library + run: | + cmake -S deps/Aleph-w -B deps/Aleph-w/build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTS=OFF \ + -DBUILD_EXAMPLES=OFF + cmake --build deps/Aleph-w/build --target Aleph --parallel + ALEPH_LIB="$(find deps/Aleph-w/build -name libAleph.a | head -n 1)" + test -n "$ALEPH_LIB" + cp "$ALEPH_LIB" deps/Aleph-w/libAleph.a + ls -l deps/Aleph-w/libAleph.a + echo "ALEPHW_DIR=${GITHUB_WORKSPACE}/deps/Aleph-w" >> "$GITHUB_ENV" + + - name: Configure + run: | + rm -rf build-docs + cmake -S . -B build-docs -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DALEPHW="${ALEPHW_DIR}" \ + -DJSON_PATH=/usr \ + -DENABLE_SANITIZERS=OFF \ + -DBUILD_DOCS=ON + + - name: Build docs + run: | + set -euo pipefail + cmake --build build-docs --target docs --parallel + test -f build-docs/docs/doxygen/html/index.html + test -f build-docs/generated/units/units_catalog.md + test -f build-docs/docs/doxygen/html/units_catalog.html + + CATALOG=build-docs/generated/units/units_catalog.md + grep -q '^| Quantity | Symbol | LaTeX | Units | Description | DSL source |$' "$CATALOG" + grep -q '^| Unit | Symbol | LaTeX | Range | Description | DSL source |$' "$CATALOG" + + pq_count=$(grep -E '^- Physical quantities: [0-9]+$' "$CATALOG" | awk '{print $4}') + unit_count=$(grep -E '^- Units: [0-9]+$' "$CATALOG" | awk '{print $3}') + conv_count=$(grep -E '^- Conversions: [0-9]+$' "$CATALOG" | awk '{print $3}') + + test -n "$pq_count" + test -n "$unit_count" + test -n "$conv_count" + test "$pq_count" -gt 0 + test "$unit_count" -gt 0 + test "$conv_count" -gt 0 + + # Strong regression gate: fail if catalog coverage drops too much. + test "$pq_count" -ge 10 + test "$unit_count" -ge 40 + test "$conv_count" -ge 100 + + - name: Gate on Doxygen warnings + run: | + WARN_FILE=build-docs/docs/doxygen/warnings.log + if [ -s "$WARN_FILE" ]; then + echo "Doxygen warnings were found:" + cat "$WARN_FILE" + exit 1 + fi + echo "No Doxygen warnings." + + - name: Upload docs warnings on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: doxygen-warnings + path: build-docs/docs/doxygen/warnings.log + + ci-success: + name: CI Success + runs-on: ubuntu-24.04 + needs: + - build-and-test + - sanitizers + - coverage-gate + - docs-check + if: always() + steps: + - name: Check results + run: | + set -euo pipefail + echo "build-and-test result: ${{ needs.build-and-test.result }}" + echo "sanitizers result: ${{ needs.sanitizers.result }}" + echo "coverage-gate result: ${{ needs.coverage-gate.result }}" + echo "docs-check result: ${{ needs.docs-check.result }}" + + is_ok() { + [ "$1" = "success" ] || [ "$1" = "skipped" ] + } + + all_ok=1 + + check_job() { + job_name="$1" + job_result="$2" + if ! is_ok "$job_result"; then + echo "Job '$job_name' did not succeed (result: $job_result)" + all_ok=0 + fi + } + + check_job "build-and-test" "${{ needs.build-and-test.result }}" + check_job "sanitizers" "${{ needs.sanitizers.result }}" + check_job "coverage-gate" "${{ needs.coverage-gate.result }}" + check_job "docs-check" "${{ needs.docs-check.result }}" + + if [ "$all_ok" -ne 1 ]; then + echo "CI failed" + exit 1 + fi + echo "CI passed" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..10bfafb --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,116 @@ +name: Docs (Doxygen) + +on: + push: + branches: [main, master] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-deploy: + name: build-and-deploy (GitHub Pages) + runs-on: ubuntu-24.04 + timeout-minutes: 30 + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + cmake ninja-build git ruby-full \ + doxygen graphviz g++ \ + libgmp-dev libmpfr-dev libgsl-dev libx11-dev \ + nlohmann-json3-dev \ + libgtest-dev + + - name: Checkout Aleph-w dependency + run: | + git clone --depth 1 https://github.com/lrleon/Aleph-w.git deps/Aleph-w + + - name: Build Aleph static library + run: | + cmake -S deps/Aleph-w -B deps/Aleph-w/build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTS=OFF \ + -DBUILD_EXAMPLES=OFF + cmake --build deps/Aleph-w/build --target Aleph --parallel + ALEPH_LIB="$(find deps/Aleph-w/build -name libAleph.a | head -n 1)" + test -n "$ALEPH_LIB" + cp "$ALEPH_LIB" deps/Aleph-w/libAleph.a + ls -l deps/Aleph-w/libAleph.a + echo "ALEPHW_DIR=${GITHUB_WORKSPACE}/deps/Aleph-w" >> "$GITHUB_ENV" + + - name: Configure + run: | + rm -rf build + cmake -S . -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DALEPHW="${ALEPHW_DIR}" \ + -DJSON_PATH=/usr \ + -DENABLE_SANITIZERS=OFF \ + -DBUILD_DOCS=ON + + - name: Build Doxygen HTML + run: | + set -euo pipefail + cmake --build build --target docs --parallel + test -f build/docs/doxygen/html/index.html + test -f build/generated/units/units_catalog.md + test -f build/docs/doxygen/html/units_catalog.html + + CATALOG=build/generated/units/units_catalog.md + grep -q '^| Quantity | Symbol | LaTeX | Units | Description | DSL source |$' "$CATALOG" + grep -q '^| Unit | Symbol | LaTeX | Range | Description | DSL source |$' "$CATALOG" + + pq_count=$(grep -E '^- Physical quantities: [0-9]+$' "$CATALOG" | awk '{print $4}') + unit_count=$(grep -E '^- Units: [0-9]+$' "$CATALOG" | awk '{print $3}') + conv_count=$(grep -E '^- Conversions: [0-9]+$' "$CATALOG" | awk '{print $3}') + + test -n "$pq_count" + test -n "$unit_count" + test -n "$conv_count" + test "$pq_count" -gt 0 + test "$unit_count" -gt 0 + test "$conv_count" -gt 0 + + # Strong regression gate: fail if catalog coverage drops too much. + test "$pq_count" -ge 10 + test "$unit_count" -ge 40 + test "$conv_count" -ge 100 + + - name: Gate on Doxygen warnings + run: | + WARN_FILE=build/docs/doxygen/warnings.log + if [ -s "$WARN_FILE" ]; then + echo "Doxygen warnings were found:" + cat "$WARN_FILE" + exit 1 + fi + echo "No Doxygen warnings." + + - name: Configure Pages + uses: actions/configure-pages@v5 + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: build/docs/doxygen/html + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/full_analysis.yml b/.github/workflows/full_analysis.yml new file mode 100644 index 0000000..f63a693 --- /dev/null +++ b/.github/workflows/full_analysis.yml @@ -0,0 +1,134 @@ +name: Full Analysis + +on: + schedule: + # Weekly on Sunday at 02:00 UTC. + - cron: '0 2 * * 0' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + full-analysis: + name: full-analysis (ubuntu-24.04, gcc, Release) + runs-on: ubuntu-24.04 + timeout-minutes: 90 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + cmake ninja-build pkg-config ccache git \ + g++ ruby-full doxygen graphviz cppcheck \ + libgmp-dev libmpfr-dev libgsl-dev libx11-dev \ + libtclap-dev \ + nlohmann-json3-dev \ + libgtest-dev + + - name: Checkout Aleph-w dependency + run: | + git clone --depth 1 https://github.com/lrleon/Aleph-w.git deps/Aleph-w + + - name: Build Aleph static library + run: | + cmake -S deps/Aleph-w -B deps/Aleph-w/build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTS=OFF \ + -DBUILD_EXAMPLES=OFF + cmake --build deps/Aleph-w/build --target Aleph --parallel + ALEPH_LIB="$(find deps/Aleph-w/build -name libAleph.a | head -n 1)" + test -n "$ALEPH_LIB" + cp "$ALEPH_LIB" deps/Aleph-w/libAleph.a + ls -l deps/Aleph-w/libAleph.a + echo "ALEPHW_DIR=${GITHUB_WORKSPACE}/deps/Aleph-w" >> "$GITHUB_ENV" + + - name: Configure + run: | + rm -rf build-full + cmake -S . -B build-full -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DALEPHW="${ALEPHW_DIR}" \ + -DJSON_PATH=/usr \ + -DENABLE_SANITIZERS=OFF \ + -DBUILD_DOCS=ON + + - name: Build + run: | + cmake --build build-full --parallel + + - name: Test + env: + ASAN_OPTIONS: detect_leaks=0 + run: | + ctest --test-dir build-full --output-on-failure --parallel 2 --timeout 600 + + - name: cppcheck (full repo) + run: | + rm -f cppcheck.xml cppcheck.txt + cppcheck \ + --enable=warning,performance,portability \ + --inconclusive \ + --inline-suppr \ + --suppress=missingIncludeSystem \ + --error-exitcode=1 \ + --xml --xml-version=2 \ + -I include \ + -I "${ALEPHW_DIR}" \ + . 2> cppcheck.xml + cppcheck \ + --enable=warning,performance,portability \ + --inconclusive \ + --inline-suppr \ + --suppress=missingIncludeSystem \ + -I include \ + -I "${ALEPHW_DIR}" \ + . 2> cppcheck.txt || true + + - name: Doxygen (collect warnings) + run: | + set -euo pipefail + cmake --build build-full --target docs --parallel + test -f build-full/generated/units/units_catalog.md + test -f build-full/docs/doxygen/html/units_catalog.html + + CATALOG=build-full/generated/units/units_catalog.md + grep -q '^| Quantity | Symbol | LaTeX | Units | Description | DSL source |$' "$CATALOG" + grep -q '^| Unit | Symbol | LaTeX | Range | Description | DSL source |$' "$CATALOG" + + pq_count=$(grep -E '^- Physical quantities: [0-9]+$' "$CATALOG" | awk '{print $4}') + unit_count=$(grep -E '^- Units: [0-9]+$' "$CATALOG" | awk '{print $3}') + conv_count=$(grep -E '^- Conversions: [0-9]+$' "$CATALOG" | awk '{print $3}') + + test -n "$pq_count" + test -n "$unit_count" + test -n "$conv_count" + test "$pq_count" -gt 0 + test "$unit_count" -gt 0 + test "$conv_count" -gt 0 + + # Strong regression gate: fail if catalog coverage drops too much. + test "$pq_count" -ge 10 + test "$unit_count" -ge 40 + test "$conv_count" -ge 100 + + cp build-full/docs/doxygen/warnings.log doxygen_warnings.log + cp build-full/generated/units/units_catalog.md units_catalog.md + + - name: Upload full analysis reports + uses: actions/upload-artifact@v4 + with: + name: full-analysis-reports + path: | + cppcheck.xml + cppcheck.txt + doxygen_warnings.log + units_catalog.md diff --git a/.gitignore b/.gitignore index e64feb8..483fd2d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,7 @@ Makefile *.bak gitversion.H tests/unittests/*.cc -lib/zen.cc \ No newline at end of file +lib/zen.cc + +# Generated unit headers (build output). They should never be committed from source tree. +include/units/*.H diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..ab1f416 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..b68a1ad --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,112 @@ +cmake_minimum_required(VERSION 3.10) + +# Optional Clang helper for environments where Clang does not auto-detect +# a usable libstdc++ toolchain during compiler checks (try-compile). +set(UCONV_CLANG_GCC_INSTALL_DIR "${UCONV_CLANG_GCC_INSTALL_DIR}" CACHE STRING + "Path passed to Clang via --gcc-install-dir (e.g. /usr/lib/gcc/x86_64-linux-gnu/13)") +set(UCONV_CLANG_GCC_TOOLCHAIN "${UCONV_CLANG_GCC_TOOLCHAIN}" CACHE STRING + "Path passed to Clang via --gcc-toolchain (e.g. /usr)") +if(UCONV_CLANG_GCC_INSTALL_DIR) + set(_UCONV_CLANG_GCC_FLAG "--gcc-install-dir=${UCONV_CLANG_GCC_INSTALL_DIR}") +elseif(UCONV_CLANG_GCC_TOOLCHAIN) + set(_UCONV_CLANG_GCC_FLAG "--gcc-toolchain=${UCONV_CLANG_GCC_TOOLCHAIN}") +endif() + +if(DEFINED _UCONV_CLANG_GCC_FLAG) + set(CMAKE_C_FLAGS_INIT "${CMAKE_C_FLAGS_INIT} ${_UCONV_CLANG_GCC_FLAG}") + set(CMAKE_CXX_FLAGS_INIT "${CMAKE_CXX_FLAGS_INIT} ${_UCONV_CLANG_GCC_FLAG}") + set(CMAKE_EXE_LINKER_FLAGS_INIT "${CMAKE_EXE_LINKER_FLAGS_INIT} ${_UCONV_CLANG_GCC_FLAG}") + set(CMAKE_SHARED_LINKER_FLAGS_INIT "${CMAKE_SHARED_LINKER_FLAGS_INIT} ${_UCONV_CLANG_GCC_FLAG}") +endif() + +project(uconv CXX) + +set(CMAKE_CXX_STANDARD 20) +file(STRINGS "${CMAKE_SOURCE_DIR}/version.txt" UCONV_VERSION LIMIT_COUNT 1) +if(NOT UCONV_VERSION) + set(UCONV_VERSION "dev") +endif() + +# Dependencies +if(NOT DEFINED ALEPHW) + if(DEFINED ENV{ALEPHW}) + set(ALEPHW $ENV{ALEPHW}) + else() + message(FATAL_ERROR "ALEPHW is not defined. Please set it to the Aleph library path (e.g. cmake -DALEPHW=/path/to/aleph ..)") + endif() +endif() + +if(NOT DEFINED JSON_PATH) + if(DEFINED ENV{JSON}) + set(JSON_PATH $ENV{JSON}) + else() + message(FATAL_ERROR "JSON_PATH is not defined. Please set it to the nlohmann/json root path (e.g. cmake -DJSON_PATH=/path/to/json ..)") + endif() +endif() + +# Global include directories +include_directories(${CMAKE_SOURCE_DIR}/include) +include_directories(${ALEPHW}) +include_directories(${JSON_PATH}/include) + +# Compilation options +add_compile_options(-Wall -Wextra -Wcast-align -Wno-sign-compare -Wno-write-strings -Wno-parentheses) + +if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + if(UCONV_CLANG_GCC_INSTALL_DIR) + message(STATUS "Using Clang gcc-install-dir: ${UCONV_CLANG_GCC_INSTALL_DIR}") + elseif(UCONV_CLANG_GCC_TOOLCHAIN) + message(STATUS "Using Clang gcc-toolchain: ${UCONV_CLANG_GCC_TOOLCHAIN}") + endif() +endif() + +# Generated artifacts live in the build tree. +set(UCONV_GENERATED_DIR ${CMAKE_BINARY_DIR}/generated) +set(UCONV_GENERATED_UNITS_DIR ${UCONV_GENERATED_DIR}/units) + +# Sanitizers (enabled by default as in Imakefile) +option(ENABLE_SANITIZERS "Enable address and undefined sanitizers" ON) +if(ENABLE_SANITIZERS) + add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer) + add_link_options(-fsanitize=address,undefined) +endif() + +option(BUILD_DOCS "Build API documentation with Doxygen" ON) + +enable_testing() + +# Subdirectories +add_subdirectory(lib) +add_subdirectory(utils) +add_subdirectory(Tests) +add_subdirectory(fuzz) +add_subdirectory(Examples) + +if(BUILD_DOCS) + find_package(Doxygen QUIET) + if(DOXYGEN_FOUND) + set(DOXYGEN_OUTPUT_DIR ${CMAKE_BINARY_DIR}/docs/doxygen) + set(DOXYGEN_CONFIG_FILE ${CMAKE_BINARY_DIR}/Doxyfile) + set(DOXYGEN_INDEX_FILE ${DOXYGEN_OUTPUT_DIR}/html/index.html) + + configure_file( + ${CMAKE_SOURCE_DIR}/cmake/Doxyfile.in + ${DOXYGEN_CONFIG_FILE} + @ONLY + ) + + add_custom_command( + OUTPUT ${DOXYGEN_INDEX_FILE} + COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_CONFIG_FILE} + DEPENDS uconv ${DOXYGEN_CONFIG_FILE} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMENT "Generating API documentation with Doxygen" + VERBATIM + ) + + add_custom_target(docs DEPENDS ${DOXYGEN_INDEX_FILE}) + message(STATUS "Doxygen docs target enabled: 'docs'") + else() + message(WARNING "Doxygen not found. 'docs' target will not be available.") + endif() +endif() diff --git a/Examples/CMakeLists.txt b/Examples/CMakeLists.txt new file mode 100644 index 0000000..98b81c2 --- /dev/null +++ b/Examples/CMakeLists.txt @@ -0,0 +1,19 @@ +# Examples configuration + +# Find dependencies (same as in Tests) +find_library(ALEPH_LIB Aleph PATHS ${ALEPHW} NO_DEFAULT_PATH) +find_library(GSL_LIB gsl) +find_library(GSLCBLAS_LIB gslcblas) +find_library(M_LIB m) + +set(EXAMPLES + basic_conversion + dynamic_units + physics +) + +foreach(ex ${EXAMPLES}) + add_executable(${ex} ${ex}.cc) + add_dependencies(${ex} uconv) + target_link_libraries(${ex} uconv ${ALEPH_LIB} ${GSL_LIB} ${GSLCBLAS_LIB} ${M_LIB}) +endforeach() diff --git a/Examples/basic_conversion.cc b/Examples/basic_conversion.cc new file mode 100644 index 0000000..399f684 --- /dev/null +++ b/Examples/basic_conversion.cc @@ -0,0 +1,37 @@ +#include +#include +#include +#include + +// Instantiate the units system (required once in the program) +UnitsInstancer init; + + +int main() { + std::cout << "=== Example 1: Basic Conversion ===" << std::endl; + + // Definition of quantities with strong types + Quantity length = 10.0; + Quantity width = 50.0; + + std::cout << "Initial length: " << length << std::endl; + std::cout << "Initial width: " << width << std::endl; + + // Arithmetic operations: automatic conversion + // 'width' is converted to 'meter' before adding + Quantity total = length + width; + + std::cout << "Total (length + width) in meters: " << total << std::endl; + + // Explicit conversion to another unit + Quantity total_yards = total; + std::cout << "Total in yards: " << total_yards << std::endl; + + // Example with temperature + Quantity t_c = 100.0; + Quantity t_f = t_c; + + std::cout << "\nBoiling point temperature: " << t_c << " = " << t_f << std::endl; + + return 0; +} diff --git a/Examples/dynamic_units.cc b/Examples/dynamic_units.cc new file mode 100644 index 0000000..9c1b383 --- /dev/null +++ b/Examples/dynamic_units.cc @@ -0,0 +1,41 @@ +#include +#include +#include +#include // Incluye todas las definiciones + +UnitsInstancer init; + + +int main() { + std::cout << "=== Ejemplo 2: Unidades Dinámicas (VtlQuantity) ===" << std::endl; + std::cout << "Este ejemplo simula el manejo de unidades seleccionadas por el usuario en tiempo de ejecución." << std::endl; + + try { + // Supongamos que estos strings vienen de una interfaz de usuario o archivo de config + std::string unidad_entrada = "psia"; + double valor_entrada = 30.0; + + std::string unidad_salida = "bar"; + + // Crear cantidad dinámicamente + VtlQuantity presion(unidad_entrada, valor_entrada); + std::cout << "\nEntrada: " << presion << std::endl; + + // Convertir dinámicamente + VtlQuantity resultado(unidad_salida, presion); + std::cout << "Convertido a " << unidad_salida << ": " << resultado << std::endl; + + // Operaciones mixtas + VtlQuantity extra("Pa", 5000.0); + std::cout << "Sumando " << extra << "..." << std::endl; + + // El resultado mantiene la unidad del operando izquierdo (psi) + VtlQuantity total = presion + extra; + std::cout << "Total: " << total << std::endl; + + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + } + + return 0; +} diff --git a/Examples/physics.cc b/Examples/physics.cc new file mode 100644 index 0000000..012b6bb --- /dev/null +++ b/Examples/physics.cc @@ -0,0 +1,33 @@ +#include +#include +#include +#include +#include + +UnitsInstancer init; + + +int main() { + std::cout << "=== Ejemplo 3: Cálculos Físicos ===" << std::endl; + + // Definir velocidad y tiempo + Quantity velocidad = 25.0; // 25 m/s + Quantity tiempo = 10.0; // 10 segundos + + std::cout << "Velocidad: " << velocidad << std::endl; + std::cout << "Tiempo: " << tiempo << std::endl; + + // Calcular distancia + // Como no hemos definido explícitamente la unidad compuesta (m/s * s = m), + // operamos con los valores crudos (raw) y asignamos a la unidad resultante esperada. + double distancia_val = velocidad.raw() * tiempo.raw(); + Quantity distancia = distancia_val; + + std::cout << "Distancia recorrida: " << distancia << std::endl; + + // Conversión a otras unidades de longitud + std::cout << "En pies: " << Quantity(distancia) << std::endl; + std::cout << "En yardas: " << Quantity(distancia) << std::endl; + + return 0; +} diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..45a70a0 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +gem 'colored2' # For colored terminal output +gem 'thor' # For command line interface utilities diff --git a/Imakefile b/Imakefile deleted file mode 100644 index 505a3ab..0000000 --- a/Imakefile +++ /dev/null @@ -1,10 +0,0 @@ - -# define IHaveSubdirs -# define PassCDebugFlags - -ALEPHW = $(shell echo $$ALEPHW) - -SUBDIRS = include lib tests fuzz - -MakeSubdirs($(SUBDIRS)) -DependSubdirs($(SUBDIRS)) diff --git a/README.md b/README.md index fe92c62..5b542b5 100644 --- a/README.md +++ b/README.md @@ -1,371 +1,364 @@ -# Units definitions and their conversion library - -## Purpose - -`uconv` is a C++ library that allows to define physical quantities -along with units definitions and conversions between different units -through simple classes for defining and handling physical quantities, -units measuring them, and quantities related to the units with -transparent conversions. - -### Physical Quantities - -A physical quantity is defined with the macro - - Declare_Physical_Unit(Physical_Quantity_Class_Name, - "Unit Symbol", "Unit LaTeX symbol", - "description"); - -This macro declares a class defining a physical quantity. For example, -we can define class named `Temperature`, around which temperature units -would be grouped, as follows: - - Declare_Physical_Quantity(Temperature, "T", "T", - "Average thermal energy of the particles in a substance"); - -### Units Definition - -A new unit is defined with the macro - - Declare_Unit(Unit_Class_Name, "Unit symbol", "LaTeX symbol", - "description", Physical_Quantity, - min-value, max-value); - -This macro declares a class that defines a new unit related to the -`Physical_Quantity` class which should be previously already -declared. `min-value` and `max-value` are doubles defining the lower -and upper bounds of the unit. - -As example, let us consider `Fahrenheit` as class name that defines -the well known temperature unit: - - Declare_Unit(Fahrenheit, "degF", "\\degree{F}", - "Scale based on brine freezing (0) and boiling point (100)", - Temperature, -459.67, 750); - -Since LaTeX math mode commands often start by backslash, you must -escape each backslash belonging to LaTeX expression. This is the -reason for which you see two backslashes in the string that defines -the LaTeX symbol. - -### Defining Unit Conversions - -Unit conversion is relatively very easy if you use the -`Declare_Conversion` macro whose syntax is as follows: - - Declare_Conversion(Source_Unit_Class_Name, - Target_Unit_Class_Name, parameter-name) - -This macro declares a function signature to perform the conversion of -a quantity expressed in `Source_Unit_Class_Name` towards -`Target_Unit_Class_Name`, whose parameter is a double, and it returns -a double corresponding to the converted value. Next of using of -`Declare_Conversion`, the function conversion body, which implements -the conversion, must be written. For example, for converting from -`Fahrenheit` to `Celsius` you might write this thus: - - Declare_Conversion(Fahrenheit, Celsius, t) { return (t - 32) / 1.8; } - -`Fahrenheit` and `Celsius` class names must already be defined. The -parameter `t` is a double in `Fahrenheit` and in the function body -(between `{}`) is written the conversion. - -### Defining and Using Quantities - -There are two ways for defining a quantity with its unit, everyone -with its own class: - -1. `Quantity`: a quantity whose unit is known in - compiling time. - -2. `VtlQuantity`: a quantity whose unit is known in run time. - -#### `Quantity` class - -`Quantity` is designed for dealing units and its -conversions in compiling time, without need of lookups in run time. In -addition, the template parameter directly indicates the unit that is -being used. - -As example, let us consider the following function: - - Quantity speed(const Quantity & distance, - const Quantity & time) - { - Quantity ret_val = distance.raw() / time.raw(); - return ret_val; - } - -The function computes the speed given the traversed distance and the -lasted time. Distance is measured in meters and time in -hours. However, conditioned to the existence of a conversion function, -the distance and time could be in different units, kilometers or -seconds, for example. So, you could have some like this: - - Quantity dist = 100; - Quantity t = 3600; - - Quantity s = speed(dist, t); - -It is very important to understand everything that happens in the last -line. First, since the `speed()` function was designed for handling -meters and hours, the conversions form meters to kilometers and from -seconds to hours are performed. So, at the beginning of the `speed()` -function, the `distance` and `time` parameters are already in meters -and hours respectively. - -Second, the return value of `speed()` function is a `Quantity` whose -unit is `Km_h` (kilometers per hour) but the computation is done in -`Meter_h` (meters per hour). It is logic to define the result in -meters per hours because this is the unit that fits with the received -parameters. However the return value was declared in `Km_h` -(kilometers per hour), which should not pose any problem if the -conversion from `Meter_s` towards `Km_h` has been defined. - -Finally, it is time to mention the `raw()` method, which simply -removes the unit and returns a double. This is very important to -notice because if you performed some such as this: - - Quantity ret_val = distance / time; - -Then the library would try to find a unit that combines `Meter` and -`Second` units in the division operation. Such kind of unit is called -*compounded* unit and it could be defined with `Declare_Compound_Unit` -macro in this way: - - Declare_Compound_Unit(Meter_h, "Mt/s", "meters per second", - Speed, 0, 299792458, Meter, Second); - -Compound units could be very useful for validations. However, they require -to define many more conversions and especially to know the unit -bounds, which increases the complexity. For this reason, it could be -preferable to directly define speed units without indicating that they -are compounded. Thus, in the example, we remove the units with the -`raw()` method, next we perform the division, and finally we build a -`Quantity` object to store the result and express its unit. - -When a conversion is required but this one has not been defined, a -compiler error will be generated. - -`Quantity` object always require to know their unit. Consequently, it -is not possible to instantiate a empty `Quantity`, without a unit, -because if not the boundary check could not be performed. - -#### `VltQuantity` class - -`Quantity` class is very adequate for defining computations without -worry for the entry units. Computations reflecting a physical -phenomenon could be defined in a natural way, directly expressed in -their original units. The `speed()` function example clearly shows -this kind of independence. Therefore, to the extent possible, we -strongly advice to use `Quantity` class rather than `VtlQuantity` -because it is safer and faster. - -However, there are situations where it is not possible or desirable to -know the units in which the computations will be expressed. Consider, -for instance, to incorporate new operations in running time, some such -like a new formula. In this case, a `Quantity` object is not -applicable because it requires to know the unit type in -compiling time. For dealing with situations like this, the -`VtlQuantity` class is provided. - -There is not so much difference between a `Quantity` and a -`Vtlquantity` object. Essentially, the main difference is that the -`Vtlquantity` constructor requires the unit as part of its parameters -(to the difference of `Quantity` that receives the unit as a template -parameter). - -The price of a `Vtlquantity` object is the possibility of a table look -up in order to find the conversion function. Apart of that, practically -use of `Quantity` and `VtlQuantity` is very similar. In fact, you can -arbitrarily combine them. - -#### Mathematical operations - -`uconv` export some mathematical operations. In all operations, the -unit limits are checked. - -The main binary mathematical operators `+`, `-`, `*` and `/` are -exported. Each binary operation can receive, `Quantity`, `VtlQuantity` -or double objects. If a double is received, then it is assumed that -its unit is the same than the another operand. `+` and `-` operators -require that both operands are in the same physical quantity yet -conversion is transparently done if the units are different (but -belonging to the same physical quantity). In the case of `*` and `/` -operands, the compound unit corresponding to the result is required. - -Other wrappers to C mathematical functions are provided: `pow()`, -`exp()`, `log10()`, `sqrt()`, etc. All this functions return a double. - -### Defining bounds tolerance - -Since the most of conversions involve floating point operations, the -units limits are verified respect a tolerance value called epsilon. By -default, this value is `1e-6` but it can be set to any other -value. Let be `min-val` and `max-val` the limits of some unit. Then, -the limits check is done on the interval `[min-val - epsilon, -max-val + epsilon]`. Notice that without this tolerance, then we could -get an `OutOfUnitRange` exception if, for example, we convert -`min-value` to another unit and we return again on the original unit. - -### The `uconv-list.H` header file - -The current way for indicating to the library which are the units is -through file inclusion in the `uconv-list.H` header file. When the -library is built, the system will search this file in order to -generate all the needed bookkeeping. - -## Integration - -There are two ways for integrating `uconv` to your project. The first -one is by adding your units directly to the library distribution and -then building the `libuconv.a` file. This way implies that you should -edit the `uconv-list.H` file and put file inclusions to your unit -definitions. The second way, which is the recommended, is to create -and manage your own `uconv-list.H` (or another name that you want) and -building your own `libuconv.a` file (or another name that you want). - -Let us suppose that your unit definitions are in `uconv-list.H` -file. First, this file must include the header `uconv.H` followed by -file inclusions to your units definitions. Some such that: - - # include - # include "units/temperature-unit.H" - # include "units/pressure-unit.H" - # include "units/density-unit.H" - // so on ... - -From the second line, each line is a file inclusion to a header -containing a physical quantity along with its units. - -You must put an inclusion for `uconv-list.H` in every place of your -project requiring to manage units. Note that you can rename -`uconv-list.H`, but you should not remove the inclusion to `uconv.H`. - -Your project must be able to locate the headers `uconv.H` and -`uconv-list.H` and to link `libuconv.a` file to the every executable. - -#### Building the `libuconv.a` library inside the `uconv` distribution - -In this case, the recommended way is to place the units definitions -headers in the `include/units` sub directory. Once you have already -defined all your units, go to the `lib` directory and perform: - - make depend - make all - -If everything was okay, you will have `libuconv.a` file in the directory. - -The provided `Imakefile` file is configured for using `clang` compiler -and its sanitizer. Also, by default the library is compiled without -optimization. Therefore, if you really adopt this approach to -integrate your units to your project, we advice you to edit the -`Imakefile`, suppress the sanitizer flags, and to set `-O2` compiler -flag. +# uconv -#### Building your own `libuconv.a` library +A C++20 units and physical-quantities conversion library with: -If you have several projects managing units, then you probably will -want to manage several and independent unit systems. In this case, you -will not probably want to build `libuconv.a` inside `uconv` -distribution because this library will contain all the units through -all your projects with the consequent waste of space and time. +- Strongly typed quantities at compile time (`Quantity`) +- Runtime quantities (`VtlQuantity`) +- A Ruby DSL to define physical quantities, units, and conversions +- Strong DSL validation (duplicates, missing references, conversion coverage) +- Automatic C++ code generation and Doxygen documentation -In order to manage this, you can use the script `buildunits` contained -in the `bin` directory. Its command line syntax is as follows: +## Contents - buildunits -U uconv-list-file -H path-to-uconv-header - -Let us suppose that you have defined your unit in `my-units.H` -file. Then, you perform: +1. [Project Status](#project-status) +2. [Repository Layout](#repository-layout) +3. [Requirements](#requirements) +4. [Quick Start](#quick-start) +5. [Consuming the Library](#consuming-the-library) +6. [Build Targets](#build-targets) +7. [Running Tests](#running-tests) +8. [Running Fuzz (Bounded)](#running-fuzz-bounded) +9. [Running Utilities](#running-utilities) +10. [Examples](#examples) +11. [Defining New Units and Conversions](#defining-new-units-and-conversions) +12. [DSL Validation Rules](#dsl-validation-rules) +13. [Documentation (Doxygen)](#documentation-doxygen) +14. [CI Workflows](#ci-workflows) +15. [Exception Style Guide](#exception-style-guide) +16. [Troubleshooting](#troubleshooting) +17. [License](#license) - buildunits -U my-units.H -H myproject/include - -This command will build `libuconv.a` in the directory where -`buildunits` was called. +## Project Status -If you need a different name for your library, then you could rename -it or use the flag `-l library-name`. Referring the previous example, -you could perform: +The project is currently organized around the DSL pipeline: - buildunits -U my-units.H -H myproject/include -l my-units.a +- Source of truth for units DSL: `lib/unit-defs.rb` + `lib/unit-defs/*.rb` +- Generated C++ headers: `/generated/units/*.H` +- Core runtime API: `include/uconv.H` +- Unit tests (GoogleTest + `ctest`): `Tests/` +- CLI utilities (manual checks/tools): `utils/` -#### Usage in your project +Generated headers are build artifacts and are not versioned in Git. -`uconv` internally uses several tables for storing physical quantities -names, units and conversions. For this reason, it is imperative to -explicitly initialize the library. The most recommended way for doing -that is with `UnitsInstancer` class, which is a singleton and that must -be called before any use of `uconv`. As general rule, we recommend to -instantiate just next the `uconv-list.H` header inclusion. Some such -this: +## Repository Layout - # include - UnitsInstancer init; // or any other name that you want - // here you could put other header inclusions - -Be careful with a double instantiation that could cause a linking -conflict. You could avoid that by using a macro guard: +- `include/`: public headers (`uconv.H`, helper headers) +- `lib/`: core implementation and DSL definitions +- `bin/gen-units`: Ruby generator + DSL validator +- `Tests/`: automated unit tests +- `utils/`: manual CLI utilities +- `Examples/`: usage examples +- `docs/`: human-facing technical documentation (included in Doxygen) +- `cmake/Doxyfile.in`: Doxygen configuration template - # ifndef UNITS_INSTANTIATED - # define UNITS_INSTANTIATED 1 - UnitsInstancer init; - # endif +## Requirements -### Building Requirements +### Required -You will need `Aleph-w` library, which can be downloaded from - and the Niels Lohmann (nlohmann) jsbon -library, which can be downloaded from . +- C++20 compiler (`g++` or `clang++`) +- CMake >= 3.10 +- Ruby (to run `bin/gen-units`) +- `Aleph-w` headers + library +- `nlohmann/json` include path +- GoogleTest (for `Tests/`) -In order to build the library, you will need to have installed `Ruby`, -given that the scripts are written in this language. +### Optional -Tests and demos expect to find the `clang` compiler and they use its -sanitizer. You can remove this dependence by editing the -`Imakefiles`. On each `Imakefile` do: +- Doxygen + Graphviz (for docs) -1. Comment the definitions of `CXX`, `CLINK`, `AR` and `RANLIB`. +## Quick Start -2. Erase the compiler flag `-fsanitize=address,undefined`. +### 1. Set dependencies -`uconv` has only been tested on Linux systems, but it is supposed to -run without problems on other systems where `Aleph-w` library is -installed. +You can provide dependencies as environment variables or CMake variables. -### Running Tests +Using environment variables: -Unit test cases are located it `Tests` directory. For performing -them, you will need `googletest` -. +```bash +export ALEPHW=/path/to/Aleph-w +export JSON=/path/to/json +``` -For executing all the tests: +Or pass them explicitly to CMake: - ./all-test - -For executing a specific test: +```bash +cmake -S . -B cmake-build-debug -DALEPHW=/path/to/Aleph-w -DJSON_PATH=/path/to/json +``` - ./run-test -f test-file - -Demo tests are located in `tests` directory. Inside `tests` directory -execute +### 2. Configure - make all +```bash +cmake -S . -B cmake-build-debug -DALEPHW=/path/to/Aleph-w -DJSON_PATH=/path/to/json +``` -You will have the following executables: +### 3. Build -1. `convert`: simple unit converter. +```bash +cmake --build cmake-build-debug -j +``` -2. `vector-convert`: vector unit converter. +This build automatically regenerates `/generated/units/*.H` from the DSL when needed. -3. `test-all-units`: a unit bounds tester given a physical quantity. +## Consuming the Library +### CMake consumer (recommended) -## License +Link against target `uconv` and include: + +```cpp +#include +``` + +`uconv-list.H` includes generated `all_units.H` automatically. +When you link `uconv`, CMake exports include paths for both: + +- `include/` +- `/generated/units/` + +Minimal consumer example: + +```cmake +add_executable(my_app main.cpp) +target_link_libraries(my_app PRIVATE uconv) +``` + +```cpp +#include + +int main() { + const auto& _units = init_units(); + (void)_units; + Quantity d(10.0); + Quantity f = d; + (void)f; + return 0; +} +``` + +### Manual compilation (if not using CMake targets) + +You must add both include paths explicitly: + +- `-I/path/to/uconv/include` +- `-I/path/to/uconv//generated/units` + +### Where to inspect available units + +- DSL source of truth: `lib/unit-defs.rb`, `lib/unit-defs/*.rb` +- Generated API docs: `cmake-build-debug/docs/doxygen/html/index.html` +- In Doxygen, search/open page `Units Catalog` (auto-generated from DSL) +- Generated headers: `cmake-build-debug/generated/units/*.H` + +## Build Targets + +Common targets: + +- Default build: `cmake --build cmake-build-debug` +- Unit tests binaries: included via `Tests/` +- Utilities binaries: included via `utils/` +- Examples binaries: included via `Examples/` +- Docs site: `cmake --build cmake-build-debug --target docs` + +CMake options: + +- `-DENABLE_SANITIZERS=ON|OFF` (default `ON`) +- `-DBUILD_DOCS=ON|OFF` (default `ON`) + +## Running Tests + +Automated unit tests are in `Tests/` and are registered with `ctest`. + +Run all: + +```bash +ctest --test-dir cmake-build-debug --output-on-failure +``` + +If ASAN leak detection interferes in your environment, use: + +```bash +ASAN_OPTIONS=detect_leaks=0 ctest --test-dir cmake-build-debug --output-on-failure +``` + +## Running Fuzz (Bounded) + +The fuzz target is `simple` in `fuzz/`. + +Important: libFuzzer runs indefinitely by default. Use bounded flags: + +```bash +./cmake-build-debug/fuzz/simple -runs=20000 -max_total_time=20 +``` + +In Clang builds, `ctest` also registers a bounded smoke test: + +```bash +ctest --test-dir cmake-build-debug -R fuzz_simple_smoke --output-on-failure +``` + +## Running Utilities + +Manual utilities are built under `utils/`: + +- `util-convert`: convert scalar values between units +- `util-vector-convert`: convert value vectors +- `util-all-units`: validate/range-check by physical quantity +- `util-unit-table`: inspect registered units table + +Example: + +```bash +./cmake-build-debug/utils/util-convert --help +``` + +## Examples + +Examples are built under `Examples/`: + +- `basic_conversion` +- `dynamic_units` +- `physics` + +Example: + +```bash +./cmake-build-debug/Examples/basic_conversion +``` + +## Defining New Units and Conversions + +### 1. Add or edit DSL files + +- Root DSL entrypoint: `lib/unit-defs.rb` +- Per-domain DSL files: `lib/unit-defs/*.rb` + +If you add a new DSL file, import it in `lib/unit-defs.rb`. -Since `uconv` depends on components that are licensed under GNU GPL v3, -`uconv` is also licensed on the GNU GPL v3. +### 2. Use DSL primitives + +- `physical_quantity :Name do ... end` +- `unit :UnitName do ... end` +- `conversion :FromUnit, :ToUnit do |v| "...#{v}..." end` + +Required metadata: + +- Physical quantity: `symbol`, `latex`, `description` +- Unit: `symbol`, `latex`, `description`, `quantity`, `range` +- Conversion: `from`, `to`, formula block + +### 3. Regenerate headers + +```bash +ruby bin/gen-units lib/unit-defs.rb /generated/units +``` + +### 4. Build and test + +```bash +cmake --build cmake-build-debug -j +ctest --test-dir cmake-build-debug --output-on-failure +``` + +## DSL Validation Rules + +`bin/gen-units` enforces structural checks before generating C++: + +- Missing fields (`symbol`, `latex`, `description`, `quantity`, `range`, formula) +- Invalid ranges (`min > max`, non-numeric range) +- Duplicates (quantity names/symbols, unit names/symbols, conversion pairs) +- Unknown references (unit -> quantity, conversion endpoints) +- Cross-quantity conversions (forbidden) +- Conversion coverage (directed full coverage within each quantity family) +- Explicit self-conversions (forbidden) + +If validation fails, generation aborts with a detailed error list. + +## Documentation (Doxygen) + +### Generate docs + +```bash +cmake --build cmake-build-debug --target docs +``` + +Output: + +- HTML index: `cmake-build-debug/docs/doxygen/html/index.html` +- Warnings log: `cmake-build-debug/docs/doxygen/warnings.log` + +### What is documented + +- Full public API in `include/uconv.H` +- Generated units headers (`/generated/units/*.H`) +- DSL source files (`lib/unit-defs.rb`, `lib/unit-defs/*.rb`) +- Markdown docs in `docs/` + +### Automatic docs for future units/conversions + +When you define new units or conversions in the DSL: + +1. `bin/gen-units` regenerates headers with Doxygen blocks per quantity/unit/conversion. +2. `docs` target runs Doxygen over generated headers and DSL docs. + +So new DSL definitions appear in Doxygen automatically after: + +```bash +cmake --build cmake-build-debug --target docs +``` + +## CI Workflows + +Current workflows (`.github/workflows/`): + +- `ci.yml`: matrix build/test + sanitizers (`gcc` and `clang`, Debug) + docs warnings gate +- `docs.yml`: build and publish Doxygen site to GitHub Pages +- `full_analysis.yml`: scheduled deeper analysis (build/test/cppcheck/docs) +- All workflows clone `Aleph-w`, build `libAleph.a`, and export `ALEPHW_DIR` before configuring `uconv`. + +## Exception Style Guide + +All new exception throws must use macro-based style. + +- For standard exceptions (`std::domain_error`, `std::range_error`, etc.): + use `ah-errors.H` macros (for example `ah_domain_error_if(...)`, `ah_range_error_unless(...)`). +- For uconv/Aleph custom exceptions not present in `ah-errors.H`: + use `include/ah-uconv-errors.H` macros (for example + `ah_unit_not_found_error_if(...)`, `ah_unit_conversion_not_found_error_if(...)`). + +Rules: + +- Prefer conditional macros (`*_if`, `*_unless`) instead of manual `if (...) throw ...`. +- Build error messages with stream syntax (`<<`) directly on the macro. +- Avoid direct `throw ...` in library/runtime code unless there is no macro coverage yet. + +## Troubleshooting + +### `ALEPHW is not defined` + +Set `ALEPHW` or pass `-DALEPHW=...`. + +### `JSON_PATH is not defined` + +Set `JSON` env var or pass `-DJSON_PATH=...`. + +### DSL generation fails + +Run directly to see validation errors: + +```bash +ruby bin/gen-units lib/unit-defs.rb /generated/units +``` + +### Docs target not available + +Install Doxygen and reconfigure: + +```bash +sudo apt-get install doxygen graphviz +cmake -S . -B cmake-build-debug +``` + +## License -See -[LICENSE](https://github.com/lrleon/uconv/blob/master/LICENSE). COPYRIGHT -(c) 2018, Leandro Rabindranath Leon, Ixhel Mejias and Alberto Valderrama +`uconv` is licensed under GNU GPL v3. +See `LICENSE`. diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 57c24f4..0000000 --- a/TODO.md +++ /dev/null @@ -1 +0,0 @@ -# TODO list diff --git a/TODO.txt b/TODO.txt deleted file mode 100644 index e69de29..0000000 diff --git a/Tests/80.txt b/Tests/80.txt deleted file mode 100644 index a2d9b19..0000000 --- a/Tests/80.txt +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt new file mode 100644 index 0000000..dd3a10a --- /dev/null +++ b/Tests/CMakeLists.txt @@ -0,0 +1,76 @@ + +find_package(GTest REQUIRED) +find_package(Threads REQUIRED) + +include_directories(${GTEST_INCLUDE_DIRS}) + +# Find Aleph and other dependencies +find_library(ALEPH_LIB Aleph PATHS ${ALEPHW} NO_DEFAULT_PATH) +find_library(GSL_LIB gsl) +find_library(GSLCBLAS_LIB gslcblas) +find_library(M_LIB m) + +if(NOT ALEPH_LIB) + message(FATAL_ERROR "Aleph library not found in ${ALEPHW}") +endif() + +# Test sources +set(TEST_SOURCES + quantity.cc + unititem.cc + line.cc + compound.cc + general_units.cc + error_paths.cc + uconv_api.cc + numeric_robustness.cc + api_ops_legacy.cc + coverage_drivers.cc +) + +foreach(test_src ${TEST_SOURCES}) + get_filename_component(test_name ${test_src} NAME_WE) + add_executable(Test_${test_name} ${test_src}) + add_dependencies(Test_${test_name} uconv) + target_link_libraries(Test_${test_name} uconv ${ALEPH_LIB} ${GSL_LIB} ${GSLCBLAS_LIB} ${M_LIB} GTest::GTest GTest::Main Threads::Threads) + add_test(NAME ${test_name} COMMAND Test_${test_name}) +endforeach() + +# DSL negative tests: ensure the generator rejects invalid fixtures. +set(GEN_UNITS ${CMAKE_SOURCE_DIR}/bin/gen-units) +set(DSL_INVALID_DIR ${CMAKE_SOURCE_DIR}/Tests/dsl-invalid) +set(DSL_INVALID_OUT ${CMAKE_BINARY_DIR}/dsl-invalid) +set(DSL_NEGATIVE_RUNNER ${CMAKE_SOURCE_DIR}/Tests/run_dsl_negative_case.sh) + +function(add_dsl_negative_test test_name fixture_file expected_message) + add_test( + NAME ${test_name} + COMMAND bash ${DSL_NEGATIVE_RUNNER} + ${GEN_UNITS} + ${DSL_INVALID_DIR}/${fixture_file} + ${DSL_INVALID_OUT}/${test_name} + ${expected_message} + ) +endfunction() + +add_dsl_negative_test(dsl_invalid_missing_symbol missing_symbol.rb "Missing symbol for") +add_dsl_negative_test(dsl_invalid_unknown_quantity unknown_quantity.rb "Unknown physical quantity") +add_dsl_negative_test(dsl_invalid_cross_quantity_conversion cross_quantity_conversion.rb "Cross-quantity conversion") +add_dsl_negative_test(dsl_invalid_missing_coverage missing_coverage.rb "Missing conversion coverage") +add_dsl_negative_test(dsl_invalid_missing_latex missing_latex.rb "Missing latex for") +add_dsl_negative_test(dsl_invalid_missing_description missing_description.rb "Missing description for") +add_dsl_negative_test(dsl_invalid_missing_range missing_range.rb "Missing range for") +add_dsl_negative_test(dsl_invalid_invalid_range invalid_range.rb "Invalid range (min > max)") +add_dsl_negative_test(dsl_invalid_duplicate_unit_symbol duplicate_unit_symbol.rb "Duplicated unit symbol") +add_dsl_negative_test(dsl_invalid_duplicate_unit_name duplicate_unit_name.rb "Duplicated unit name") +add_dsl_negative_test(dsl_invalid_duplicate_pq_name duplicate_pq_name.rb "Duplicated physical quantity name") +add_dsl_negative_test(dsl_invalid_duplicate_pq_symbol duplicate_pq_symbol.rb "Duplicated physical quantity symbol") +add_dsl_negative_test(dsl_invalid_duplicate_conversion_pair duplicate_conversion_pair.rb "Duplicated conversion pair") +add_dsl_negative_test(dsl_invalid_type_name_collision type_name_collision.rb "Type name collision") +add_dsl_negative_test(dsl_invalid_self_conversion self_conversion.rb "Explicit self-conversion is not allowed") +add_dsl_negative_test(dsl_invalid_unknown_source_unit unknown_source_unit.rb "Unknown source unit") +add_dsl_negative_test(dsl_invalid_unknown_target_unit unknown_target_unit.rb "Unknown target unit") +add_dsl_negative_test(dsl_invalid_missing_formula missing_formula.rb "Missing conversion formula block") +add_dsl_negative_test(dsl_invalid_missing_quantity_reference missing_quantity_reference.rb "Missing quantity reference for") +add_dsl_negative_test(dsl_invalid_non_numeric_range non_numeric_range.rb "Range values must be numeric") +add_dsl_negative_test(dsl_invalid_parse_error parse_error.rb "syntax error") diff --git a/Tests/api_ops_legacy.cc b/Tests/api_ops_legacy.cc new file mode 100644 index 0000000..c40db3b --- /dev/null +++ b/Tests/api_ops_legacy.cc @@ -0,0 +1,184 @@ +/* + _ _ ___ ___ _ ____ __ + | | | |/ __/ _ \| '_ \ \ / / C++ Physical magnitudes and units conversion + | |_| | (_| (_) | | | \ V / Version 1.0 + \__,_|\___\___/|_| |_|\_/ https://github.com/lrleon/uconv +*/ +# include + +# include +# include +# include + +# include + +UnitsInstancer api_ops_legacy_init; + +Declare_Physical_Quantity(LegacyLengthPQ, "LegL", "LegL", + "Legacy length quantity for API compatibility tests"); +Declare_Physical_Quantity(LegacyTimePQ, "LegT", "LegT", + "Legacy time quantity for API compatibility tests"); +Declare_Physical_Quantity(LegacyAreaPQ, "LegA", "LegA", + "Legacy area quantity for API compatibility tests"); +Declare_Physical_Quantity(LegacyVelocityPQ, "LegV", "LegV", + "Legacy velocity quantity for API compatibility tests"); + +Declare_Unit(LegacyLengthUnit, "leg_lu", "leg_lu", "legacy length unit", + LegacyLengthPQ, 0, 1e6); +Declare_Unit(LegacyTimeUnit, "leg_tu", "leg_tu", "legacy time unit", + LegacyTimePQ, 1e-6, 1e6); +Declare_Product_Unit(LegacyAreaUnit, "leg_au", "legacy area unit", + LegacyAreaPQ, 0, 1e12, LegacyLengthUnit, LegacyTimeUnit); +Declare_Ratio_Unit(LegacyVelocityUnit, "leg_vu", "legacy velocity unit", + LegacyVelocityPQ, 0, 1e12, LegacyLengthUnit, LegacyTimeUnit); + +TEST(ApiOpsLegacy, LegacyCompoundLookupAlias) +{ + const auto * const product = search_product_compound_unit( + LegacyLengthUnit::get_instance().name, + LegacyTimeUnit::get_instance().name); + ASSERT_NE(product, nullptr); + + const auto * const legacy = search_compound_unit( + LegacyLengthUnit::get_instance().name, + LegacyTimeUnit::get_instance().name); + ASSERT_NE(legacy, nullptr); + + EXPECT_EQ(product, legacy); + EXPECT_EQ(legacy, &LegacyAreaUnit::get_instance()); +} + +TEST(ApiOpsLegacy, LegacyVerifyCompoundAlias) +{ + const Unit & via_legacy = VtlQuantity::verify_compound( + LegacyLengthUnit::get_instance(), + LegacyTimeUnit::get_instance()); + + const Unit & via_product = VtlQuantity::verify_product_compound( + LegacyLengthUnit::get_instance(), + LegacyTimeUnit::get_instance()); + + EXPECT_EQ(&via_legacy, &via_product); + EXPECT_EQ(&via_legacy, &LegacyAreaUnit::get_instance()); +} + +TEST(ApiOpsLegacy, CombineUnitsAlias) +{ + static_assert(std::is_same::type, + LegacyVelocityUnit>::value, + "Combine_Units alias must map to Divide_Units"); + + const Quantity length = 24.0; + const Quantity time = 6.0; + Quantity::type> velocity = + length / time; + EXPECT_NEAR(velocity.raw(), 4.0, 1e-12); +} + +TEST(ApiOpsLegacy, UnitFormattingAndBoundsHelpers) +{ + const Unit & m = meter::get_instance(); + + const std::string formatted = m.to_string(48, 2); + EXPECT_NE(formatted.find(" Unit name"), std::string::npos); + EXPECT_NE(formatted.find("description"), std::string::npos); + EXPECT_NE(formatted.find("physical quantity"), std::string::npos); + + const VtlQuantity min_q = m.min(); + const VtlQuantity max_q = m.max(); + EXPECT_EQ(&min_q.unit, &m); + EXPECT_EQ(&max_q.unit, &m); + EXPECT_DOUBLE_EQ(min_q.raw(), m.min_val); + EXPECT_DOUBLE_EQ(max_q.raw(), m.max_val); + + EXPECT_DOUBLE_EQ(bind_to_unit_limits(m.min_val - 10.0, m), m.min_val); + EXPECT_DOUBLE_EQ(bind_to_unit_limits(m.max_val + 10.0, m), m.max_val); + EXPECT_DOUBLE_EQ(bind_to_unit_limits(12.5, m), 12.5); +} + +TEST(ApiOpsLegacy, QuantityScalarOverloads) +{ + const Quantity q = 2.0; + + EXPECT_NEAR((1.5 + q).raw(), 3.5, 1e-12); + EXPECT_NEAR((5.0 - q).raw(), 3.0, 1e-12); + EXPECT_NEAR((2.0 * q).raw(), 4.0, 1e-12); + EXPECT_NEAR((q * 3.0).raw(), 6.0, 1e-12); + EXPECT_NEAR((10.0 / q).raw(), 5.0, 1e-12); + EXPECT_NEAR((q / 2.0).raw(), 1.0, 1e-12); + + EXPECT_TRUE(q < 3.0); + EXPECT_TRUE(3.0 > q); + EXPECT_TRUE(q <= 2.0); + EXPECT_TRUE(2.0 >= q); + EXPECT_TRUE(q == 2.0); + EXPECT_TRUE(2.0 == q); + EXPECT_TRUE(q != 1.0); + EXPECT_TRUE(1.0 != q); +} + +TEST(ApiOpsLegacy, QuantityVtlAndMathHelpers) +{ + Quantity m = 2.0; + const VtlQuantity one_m("m", 1.0); + + const VtlQuantity sum = m + one_m; + const VtlQuantity diff = m - one_m; + EXPECT_EQ(&sum.unit, &meter::get_instance()); + EXPECT_EQ(&diff.unit, &meter::get_instance()); + EXPECT_NEAR(sum.raw(), 3.0, 1e-12); + EXPECT_NEAR(diff.raw(), 1.0, 1e-12); + + m += one_m; + EXPECT_NEAR(m.raw(), 3.0, 1e-12); + m -= one_m; + EXPECT_NEAR(m.raw(), 2.0, 1e-12); + + const Quantity q = 2.0; + EXPECT_NEAR(pow2(q), 4.0, 1e-12); + EXPECT_NEAR(pow3(q), 8.0, 1e-12); + EXPECT_NEAR(pow(q, 2.0), 4.0, 1e-12); + EXPECT_NEAR(powl(q, 2.0L), 4.0, 1e-12); + + const Quantity one = 1.0; + EXPECT_NEAR(exp(one), std::exp(1.0), 1e-12); + EXPECT_NEAR(log(one), 0.0, 1e-12); + EXPECT_NEAR(log10(one), 0.0, 1e-12); + EXPECT_NEAR(sqrt(one), 1.0, 1e-12); + EXPECT_NEAR(cbrt(one), 1.0, 1e-12); +} + +TEST(ApiOpsLegacy, VtlScalarOverloadsAndNextPrev) +{ + VtlQuantity v("m", 2.0); + + EXPECT_NEAR((1.0 + v).raw(), 3.0, 1e-12); + EXPECT_NEAR((v + 1.0).raw(), 3.0, 1e-12); + EXPECT_NEAR((5.0 - v).raw(), 3.0, 1e-12); + EXPECT_NEAR((v - 1.0).raw(), 1.0, 1e-12); + EXPECT_NEAR((2.0 * v).raw(), 4.0, 1e-12); + EXPECT_NEAR((v * 2.0).raw(), 4.0, 1e-12); + EXPECT_NEAR((8.0 / v).raw(), 4.0, 1e-12); + EXPECT_NEAR((v / 2.0).raw(), 1.0, 1e-12); + + EXPECT_TRUE(v < 3.0); + EXPECT_TRUE(3.0 > v); + EXPECT_TRUE(v <= 2.0); + EXPECT_TRUE(2.0 >= v); + EXPECT_TRUE(v == 2.0); + EXPECT_TRUE(2.0 == v); + EXPECT_TRUE(v != 1.0); + EXPECT_TRUE(1.0 != v); + + const auto q_next = next_value(v); + const auto q_prev = prev_value(v); + EXPECT_EQ(&q_next.unit, &v.unit); + EXPECT_EQ(&q_prev.unit, &v.unit); + EXPECT_GT(q_next.raw(), v.raw()); + EXPECT_LT(q_prev.raw(), v.raw()); + + const Quantity q = 2.0; + EXPECT_GT(q.next().raw(), q.raw()); + EXPECT_LT(q.prev().raw(), q.raw()); +} diff --git a/Tests/compound.cc b/Tests/compound.cc new file mode 100644 index 0000000..309e7c2 --- /dev/null +++ b/Tests/compound.cc @@ -0,0 +1,91 @@ +/* + _ _ ___ ___ _ ____ __ + | | | |/ __/ _ \| '_ \ \ / / C++ Physical magnitudes and units conversion + | |_| | (_| (_) | | | \ V / Version 1.0 + \__,_|\___\___/|_| |_|\_/ https://github.com/lrleon/uconv + + This file is part of uconv library + + Copyright (c) 2018 Ixhel Mejias + Alberto Valderrama + Leandro Rabindranath Leon + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +# include + +# include + +Declare_Physical_Quantity(CmpLengthPQ, "CmpL", "CmpL", + "Compound length quantity for testing"); +Declare_Physical_Quantity(CmpTimePQ, "CmpT", "CmpT", + "Compound time quantity for testing"); +Declare_Physical_Quantity(CmpVelocityPQ, "CmpV", "CmpV", + "Compound velocity quantity for testing"); +Declare_Physical_Quantity(CmpAreaPQ, "CmpA", "CmpA", + "Compound area quantity for testing"); + +Declare_Unit(CmpLengthUnit, "cmp_lu", "cmp_lu", "compound length unit", + CmpLengthPQ, 0, 1e9); +Declare_Unit(CmpTimeUnit, "cmp_tu", "cmp_tu", "compound time unit", + CmpTimePQ, 1e-9, 1e9); +Declare_Product_Unit(CmpAreaUnit, "cmp_au", "compound area unit", + CmpAreaPQ, 0, 1e12, CmpLengthUnit, CmpTimeUnit); +Declare_Ratio_Unit(CmpVelocityUnit, "cmp_vu", "compound velocity unit", + CmpVelocityPQ, 0, 1e9, CmpLengthUnit, CmpTimeUnit); + +TEST(Compound, RuntimeLookupProduct) +{ + const Unit & u1 = VtlQuantity::verify_product_compound(CmpLengthUnit::get_instance(), + CmpTimeUnit::get_instance()); + EXPECT_EQ(&u1, &CmpAreaUnit::get_instance()); + + const Unit & u2 = VtlQuantity::verify_product_compound(CmpTimeUnit::get_instance(), + CmpLengthUnit::get_instance()); + EXPECT_EQ(&u2, &CmpAreaUnit::get_instance()); +} + +TEST(Compound, RuntimeLookupRatio) +{ + const Unit & u = VtlQuantity::verify_ratio_compound(CmpLengthUnit::get_instance(), + CmpTimeUnit::get_instance()); + EXPECT_EQ(&u, &CmpVelocityUnit::get_instance()); + + EXPECT_THROW(VtlQuantity::verify_ratio_compound(CmpTimeUnit::get_instance(), + CmpLengthUnit::get_instance()), + std::exception); +} + +TEST(Compound, QuantityOps) +{ + Quantity length = 120; + Quantity time = 6; + Quantity velocity = length / time; + + EXPECT_NEAR(velocity.raw(), 20, 1e-12); +} + +TEST(Compound, VtlQuantityOps) +{ + VtlQuantity length(CmpLengthUnit::get_instance(), 120); + VtlQuantity time(CmpTimeUnit::get_instance(), 6); + + const VtlQuantity velocity = length / time; + const VtlQuantity area = length * time; + + EXPECT_EQ(&velocity.unit, &CmpVelocityUnit::get_instance()); + EXPECT_NEAR(velocity.raw(), 20, 1e-12); + EXPECT_EQ(&area.unit, &CmpAreaUnit::get_instance()); + EXPECT_NEAR(area.raw(), 720, 1e-12); +} diff --git a/Tests/coverage_drivers.cc b/Tests/coverage_drivers.cc new file mode 100644 index 0000000..a825a66 --- /dev/null +++ b/Tests/coverage_drivers.cc @@ -0,0 +1,225 @@ +/* + _ _ ___ ___ _ ____ __ + | | | |/ __/ _ \| '_ \ \ / / C++ Physical magnitudes and units conversion + | |_| | (_| (_) | | | \ V / Version 1.0 + \__,_|\___\___/|_| |_|\_/ https://github.com/lrleon/uconv +*/ +# include + +# include +# include +# include + +# include + +UnitsInstancer coverage_drivers_init; + +Declare_Physical_Quantity(CovMissingPQ, "CovM", "CovM", + "Physical quantity used to drive missing conversion paths"); +Declare_Unit(CovMissingA, "cov_a", "cov_a", "Coverage unit A", + CovMissingPQ, 0, 1000); +Declare_Unit(CovMissingB, "cov_b", "cov_b", "Coverage unit B", + CovMissingPQ, 0, 1000); +Declare_Conversion(CovMissingA, CovMissingB, V) { return 2.0 * V; } +UnitConverter __uc__CovMissingA__to__CovMissingB; + +namespace +{ + template + std::size_t dynlist_size(const DynList & list) + { + std::size_t n = 0; + for (auto it = list.get_it(); it.has_curr(); it.next()) + ++n; + return n; + } + + bool dynlist_contains(const DynList & list, const std::string & value) + { + for (auto it = list.get_it(); it.has_curr(); it.next()) + if (it.get_curr() == value) + return true; + return false; + } +} + +TEST(CoverageDrivers, StringApisAndMetadata) +{ + const auto pq_names = PhysicalQuantity::names(); + EXPECT_TRUE(dynlist_contains(pq_names, "Length")); + + const Unit & m = meter::get_instance(); + EXPECT_GE(dynlist_size(m.family_units()), 1u); + + const std::string unit_dump = m.to_string(); + EXPECT_NE(unit_dump.find("Unit name"), std::string::npos); + EXPECT_NE(unit_dump.find("symbol"), std::string::npos); + + std::ostringstream unit_stream; + unit_stream << m; + EXPECT_NE(unit_stream.str().find("Unit name"), std::string::npos); + + const VtlQuantity q("m", 12.5); + const std::string quantity_dump = q.to_string(); + EXPECT_NE(quantity_dump.find("m"), std::string::npos); + + std::ostringstream quantity_stream; + quantity_stream << q; + EXPECT_NE(quantity_stream.str().find("m"), std::string::npos); +} + +TEST(CoverageDrivers, ConversionErrorBranches) +{ + DynList values; + values.append(1.0); + values.append(2.0); + + const auto same = unit_convert(meter::get_instance(), values, meter::get_instance()); + EXPECT_EQ(dynlist_size(same), dynlist_size(values)); + + auto mutable_same = values; + EXPECT_NO_THROW(mutable_unit_convert(meter::get_instance(), mutable_same, + meter::get_instance())); + EXPECT_EQ(dynlist_size(mutable_same), dynlist_size(values)); + + EXPECT_THROW(unit_convert(meter::get_instance(), values, second::get_instance()), + std::exception); + auto mutable_missing = values; + EXPECT_THROW(mutable_unit_convert(meter::get_instance(), mutable_missing, + second::get_instance()), + std::exception); + EXPECT_THROW(unit_convert(meter::get_instance(), 1.0, second::get_instance()), + std::exception); + + EXPECT_THROW(unit_convert_name_to_name("unknown_src_name", 1.0, "meter"), + std::exception); + EXPECT_THROW(unit_convert_name_to_name("meter", 1.0, "unknown_tgt_name"), + std::exception); + EXPECT_THROW(unit_convert_name_to_name("meter", 1.0, "second"), + std::exception); + + EXPECT_THROW(unit_convert_symbol_to_symbol("unknown_src_symbol", 1.0, "m"), + std::exception); + EXPECT_THROW(unit_convert_symbol_to_symbol("m", 1.0, "unknown_tgt_symbol"), + std::exception); + EXPECT_THROW(unit_convert_symbol_to_symbol("m", 1.0, "sec"), + std::exception); +} + +TEST(CoverageDrivers, CheckConversionsNegativePath) +{ + const auto report = check_conversions(CovMissingPQ::get_instance()); + EXPECT_FALSE(report.first); + EXPECT_FALSE(report.second.is_empty()); + + bool found_missing_message = false; + for (auto it = report.second.get_it(); it.has_curr(); it.next()) + { + if (it.get_curr().find("Missing conversion from") != std::string::npos) + { + found_missing_message = true; + break; + } + } + EXPECT_TRUE(found_missing_message); +} + +TEST(CoverageDrivers, VtlQuantityConstructorsAndAssignments) +{ + const Quantity qft = 3.0; + + const VtlQuantity same_symbol("ft", qft); + const VtlQuantity diff_symbol("m", qft); + const VtlQuantity same_unit(foot::get_instance(), qft); + const VtlQuantity diff_unit(meter::get_instance(), qft); + + EXPECT_NEAR(same_symbol.raw(), 3.0, 1e-12); + EXPECT_NEAR(same_unit.raw(), 3.0, 1e-12); + EXPECT_NEAR(diff_symbol.raw(), 0.9144, 1e-9); + EXPECT_NEAR(diff_unit.raw(), 0.9144, 1e-9); + + VtlQuantity self_assign("m", 1.0); + const VtlQuantity & self_ref = self_assign; + self_assign = self_ref; + EXPECT_NEAR(self_assign.raw(), 1.0, 1e-12); + + VtlQuantity null_lhs; + const VtlQuantity rhs("m", 2.0); + null_lhs = rhs; + EXPECT_FALSE(null_lhs.is_null()); + EXPECT_EQ(&null_lhs.unit, &meter::get_instance()); + EXPECT_NEAR(null_lhs.raw(), 2.0, 1e-12); + + VtlQuantity lhs("m", 3.0); + const VtlQuantity null_rhs; + EXPECT_THROW(lhs = null_rhs, std::exception); + EXPECT_TRUE(lhs.is_null()); + + VtlQuantity same_lhs("m", 1.0); + const VtlQuantity same_rhs("m", 2.0); + same_lhs = same_rhs; + EXPECT_NEAR(same_lhs.raw(), 2.0, 1e-12); + + VtlQuantity diff_lhs("m", 1.0); + const VtlQuantity diff_rhs("ft", 3.0); + diff_lhs = diff_rhs; + EXPECT_NEAR(diff_lhs.raw(), 0.9144, 1e-9); +} + +TEST(CoverageDrivers, VtlQuantityDifferentUnitOps) +{ + const VtlQuantity one_m("m", 1.0); + const VtlQuantity one_m_in_ft("ft", 3.280839895013123); + const VtlQuantity two_m_in_ft("ft", 6.561679790026246); + + EXPECT_NEAR((one_m + one_m_in_ft).raw(), 2.0, 1e-9); + EXPECT_NEAR((one_m - one_m_in_ft).raw(), 0.0, 1e-9); + + VtlQuantity acc("m", 1.0); + acc += one_m_in_ft; + EXPECT_NEAR(acc.raw(), 2.0, 1e-9); + acc -= one_m_in_ft; + EXPECT_NEAR(acc.raw(), 1.0, 1e-9); + + EXPECT_TRUE(one_m < two_m_in_ft); + EXPECT_FALSE(two_m_in_ft < one_m); +} + +TEST(CoverageDrivers, LookupNormalizationAndAliasPaths) +{ + const Unit * spaced = Unit::search_by_symbol(" \t m \n"); + ASSERT_NE(spaced, nullptr); + EXPECT_EQ(spaced->symbol, "m"); + + const Unit * omega = Unit::search_by_symbol("\xCE\xA9"); // Omega symbol + ASSERT_NE(omega, nullptr); + EXPECT_EQ(omega->symbol, "ohm"); + + EXPECT_EQ(Unit::search_by_symbol(" \t "), nullptr); + + EXPECT_FALSE(exist_conversion("m", "unknown_target_symbol")); + EXPECT_EQ(search_conversion_fct("unknown_source_symbol", "m"), nullptr); + EXPECT_EQ(search_conversion_fct("m", "unknown_target_symbol"), nullptr); +} + +TEST(CoverageDrivers, BaseQuantityValidationEdgeCases) +{ + const Unit & m = meter::get_instance(); + const double just_over_max = m.max_val + 0.5 * m.get_epsilon(); + EXPECT_TRUE(BaseQuantity::is_valid(just_over_max, m)); + + const double nan_value = std::numeric_limits::quiet_NaN(); + EXPECT_TRUE(BaseQuantity::is_valid(nan_value, Unit::null_unit)); + + VtlQuantity null_q; + EXPECT_THROW(null_q = 1.0, std::exception); +} + +TEST(CoverageDrivers, VtlQuantitySameUnitComparisonPath) +{ + const VtlQuantity one_m("m", 1.0); + const VtlQuantity two_m("m", 2.0); + + EXPECT_TRUE(one_m < two_m); + EXPECT_FALSE(two_m < one_m); +} diff --git a/Tests/dsl-invalid/cross_quantity_conversion.rb b/Tests/dsl-invalid/cross_quantity_conversion.rb new file mode 100644 index 0000000..c982212 --- /dev/null +++ b/Tests/dsl-invalid/cross_quantity_conversion.rb @@ -0,0 +1,31 @@ +physical_quantity :LengthX do + symbol "LX" + latex "LX" + description "Length fixture" +end + +physical_quantity :TimeX do + symbol "TX" + latex "TX" + description "Time fixture" +end + +unit :MeterX do + symbol "mx" + latex "mx" + description "Length unit fixture" + quantity :LengthX + range 0..100 +end + +unit :SecondX do + symbol "sx" + latex "sx" + description "Time unit fixture" + quantity :TimeX + range 0..100 +end + +conversion :MeterX, :SecondX do |v| + v +end diff --git a/Tests/dsl-invalid/duplicate_conversion_pair.rb b/Tests/dsl-invalid/duplicate_conversion_pair.rb new file mode 100644 index 0000000..8dd8b51 --- /dev/null +++ b/Tests/dsl-invalid/duplicate_conversion_pair.rb @@ -0,0 +1,33 @@ +physical_quantity :QDupConv do + symbol "QDC" + latex "QDC" + description "Fixture for duplicate conversion pair validation" +end + +unit :DupConvA do + symbol "dca" + latex "dca" + description "Unit A" + quantity :QDupConv + range 0..10 +end + +unit :DupConvB do + symbol "dcb" + latex "dcb" + description "Unit B" + quantity :QDupConv + range 0..10 +end + +conversion :DupConvA, :DupConvB do |v| + v +end + +conversion :DupConvA, :DupConvB do |v| + "2.0 * #{v}" +end + +conversion :DupConvB, :DupConvA do |v| + "0.5 * #{v}" +end diff --git a/Tests/dsl-invalid/duplicate_pq_name.rb b/Tests/dsl-invalid/duplicate_pq_name.rb new file mode 100644 index 0000000..cf76b5e --- /dev/null +++ b/Tests/dsl-invalid/duplicate_pq_name.rb @@ -0,0 +1,11 @@ +physical_quantity :QDupPQName do + symbol "QDPN1" + latex "QDPN1" + description "First physical quantity with duplicate name" +end + +physical_quantity :QDupPQName do + symbol "QDPN2" + latex "QDPN2" + description "Second physical quantity with duplicate name" +end diff --git a/Tests/dsl-invalid/duplicate_pq_symbol.rb b/Tests/dsl-invalid/duplicate_pq_symbol.rb new file mode 100644 index 0000000..9e4e03b --- /dev/null +++ b/Tests/dsl-invalid/duplicate_pq_symbol.rb @@ -0,0 +1,11 @@ +physical_quantity :QDupPQSymbolA do + symbol "QDPS" + latex "QDPSA" + description "First physical quantity with duplicate symbol" +end + +physical_quantity :QDupPQSymbolB do + symbol "QDPS" + latex "QDPSB" + description "Second physical quantity with duplicate symbol" +end diff --git a/Tests/dsl-invalid/duplicate_unit_name.rb b/Tests/dsl-invalid/duplicate_unit_name.rb new file mode 100644 index 0000000..f8f5cd9 --- /dev/null +++ b/Tests/dsl-invalid/duplicate_unit_name.rb @@ -0,0 +1,21 @@ +physical_quantity :QDupName do + symbol "QDN" + latex "QDN" + description "Fixture for duplicate unit name validation" +end + +unit :UnitDupName do + symbol "udn_a" + latex "udn_a" + description "First unit with duplicate name" + quantity :QDupName + range 0..10 +end + +unit :UnitDupName do + symbol "udn_b" + latex "udn_b" + description "Second unit with duplicate name" + quantity :QDupName + range 0..10 +end diff --git a/Tests/dsl-invalid/duplicate_unit_symbol.rb b/Tests/dsl-invalid/duplicate_unit_symbol.rb new file mode 100644 index 0000000..807b82e --- /dev/null +++ b/Tests/dsl-invalid/duplicate_unit_symbol.rb @@ -0,0 +1,21 @@ +physical_quantity :QDupSymbol do + symbol "QDS" + latex "QDS" + description "Fixture for duplicate unit symbol validation" +end + +unit :UnitDupSymbolA do + symbol "dup" + latex "dupA" + description "Unit A" + quantity :QDupSymbol + range 0..10 +end + +unit :UnitDupSymbolB do + symbol "dup" + latex "dupB" + description "Unit B" + quantity :QDupSymbol + range 0..10 +end diff --git a/Tests/dsl-invalid/invalid_range.rb b/Tests/dsl-invalid/invalid_range.rb new file mode 100644 index 0000000..1f9a782 --- /dev/null +++ b/Tests/dsl-invalid/invalid_range.rb @@ -0,0 +1,13 @@ +physical_quantity :QInvalidRange do + symbol "QIR" + latex "QIR" + description "Fixture for invalid range validation" +end + +unit :UnitInvalidRange do + symbol "uir" + latex "uir" + description "Unit with min > max" + quantity :QInvalidRange + range 10..0 +end diff --git a/Tests/dsl-invalid/missing_coverage.rb b/Tests/dsl-invalid/missing_coverage.rb new file mode 100644 index 0000000..2cc7f4d --- /dev/null +++ b/Tests/dsl-invalid/missing_coverage.rb @@ -0,0 +1,25 @@ +physical_quantity :LengthCoverage do + symbol "LC" + latex "LC" + description "Fixture for missing conversion coverage validation" +end + +unit :MeterCoverage do + symbol "mc" + latex "mc" + description "Unit A" + quantity :LengthCoverage + range 0..100 +end + +unit :FootCoverage do + symbol "fc" + latex "fc" + description "Unit B" + quantity :LengthCoverage + range 0..300 +end + +conversion :MeterCoverage, :FootCoverage do |v| + "3.28083989501312 * #{v}" +end diff --git a/Tests/dsl-invalid/missing_description.rb b/Tests/dsl-invalid/missing_description.rb new file mode 100644 index 0000000..a6b599e --- /dev/null +++ b/Tests/dsl-invalid/missing_description.rb @@ -0,0 +1,12 @@ +physical_quantity :QDescription do + symbol "QD" + latex "QD" +end + +unit :UnitWithDescription do + symbol "uwd" + latex "uwd" + description "Helper unit" + quantity :QDescription + range 0..10 +end diff --git a/Tests/dsl-invalid/missing_formula.rb b/Tests/dsl-invalid/missing_formula.rb new file mode 100644 index 0000000..ae3b6bc --- /dev/null +++ b/Tests/dsl-invalid/missing_formula.rb @@ -0,0 +1,23 @@ +physical_quantity :QFormula do + symbol "QF" + latex "QF" + description "Fixture for missing conversion formula validation" +end + +unit :UnitFormulaA do + symbol "ufa" + latex "ufa" + description "Unit A" + quantity :QFormula + range 0..10 +end + +unit :UnitFormulaB do + symbol "ufb" + latex "ufb" + description "Unit B" + quantity :QFormula + range 0..10 +end + +conversion :UnitFormulaA, :UnitFormulaB diff --git a/Tests/dsl-invalid/missing_latex.rb b/Tests/dsl-invalid/missing_latex.rb new file mode 100644 index 0000000..00fd38c --- /dev/null +++ b/Tests/dsl-invalid/missing_latex.rb @@ -0,0 +1,12 @@ +physical_quantity :QLatex do + symbol "QL" + latex "QL" + description "Fixture for missing latex validation" +end + +unit :UnitMissingLatex do + symbol "uml" + description "Unit with missing latex" + quantity :QLatex + range 0..10 +end diff --git a/Tests/dsl-invalid/missing_quantity_reference.rb b/Tests/dsl-invalid/missing_quantity_reference.rb new file mode 100644 index 0000000..2b85490 --- /dev/null +++ b/Tests/dsl-invalid/missing_quantity_reference.rb @@ -0,0 +1,12 @@ +physical_quantity :QMissingRef do + symbol "QMR" + latex "QMR" + description "Fixture for missing quantity reference validation" +end + +unit :UnitMissingQuantityReference do + symbol "umqr" + latex "umqr" + description "Unit intentionally missing quantity reference" + range 0..10 +end diff --git a/Tests/dsl-invalid/missing_range.rb b/Tests/dsl-invalid/missing_range.rb new file mode 100644 index 0000000..9426b7d --- /dev/null +++ b/Tests/dsl-invalid/missing_range.rb @@ -0,0 +1,12 @@ +physical_quantity :QRange do + symbol "QR" + latex "QR" + description "Fixture for missing range validation" +end + +unit :UnitMissingRange do + symbol "umr" + latex "umr" + description "Unit missing range" + quantity :QRange +end diff --git a/Tests/dsl-invalid/missing_symbol.rb b/Tests/dsl-invalid/missing_symbol.rb new file mode 100644 index 0000000..e1ba957 --- /dev/null +++ b/Tests/dsl-invalid/missing_symbol.rb @@ -0,0 +1,13 @@ +physical_quantity :LengthBad do + symbol "LB" + latex "LB" + description "Fixture for missing symbol validation" +end + +unit :MeterBad do + # symbol is intentionally missing + latex "m" + description "Broken unit fixture" + quantity :LengthBad + range 0..100 +end diff --git a/Tests/dsl-invalid/non_numeric_range.rb b/Tests/dsl-invalid/non_numeric_range.rb new file mode 100644 index 0000000..fb12052 --- /dev/null +++ b/Tests/dsl-invalid/non_numeric_range.rb @@ -0,0 +1,13 @@ +physical_quantity :QNonNumericRange do + symbol "QNNR" + latex "QNNR" + description "Fixture for non numeric range validation" +end + +unit :UnitNonNumericRange do + symbol "unnr" + latex "unnr" + description "Unit with non numeric range" + quantity :QNonNumericRange + range "a".."z" +end diff --git a/Tests/dsl-invalid/parse_error.rb b/Tests/dsl-invalid/parse_error.rb new file mode 100644 index 0000000..33b2db0 --- /dev/null +++ b/Tests/dsl-invalid/parse_error.rb @@ -0,0 +1,13 @@ +physical_quantity :QParse do + symbol "QP" + latex "QP" + description "Fixture that triggers a parser error" +end + +unit :UnitParseError do + symbol "upe" + latex "upe" + description "Broken fixture" + quantity :QParse + range 0..10 +# Intentionally missing `end` diff --git a/Tests/dsl-invalid/self_conversion.rb b/Tests/dsl-invalid/self_conversion.rb new file mode 100644 index 0000000..7be999e --- /dev/null +++ b/Tests/dsl-invalid/self_conversion.rb @@ -0,0 +1,17 @@ +physical_quantity :QSelf do + symbol "QS" + latex "QS" + description "Fixture for self conversion validation" +end + +unit :UnitSelf do + symbol "us" + latex "us" + description "Single unit" + quantity :QSelf + range 0..10 +end + +conversion :UnitSelf, :UnitSelf do |v| + v +end diff --git a/Tests/dsl-invalid/type_name_collision.rb b/Tests/dsl-invalid/type_name_collision.rb new file mode 100644 index 0000000..3f2a1d9 --- /dev/null +++ b/Tests/dsl-invalid/type_name_collision.rb @@ -0,0 +1,13 @@ +physical_quantity :CollisionType do + symbol "CT" + latex "CT" + description "Fixture for type name collision validation" +end + +unit :CollisionType do + symbol "ctu" + latex "ctu" + description "Unit with colliding type name" + quantity :CollisionType + range 0..10 +end diff --git a/Tests/dsl-invalid/unknown_quantity.rb b/Tests/dsl-invalid/unknown_quantity.rb new file mode 100644 index 0000000..fa71499 --- /dev/null +++ b/Tests/dsl-invalid/unknown_quantity.rb @@ -0,0 +1,13 @@ +physical_quantity :LengthOk do + symbol "LQ" + latex "LQ" + description "Fixture for unknown quantity reference validation" +end + +unit :MeterUnknownQuantity do + symbol "muq" + latex "muq" + description "Unit points to a non-existent quantity" + quantity :DoesNotExist + range 0..100 +end diff --git a/Tests/dsl-invalid/unknown_source_unit.rb b/Tests/dsl-invalid/unknown_source_unit.rb new file mode 100644 index 0000000..97384b8 --- /dev/null +++ b/Tests/dsl-invalid/unknown_source_unit.rb @@ -0,0 +1,17 @@ +physical_quantity :QUnknownSource do + symbol "QUS" + latex "QUS" + description "Fixture for unknown source unit validation" +end + +unit :KnownUnitTarget do + symbol "kut" + latex "kut" + description "Known target unit" + quantity :QUnknownSource + range 0..10 +end + +conversion :MissingSourceUnit, :KnownUnitTarget do |v| + v +end diff --git a/Tests/dsl-invalid/unknown_target_unit.rb b/Tests/dsl-invalid/unknown_target_unit.rb new file mode 100644 index 0000000..3985b04 --- /dev/null +++ b/Tests/dsl-invalid/unknown_target_unit.rb @@ -0,0 +1,17 @@ +physical_quantity :QUnknownTarget do + symbol "QUT" + latex "QUT" + description "Fixture for unknown target unit validation" +end + +unit :KnownUnitSource do + symbol "kus" + latex "kus" + description "Known source unit" + quantity :QUnknownTarget + range 0..10 +end + +conversion :KnownUnitSource, :MissingTargetUnit do |v| + v +end diff --git a/Tests/error_paths.cc b/Tests/error_paths.cc new file mode 100644 index 0000000..1baaa40 --- /dev/null +++ b/Tests/error_paths.cc @@ -0,0 +1,84 @@ +/* + _ _ ___ ___ _ ____ __ + | | | |/ __/ _ \| '_ \ \ / / C++ Physical magnitudes and units conversion + | |_| | (_| (_) | | | \ V / Version 1.0 + \__,_|\___\___/|_| |_|\_/ https://github.com/lrleon/uconv +*/ +# include + +# include + +UnitsInstancer error_paths_init; + +TEST(ErrorPaths, UnknownUnits) +{ + EXPECT_EQ(Unit::search_by_name("definitely_unknown_unit_name"), nullptr); + EXPECT_EQ(Unit::search_by_symbol("definitely_unknown_unit_symbol"), nullptr); + + EXPECT_THROW(VtlQuantity("definitely_unknown_unit_name", 1.0), std::exception); + EXPECT_THROW(unit_convert_name_to_name("meter", 1.0, + "definitely_unknown_unit_name"), + std::exception); + EXPECT_THROW(unit_convert_symbol_to_symbol("m", 1.0, + "definitely_unknown_unit_symbol"), + std::exception); +} + +TEST(ErrorPaths, MissingConversions) +{ + EXPECT_FALSE(exist_conversion("m", "sec")); + EXPECT_FALSE(exist_conversion("meter", "second")); + + EXPECT_THROW(unit_convert_symbol_to_symbol("m", 1.0, "sec"), std::exception); + EXPECT_THROW(unit_convert_name_to_name("meter", 1.0, "second"), std::exception); +} + +TEST(ErrorPaths, OutOfRangeValues) +{ + EXPECT_THROW(VtlQuantity("m", 1e9), std::exception); + EXPECT_THROW(VtlQuantity("degK", -1e3), std::exception); +} + +TEST(ErrorPaths, InvalidEpsilonRatio) +{ + const Unit * const m = Unit::search_by_symbol("m"); + ASSERT_NE(m, nullptr); + + EXPECT_THROW(m->set_epsilon(0.0), std::exception); + EXPECT_THROW(m->set_epsilon(0.31), std::exception); +} + +TEST(ErrorPaths, IncompatibleQuantities) +{ + const VtlQuantity length("m", 2.0); + const VtlQuantity time("sec", 1.0); + + EXPECT_THROW(static_cast(length + time), std::exception); + EXPECT_THROW(static_cast(length - time), std::exception); + EXPECT_THROW(static_cast(length < time), std::exception); +} + +TEST(ErrorPaths, MissingCompoundUnits) +{ + const VtlQuantity temperature("degC", 25.0); + const VtlQuantity length("m", 2.0); + + EXPECT_THROW(static_cast(temperature * length), std::exception); + EXPECT_THROW(static_cast(temperature / length), std::exception); +} + +TEST(ErrorPaths, QuantityAndVtlExactUnitMismatch) +{ + const Quantity m = 1.0; + const VtlQuantity ft("ft", 1.0); + + EXPECT_THROW(static_cast(m + ft), std::exception); + EXPECT_THROW(static_cast(m - ft), std::exception); +} + +TEST(ErrorPaths, NullQuantityDoubleAssignment) +{ + VtlQuantity q; + ASSERT_TRUE(q.is_null()); + EXPECT_THROW(q = 42.0, std::exception); +} diff --git a/Tests/general_units.cc b/Tests/general_units.cc new file mode 100644 index 0000000..c317701 --- /dev/null +++ b/Tests/general_units.cc @@ -0,0 +1,296 @@ +/* + _ _ ___ ___ _ ____ __ + | | | |/ __/ _ \| '_ \ \ / / C++ Physical magnitudes and units conversion + | |_| | (_| (_) | | | \ V / Version 1.0 + \__,_|\___\___/|_| |_|\_/ https://github.com/lrleon/uconv +*/ +# include + +# include + +# include + +UnitsInstancer general_units_init; + +namespace +{ + constexpr std::array all_physical_quantities = + { + "Length", + "Temperature", + "Pressure", + "Mass", + "Volume", + "Density", + "MassFlow", + "DynamicViscosity", + "KinematicViscosity", + "Acceleration", + "Frequency", + "Current", + "Voltage", + "Resistance", + "Capacitance", + "Charge", + "Angle", + "Time", + "Area", + "Power", + "Energy", + "Force", + "Torque", + "Diameter", + "FlowRate", + "Velocity", + "Dimension_Less" + }; +} + +TEST(GeneralUnits, PhysicalQuantitiesRegistered) +{ + for (const char * const quantity_name : all_physical_quantities) + { + SCOPED_TRACE(quantity_name); + EXPECT_NE(PhysicalQuantity::search(quantity_name), nullptr); + } +} + +TEST(GeneralUnits, MassConversions) +{ + const Quantity one_kg = 1.0; + const Quantity in_lbm = one_kg; + EXPECT_NEAR(in_lbm.raw(), 2.2046226218487757, 1e-12); + + const Quantity in_g = one_kg; + EXPECT_NEAR(in_g.raw(), 1000.0, 1e-12); + + const Quantity back_to_kg = in_lbm; + EXPECT_NEAR(back_to_kg.raw(), one_kg.raw(), 1e-12); +} + +TEST(GeneralUnits, VolumeConversions) +{ + const Quantity one_m3 = 1.0; + const Quantity in_gal = one_m3; + EXPECT_NEAR(in_gal.raw(), 264.1720523581484, 1e-9); + + const Quantity in_bbl = one_m3; + EXPECT_NEAR(in_bbl.raw(), 6.289810770432105, 1e-12); + + const Quantity back_to_m3 = in_gal; + EXPECT_NEAR(back_to_m3.raw(), one_m3.raw(), 1e-9); +} + +TEST(GeneralUnits, ForceConversions) +{ + const Quantity one_n = 1.0; + const Quantity in_lbf = one_n; + EXPECT_NEAR(in_lbf.raw(), 0.22480894387096263, 1e-12); + + const Quantity in_dyn = one_n; + EXPECT_NEAR(in_dyn.raw(), 1e5, 1e-3); + + const Quantity back_to_n = in_lbf; + EXPECT_NEAR(back_to_n.raw(), one_n.raw(), 1e-8); +} + +TEST(GeneralUnits, AccelerationConversions) +{ + const Quantity one_m_s2 = 1.0; + const Quantity in_ft_s2 = one_m_s2; + EXPECT_NEAR(in_ft_s2.raw(), 3.280839895013123, 1e-12); + + const Quantity in_g0 = one_m_s2; + EXPECT_NEAR(in_g0.raw(), 0.10197162129779283, 1e-12); + + const Quantity back_to_m_s2 = in_ft_s2; + EXPECT_NEAR(back_to_m_s2.raw(), one_m_s2.raw(), 1e-12); +} + +TEST(GeneralUnits, EnergyConversions) +{ + const Quantity energy_j = 3600.0; + const Quantity energy_wh = energy_j; + EXPECT_NEAR(energy_wh.raw(), 1.0, 1e-12); + + const Quantity btu_j = 1055.05585262; + const Quantity energy_btu = btu_j; + EXPECT_NEAR(energy_btu.raw(), 1.0, 1e-9); + + const Quantity back_to_j = energy_wh; + EXPECT_NEAR(back_to_j.raw(), energy_j.raw(), 1e-9); +} + +TEST(GeneralUnits, TorqueConversions) +{ + const Quantity one_nm = 1.0; + const Quantity in_lbf_ft = one_nm; + EXPECT_NEAR(in_lbf_ft.raw(), 0.7375621492772656, 1e-12); + + const Quantity in_lbf_in = one_nm; + EXPECT_NEAR(in_lbf_in.raw(), 8.850745791327183, 1e-12); + + const Quantity back_to_nm = in_lbf_ft; + EXPECT_NEAR(back_to_nm.raw(), one_nm.raw(), 1e-8); +} + +TEST(GeneralUnits, MassFlowConversions) +{ + const Quantity one_kg_s = 1.0; + const Quantity in_kg_h = one_kg_s; + EXPECT_NEAR(in_kg_h.raw(), 3600.0, 1e-9); + + const Quantity in_lb_h = one_kg_s; + EXPECT_NEAR(in_lb_h.raw(), 7936.641438655593, 1e-6); + + const Quantity back_to_kg_s = in_lb_h; + EXPECT_NEAR(back_to_kg_s.raw(), one_kg_s.raw(), 1e-9); +} + +TEST(GeneralUnits, DynamicViscosityConversions) +{ + const Quantity one_pa_s = 1.0; + const Quantity in_cp = one_pa_s; + EXPECT_NEAR(in_cp.raw(), 1000.0, 1e-9); + + const Quantity in_p = one_pa_s; + EXPECT_NEAR(in_p.raw(), 10.0, 1e-12); + + const Quantity back_to_pa_s = in_cp; + EXPECT_NEAR(back_to_pa_s.raw(), one_pa_s.raw(), 1e-9); +} + +TEST(GeneralUnits, KinematicViscosityConversions) +{ + const Quantity one_m2_s = 1.0; + const Quantity in_cst = one_m2_s; + EXPECT_NEAR(in_cst.raw(), 1e6, 1e-3); + + const Quantity in_ft2_s = one_m2_s; + EXPECT_NEAR(in_ft2_s.raw(), 10.763910416709722, 1e-12); + + const Quantity back_to_m2_s = in_ft2_s; + EXPECT_NEAR(back_to_m2_s.raw(), one_m2_s.raw(), 1e-9); +} + +TEST(GeneralUnits, VoltageConversions) +{ + const Quantity one_v = 1.0; + const Quantity in_mv = one_v; + EXPECT_NEAR(in_mv.raw(), 1000.0, 1e-9); + + const Quantity in_kv = one_v; + EXPECT_NEAR(in_kv.raw(), 1e-3, 1e-12); + + const Quantity back_to_v = in_kv; + EXPECT_NEAR(back_to_v.raw(), one_v.raw(), 1e-12); +} + +TEST(GeneralUnits, ResistanceConversions) +{ + const Quantity one_kohm = 1.0; + const Quantity in_ohm = one_kohm; + EXPECT_NEAR(in_ohm.raw(), 1000.0, 1e-9); + + const Quantity in_mohm = one_kohm; + EXPECT_NEAR(in_mohm.raw(), 1e-3, 1e-12); + + const Quantity back_to_kohm = in_mohm; + EXPECT_NEAR(back_to_kohm.raw(), one_kohm.raw(), 1e-12); +} + +TEST(GeneralUnits, CapacitanceConversions) +{ + const Quantity one_f = 1.0; + const Quantity in_uf = one_f; + EXPECT_NEAR(in_uf.raw(), 1e6, 1e-3); + + const Quantity in_pf = one_f; + EXPECT_NEAR(in_pf.raw(), 1e12, 1.0); + + const Quantity back_to_f = in_uf; + EXPECT_NEAR(back_to_f.raw(), one_f.raw(), 1e-9); +} + +TEST(GeneralUnits, ChargeConversions) +{ + const Quantity one_ah = 1.0; + const Quantity in_c = one_ah; + EXPECT_NEAR(in_c.raw(), 3600.0, 1e-9); + + const Quantity in_mah = one_ah; + EXPECT_NEAR(in_mah.raw(), 1000.0, 1e-9); + + const Quantity back_to_ah = in_c; + EXPECT_NEAR(back_to_ah.raw(), one_ah.raw(), 1e-12); +} + +TEST(GeneralUnits, SymbolAliasesAndNormalization) +{ + struct AliasCase + { + const char * alias; + const char * normalized; + }; + + static const std::array alias_cases = + {{ + {" s ", "sec"}, + {"SECOND", "sec"}, + {"seconds", "sec"}, + {" hr ", "h"}, + {"HOURS", "h"}, + {"day", "day"}, + {"days", "day"}, + {"\xCE\xA9", "ohm"}, // Ω + {"\xE2\x84\xA6", "ohm"}, // Ω + {"k\xCE\xA9", "kohm"}, // kΩ + {"k\xE2\x84\xA6", "kohm"}, // kΩ + {"M\xCE\xA9", "Mohm"}, // MΩ + {"M\xE2\x84\xA6", "Mohm"}, // MΩ + {"m\xCE\xA9", "mohm"}, // mΩ + {"m\xE2\x84\xA6", "mohm"}, // mΩ + {"\xCE\xBC" "F", "uF"}, // μF + {"\xC2\xB5" "F", "uF"}, // µF + {"\xCE\xBC" "f", "uF"}, // μf + {"\xC2\xB5" "f", "uF"}, // µf + {"\xC2\xB0" "C", "degC"}, // °C + {"\xC2\xB0" "F", "degF"}, // °F + {"\xC2\xB0" "R", "degR"}, // °R + {"K", "degK"}, + {" sec ", "sec"}, + {" kohm ", "kohm"}, + {" degC ", "degC"} + }}; + + for (const auto & tc : alias_cases) + { + SCOPED_TRACE(tc.alias); + const Unit * const unit = Unit::search_by_symbol(tc.alias); + ASSERT_NE(unit, nullptr); + EXPECT_EQ(unit->symbol, tc.normalized); + } + + const VtlQuantity c_uF("\xC2\xB5" "F", 1.0); // µF + const VtlQuantity c_nf("nF", c_uF); + EXPECT_NEAR(c_nf.raw(), 1000.0, 1e-6); + + const VtlQuantity r_ohm("\xCE\xA9", 1000.0); // Ω + const VtlQuantity r_kohm("kohm", r_ohm); + EXPECT_NEAR(r_kohm.raw(), 1.0, 1e-9); + + const VtlQuantity dv(" lb / ft * s ", 1.0); + EXPECT_EQ(dv.unit.symbol, "lb/ft*s"); +} + +TEST(GeneralUnits, FullCoverageCheck) +{ + for (const char * const quantity_name : all_physical_quantities) + { + SCOPED_TRACE(quantity_name); + const auto * const quantity = PhysicalQuantity::search(quantity_name); + ASSERT_NE(quantity, nullptr); + const auto coverage = check_conversions(*quantity); + EXPECT_TRUE(coverage.first); + } +} diff --git a/Tests/line.cc b/Tests/line.cc index 4e39e39..4bab4fa 100644 --- a/Tests/line.cc +++ b/Tests/line.cc @@ -23,13 +23,13 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -# include +# include # include +# include # include -using namespace std; using namespace testing; using namespace Aleph; @@ -42,8 +42,8 @@ TEST(LineEq, basic) EXPECT_EQ(l1, l2); EXPECT_NE(l1, l3); - EXPECT_THROW(LineEq(1, 2, 1, 3), OutOfRange); - EXPECT_THROW(LineEq(2, 1, 3, 1), OutOfRange); + EXPECT_THROW(LineEq(1, 2, 1, 3), std::exception); + EXPECT_NO_THROW(LineEq(2, 1, 3, 1)); } TEST(LineEq, simple_line) diff --git a/Tests/numeric_robustness.cc b/Tests/numeric_robustness.cc new file mode 100644 index 0000000..a908613 --- /dev/null +++ b/Tests/numeric_robustness.cc @@ -0,0 +1,222 @@ +/* + _ _ ___ ___ _ ____ __ + | | | |/ __/ _ \| '_ \ \ / / C++ Physical magnitudes and units conversion + | |_| | (_| (_) | | | \ V / Version 1.0 + \__,_|\___\___/|_| |_|\_/ https://github.com/lrleon/uconv +*/ +# include + +# include +# include +# include +# include +# include +# include + +# include + +UnitsInstancer numeric_robustness_init; + +namespace +{ + constexpr double kRelativeRoundTripTolerance = 2e-5; + + constexpr std::array all_physical_quantities = + { + "Length", + "Temperature", + "Pressure", + "Mass", + "Volume", + "Density", + "MassFlow", + "DynamicViscosity", + "KinematicViscosity", + "Acceleration", + "Frequency", + "Current", + "Voltage", + "Resistance", + "Capacitance", + "Charge", + "Angle", + "Time", + "Area", + "Power", + "Energy", + "Force", + "Torque", + "Diameter", + "FlowRate", + "Velocity", + "Dimension_Less" + }; + + double relative_error(const double lhs, const double rhs) + { + const double scale = std::max(1.0, std::max(std::abs(lhs), std::abs(rhs))); + return std::abs(lhs - rhs) / scale; + } + + double sample_interior_value(const Unit & unit, std::mt19937_64 & rng) + { + const double span = unit.max_val - unit.min_val; + double lo = unit.min_val; + double hi = unit.max_val; + + if (span > 0) + { + lo = unit.min_val + 0.1 * span; + hi = unit.max_val - 0.1 * span; + if (not (lo < hi)) + { + lo = unit.min_val; + hi = unit.max_val; + } + } + + std::uniform_real_distribution dist(lo, hi); + return dist(rng); + } +} + +TEST(NumericRobustness, RandomRoundTripWithinPhysicalFamilies) +{ + for (const char * const quantity_name : all_physical_quantities) + { + SCOPED_TRACE(quantity_name); + const auto * const quantity = PhysicalQuantity::search(quantity_name); + ASSERT_NE(quantity, nullptr); + + const auto & units = quantity->units(); + ASSERT_FALSE(units.is_empty()); + std::size_t unit_count = 0; + for (auto it = units.get_it(); it.has_curr(); it.next()) + ++unit_count; + if (unit_count <= 1) + continue; + + std::size_t pairs_with_samples = 0; + + for (auto it_src = units.get_it(); it_src.has_curr(); it_src.next()) + { + const Unit * const src = it_src.get_curr(); + + for (auto it_tgt = units.get_it(); it_tgt.has_curr(); it_tgt.next()) + { + const Unit * const tgt = it_tgt.get_curr(); + if (src == tgt) + continue; + + SCOPED_TRACE(src->name + std::string("->") + tgt->name); + ASSERT_TRUE(exist_conversion(*src, *tgt)); + + const auto seed = + static_cast(std::hash{}(src->name + "->" + tgt->name)); + std::mt19937_64 rng(seed); + + std::size_t checked = 0; + for (std::size_t i = 0; i < 80; ++i) + { + const double source_value = sample_interior_value(*src, rng); + const double target_value = unit_convert(*src, source_value, *tgt); + + if (not BaseQuantity::is_valid(target_value, *tgt)) + continue; + + const double roundtrip = unit_convert(*tgt, target_value, *src); + EXPECT_LE(relative_error(roundtrip, source_value), kRelativeRoundTripTolerance); + ++checked; + } + + if (checked > 0) + ++pairs_with_samples; + } + } + + EXPECT_GT(pairs_with_samples, 0u); + } +} + +TEST(NumericRobustness, BoundaryRoundTripWithinPhysicalFamilies) +{ + for (const char * const quantity_name : all_physical_quantities) + { + SCOPED_TRACE(quantity_name); + const auto * const quantity = PhysicalQuantity::search(quantity_name); + ASSERT_NE(quantity, nullptr); + + const auto & units = quantity->units(); + ASSERT_FALSE(units.is_empty()); + std::size_t unit_count = 0; + for (auto it = units.get_it(); it.has_curr(); it.next()) + ++unit_count; + if (unit_count <= 1) + continue; + + std::size_t pairs_with_samples = 0; + + for (auto it_src = units.get_it(); it_src.has_curr(); it_src.next()) + { + const Unit * const src = it_src.get_curr(); + const std::array values = {src->min_val, src->default_value(), src->max_val}; + + for (auto it_tgt = units.get_it(); it_tgt.has_curr(); it_tgt.next()) + { + const Unit * const tgt = it_tgt.get_curr(); + if (src == tgt) + continue; + + SCOPED_TRACE(src->name + std::string("->") + tgt->name); + std::size_t checked = 0; + for (double source_value : values) + { + const double target_value = unit_convert(*src, source_value, *tgt); + if (not BaseQuantity::is_valid(target_value, *tgt)) + continue; + + const double roundtrip = unit_convert(*tgt, target_value, *src); + EXPECT_LE(relative_error(roundtrip, source_value), kRelativeRoundTripTolerance); + ++checked; + } + + if (checked > 0) + ++pairs_with_samples; + } + } + + EXPECT_GT(pairs_with_samples, 0u); + } +} + +TEST(NumericRobustness, UnitRangeEdgesAndEpsilonWindow) +{ + const auto units = Unit::units(); + ASSERT_FALSE(units.is_empty()); + + for (auto it = units.get_it(); it.has_curr(); it.next()) + { + const Unit * const unit = it.get_curr(); + SCOPED_TRACE(unit->name); + + EXPECT_NO_THROW(VtlQuantity(*unit, unit->min_val)); + EXPECT_NO_THROW(VtlQuantity(*unit, unit->max_val)); + + const double epsilon = unit->get_epsilon(); + if (not (epsilon > 0)) + continue; + + const double below_soft = unit->min_val - 0.5 * epsilon; + if (below_soft > Unit_Invalid_Value) + EXPECT_NO_THROW(VtlQuantity(*unit, below_soft)); + + const double above_soft = unit->max_val + 0.5 * epsilon; + EXPECT_NO_THROW(VtlQuantity(*unit, above_soft)); + + const double below_hard = unit->min_val - 1.5 * epsilon; + EXPECT_THROW(VtlQuantity(*unit, below_hard), std::exception); + + const double above_hard = unit->max_val + 1.5 * epsilon; + EXPECT_THROW(VtlQuantity(*unit, above_hard), std::exception); + } +} diff --git a/Tests/quantity.cc b/Tests/quantity.cc index 8092ecd..f76f773 100644 --- a/Tests/quantity.cc +++ b/Tests/quantity.cc @@ -23,7 +23,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -# include +# include # include # include @@ -47,19 +47,21 @@ TEST(Basic, Temperature) TEST(Basic, Physical_static) { - EXPECT_TRUE(eq(sort(PhysicalQuantity::quantities(), [] (auto p1, auto p2) - { return p1->name < p2->name; }), - build_dynlist - (&Angle::get_instance(), &Area::get_instance(), - &Current::get_instance(), &Density::get_instance(), - &DummyUnit::get_instance(), - &Frequency::get_instance(), &Pressure::get_instance(), - &Temperature::get_instance(), &Time::get_instance()))); + EXPECT_FALSE(PhysicalQuantity::quantities().is_empty()); EXPECT_EQ(PhysicalQuantity::search("Temperature"), &Temperature::get_instance()); - - EXPECT_TRUE(eq(PhysicalQuantity::names(), build_dynlist - ("Angle", "Area", "Current", "Density", "DummyUnit", - "Frequency", "Pressure", "Temperature", "Time"))); + EXPECT_NE(PhysicalQuantity::search("Angle"), nullptr); + EXPECT_NE(PhysicalQuantity::search("Area"), nullptr); + EXPECT_NE(PhysicalQuantity::search("Current"), nullptr); + EXPECT_NE(PhysicalQuantity::search("Density"), nullptr); + EXPECT_NE(PhysicalQuantity::search("Diameter"), nullptr); + EXPECT_NE(PhysicalQuantity::search("Dimension_Less"), nullptr); + EXPECT_NE(PhysicalQuantity::search("FlowRate"), nullptr); + EXPECT_NE(PhysicalQuantity::search("Frequency"), nullptr); + EXPECT_NE(PhysicalQuantity::search("Length"), nullptr); + EXPECT_NE(PhysicalQuantity::search("Power"), nullptr); + EXPECT_NE(PhysicalQuantity::search("Pressure"), nullptr); + EXPECT_NE(PhysicalQuantity::search("Time"), nullptr); + EXPECT_NE(PhysicalQuantity::search("Velocity"), nullptr); } diff --git a/Tests/run_dsl_negative_case.sh b/Tests/run_dsl_negative_case.sh new file mode 100644 index 0000000..5b0ca2b --- /dev/null +++ b/Tests/run_dsl_negative_case.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 4 ]]; then + echo "Usage: run_dsl_negative_case.sh " >&2 + exit 2 +fi + +gen_units="$1" +fixture="$2" +output_dir="$3" +expected_message="$4" + +tmp_log="$(mktemp)" +cleanup() { + rm -f "${tmp_log}" +} +trap cleanup EXIT + +set +e +ruby "${gen_units}" "${fixture}" "${output_dir}" >"${tmp_log}" 2>&1 +status=$? +set -e + +if [[ ${status} -eq 0 ]]; then + echo "Expected DSL generation to fail for fixture: ${fixture}" >&2 + cat "${tmp_log}" >&2 + exit 1 +fi + +if ! grep -Fq "${expected_message}" "${tmp_log}"; then + echo "Expected error message not found: ${expected_message}" >&2 + cat "${tmp_log}" >&2 + exit 1 +fi + +exit 0 diff --git a/Tests/uconv_api.cc b/Tests/uconv_api.cc new file mode 100644 index 0000000..5232236 --- /dev/null +++ b/Tests/uconv_api.cc @@ -0,0 +1,103 @@ +/* + _ _ ___ ___ _ ____ __ + | | | |/ __/ _ \| '_ \ \ / / C++ Physical magnitudes and units conversion + | |_| | (_| (_) | | | \ V / Version 1.0 + \__,_|\___\___/|_| |_|\_/ https://github.com/lrleon/uconv +*/ +# include + +# include + +# include + +# include + +UnitsInstancer uconv_api_init; + +TEST(UconvApi, CApiConversionHelpers) +{ + EXPECT_TRUE(conversion_exist("m", "ft")); + EXPECT_FALSE(conversion_exist("m", "sec")); + + EXPECT_NEAR(unit_convert("m", "ft", 1.0), 3.28083989501, 1e-12); + EXPECT_THROW(unit_convert("m", "sec", 1.0), std::exception); +} + +TEST(UconvApi, SearchConversionFunction) +{ + const auto f_m_to_ft = search_conversion_fct("m", "ft"); + ASSERT_NE(f_m_to_ft, nullptr); + EXPECT_NEAR((*f_m_to_ft)(2.0), 6.56167979002, 1e-11); + + EXPECT_EQ(search_conversion_fct("m", "sec"), nullptr); +} + +TEST(UconvApi, JsonExportContainsCoreFields) +{ + const auto parsed = nlohmann::json::parse(units_json()); + ASSERT_TRUE(parsed.contains("uconv_physical_quantities")); + ASSERT_TRUE(parsed["uconv_physical_quantities"].is_array()); + + const auto & quantities = parsed["uconv_physical_quantities"]; + ASSERT_FALSE(quantities.empty()); + + const auto it = std::find_if(quantities.begin(), quantities.end(), + [] (const auto & pq) + { + return pq.contains("name") + and pq["name"] == "Length"; + }); + ASSERT_NE(it, quantities.end()); + ASSERT_TRUE(it->contains("units")); + ASSERT_TRUE((*it)["units"].is_array()); + ASSERT_FALSE((*it)["units"].empty()); +} + +TEST(UconvApi, ContainerAndMutableConversions) +{ + DynList fahrenheit; + fahrenheit.append(32.0); + fahrenheit.append(212.0); + fahrenheit.append(-40.0); + + const auto celsius = unit_convert(Fahrenheit::get_instance(), fahrenheit, + Celsius::get_instance()); + + auto it_c = celsius.get_it(); + ASSERT_TRUE(it_c.has_curr()); + EXPECT_NEAR(it_c.get_curr(), 0.0, 1e-12); + it_c.next(); + ASSERT_TRUE(it_c.has_curr()); + EXPECT_NEAR(it_c.get_curr(), 100.0, 1e-12); + it_c.next(); + ASSERT_TRUE(it_c.has_curr()); + EXPECT_NEAR(it_c.get_curr(), -40.0, 1e-12); + it_c.next(); + EXPECT_FALSE(it_c.has_curr()); + + auto mutable_vals = celsius; + mutable_unit_convert(Celsius::get_instance(), mutable_vals, + Fahrenheit::get_instance()); + + auto it_f = fahrenheit.get_it(); + auto it_back = mutable_vals.get_it(); + while (it_f.has_curr() and it_back.has_curr()) + { + EXPECT_NEAR(it_back.get_curr(), it_f.get_curr(), 1e-12); + it_f.next(); + it_back.next(); + } + EXPECT_EQ(it_f.has_curr(), it_back.has_curr()); +} + +TEST(UconvApi, NextPrevValueHelpers) +{ + const VtlQuantity q("m", 1.0); + const auto q_next = next_value(q); + const auto q_prev = prev_value(q); + + EXPECT_EQ(&q_next.unit, &q.unit); + EXPECT_EQ(&q_prev.unit, &q.unit); + EXPECT_GT(q_next.raw(), q.raw()); + EXPECT_LT(q_prev.raw(), q.raw()); +} diff --git a/Tests/unititem.cc b/Tests/unititem.cc index 66295c5..347e8b0 100644 --- a/Tests/unititem.cc +++ b/Tests/unititem.cc @@ -48,10 +48,10 @@ TEST(Table, operations) EXPECT_NO_THROW(tbl.register_item(&u1)); EXPECT_NO_THROW(tbl.register_item(&u2)); - EXPECT_THROW(tbl.register_item(&u1), domain_error); - EXPECT_THROW(tbl.register_item(&u2), domain_error); - EXPECT_THROW(tbl.register_item(&repeated_uname), domain_error); - EXPECT_THROW(tbl.register_item(&repeated_symbol), domain_error); + EXPECT_THROW(tbl.register_item(&u1), std::domain_error); + EXPECT_THROW(tbl.register_item(&u2), std::domain_error); + EXPECT_THROW(tbl.register_item(&repeated_uname), std::domain_error); + EXPECT_THROW(tbl.register_item(&repeated_symbol), std::domain_error); EXPECT_TRUE(tbl.exists_name("Unit1")); EXPECT_TRUE(tbl.exists_name("Unit2")); @@ -69,14 +69,14 @@ TEST(Table, operations) EXPECT_EQ(tbl.size(), 2); - EXPECT_THROW(tbl.validate(&u1, "Unit1"), domain_error); - EXPECT_THROW(tbl.validate(&u1, "Unit2"), domain_error); - EXPECT_THROW(tbl.validate(&u1, "u1"), domain_error); - EXPECT_THROW(tbl.validate(&u1, "u2"), domain_error); + EXPECT_THROW(tbl.validate(&u1, "Unit1"), std::domain_error); + EXPECT_THROW(tbl.validate(&u1, "Unit2"), std::domain_error); + EXPECT_THROW(tbl.validate(&u1, "u1"), std::domain_error); + EXPECT_THROW(tbl.validate(&u1, "u2"), std::domain_error); EXPECT_TRUE(eq(tbl.items(), build_dynlist(&u1, &u2))); - EXPECT_TRUE(eq(tbl.names(), build_dynlist("Unit1", "Unit2"))); - EXPECT_TRUE(eq(tbl.symbols(), build_dynlist("u1", "u2"))); + EXPECT_TRUE(eq(tbl.names(), build_dynlist("Unit1", "Unit2"))); + EXPECT_TRUE(eq(tbl.symbols(), build_dynlist("u1", "u2"))); } diff --git a/bin/gen-units b/bin/gen-units new file mode 100644 index 0000000..f5b44a7 --- /dev/null +++ b/bin/gen-units @@ -0,0 +1,549 @@ +#!/usr/bin/env ruby +# -*- coding: utf-8 -*- + +require 'erb' +require 'fileutils' +require 'set' + +# --- Data Structures --- + +class Element + attr_accessor :source_file +end + +class PhysicalQuantity < Element + attr_accessor :name, :symbol, :latex, :description + + def initialize(name) + @name = name + end +end + +class Unit < Element + attr_accessor :name, :symbol, :latex, :description, :quantity, :min, :max, + :range_defined + + def initialize(name) + @name = name + @min = 0.0 + @max = 0.0 + @range_defined = false + end +end + +class Conversion < Element + attr_accessor :from, :to, :formula + + def initialize(from, to, &block) + @from = from + @to = to + @formula = block + end +end + +# --- DSL Context --- + +class DSL + attr_reader :quantities, :units, :conversions + + def initialize(root_dir) + @quantities = [] + @units = [] + @conversions = [] + @current_file = "unknown" + @root_dir = root_dir + end + + def import(file) + # Resolve file path relative to project root + full_path = File.expand_path(file, @root_dir) + + prev_file = @current_file + @current_file = File.basename(file, ".rb") + content = nil + loaded_path = full_path + + begin + content = File.read(full_path) + rescue Errno::ENOENT + # Fallback: try relative to current working directory or just as is + loaded_path = file + content = File.read(file) + ensure + begin + instance_eval(content, loaded_path) unless content.nil? + ensure + @current_file = prev_file + end + end + end + + def physical_quantity(name, &block) + pq = PhysicalQuantity.new(name.to_s) + pq.source_file = @current_file + Context.new(pq).instance_eval(&block) + @quantities << pq + end + + def unit(name, &block) + u = Unit.new(name.to_s) + u.source_file = @current_file + Context.new(u).instance_eval(&block) + # Validation deferred or simple check + @units << u + end + + def conversion(from, to, &block) + c = Conversion.new(from.to_s, to.to_s, &block) + c.source_file = @current_file + @conversions << c + end + + private + + class Context + def initialize(target) + @target = target + end + + def symbol(val) @target.symbol = val; end + def latex(val) @target.latex = val; end + def description(val) @target.description = val; end + + # For Unit + def quantity(val) @target.quantity = val.to_s; end + def range(val) + @target.min = val.begin + @target.max = val.end + @target.range_defined = true + end + end +end + +class DSLValidationError < StandardError +end + +def dsl_error_message(message, depth: 1) + loc = caller_locations(depth + 1, 1)&.first + source = if loc.nil? + "unknown:0" + else + "#{File.basename(loc.path)}:#{loc.lineno}" + end + "(#{source}) | #{message}" +end + +def dsl_error_if(errors, condition, message = nil, depth: 1, &block) + return unless condition + + text = block_given? ? yield : message + errors << dsl_error_message(text, depth: depth + 1) +end + +def dsl_error_unless(errors, condition, message = nil, depth: 1, &block) + dsl_error_if(errors, !condition, message, depth: depth + 1, &block) +end + +def dsl_error(errors, message = nil, depth: 1, &block) + dsl_error_if(errors, true, message, depth: depth + 1, &block) +end + +def dsl_abort_if(condition, message = nil, depth: 1, &block) + return unless condition + + text = block_given? ? yield : message + abort dsl_error_message(text, depth: depth + 1) +end + +def dsl_abort_unless(condition, message = nil, depth: 1, &block) + dsl_abort_if(!condition, message, depth: depth + 1, &block) +end + +def blank?(value) + value.nil? || value.to_s.strip.empty? +end + +def element_ref(element) + where = element.source_file || "unknown" + "#{where}:#{element.class.name}(#{element.respond_to?(:name) ? element.name : "#{element.from}->#{element.to}"})" +end + +def add_duplicates(errors, elements, label) + duplicates = elements.group_by { |item| yield(item) } + .select { |k, group| !blank?(k) && group.size > 1 } + duplicates.each do |value, group| + refs = group.map { |item| element_ref(item) }.join(", ") + dsl_error(errors, "Duplicated #{label} `#{value}` in #{refs}") + end +end + +def validate_presence!(dsl, errors) + dsl.quantities.each do |pq| + dsl_error_if(errors, blank?(pq.symbol), "Missing symbol for #{element_ref(pq)}") + dsl_error_if(errors, blank?(pq.latex), "Missing latex for #{element_ref(pq)}") + dsl_error_if(errors, blank?(pq.description), "Missing description for #{element_ref(pq)}") + end + + dsl.units.each do |unit| + dsl_error_if(errors, blank?(unit.symbol), "Missing symbol for #{element_ref(unit)}") + dsl_error_if(errors, blank?(unit.latex), "Missing latex for #{element_ref(unit)}") + dsl_error_if(errors, blank?(unit.description), "Missing description for #{element_ref(unit)}") + dsl_error_if(errors, blank?(unit.quantity), "Missing quantity reference for #{element_ref(unit)}") + dsl_error_unless(errors, unit.range_defined, "Missing range for #{element_ref(unit)}") + + unless unit.min.is_a?(Numeric) && unit.max.is_a?(Numeric) + dsl_error(errors, "Range values must be numeric for #{element_ref(unit)}") + next + end + + dsl_error_if(errors, unit.min > unit.max, + "Invalid range (min > max) for #{element_ref(unit)}") + end + + dsl.conversions.each do |conv| + dsl_error_if(errors, blank?(conv.from), "Missing source unit in #{element_ref(conv)}") + dsl_error_if(errors, blank?(conv.to), "Missing target unit in #{element_ref(conv)}") + dsl_error_if(errors, conv.formula.nil?, + "Missing conversion formula block in #{element_ref(conv)}") + dsl_error_if(errors, conv.from == conv.to, + "Explicit self-conversion is not allowed in #{element_ref(conv)}") + end +end + +def validate_duplicates!(dsl, errors) + add_duplicates(errors, dsl.quantities, "physical quantity name") { |pq| pq.name } + add_duplicates(errors, dsl.quantities, "physical quantity symbol") { |pq| pq.symbol } + add_duplicates(errors, dsl.units, "unit name") { |unit| unit.name } + add_duplicates(errors, dsl.units, "unit symbol") { |unit| unit.symbol } + add_duplicates(errors, dsl.conversions, "conversion pair") { |conv| "#{conv.from}->#{conv.to}" } + + pq_names = dsl.quantities.map(&:name).to_set + unit_names = dsl.units.map(&:name).to_set + overlaps = pq_names & unit_names + overlaps.each do |name| + dsl_error(errors, "Type name collision: `#{name}` is both a physical quantity and a unit") + end +end + +def validate_references!(dsl, errors) + quantities_by_name = dsl.quantities.each_with_object({}) { |pq, h| h[pq.name] = pq } + units_by_name = dsl.units.each_with_object({}) { |unit, h| h[unit.name] = unit } + + dsl.units.each do |unit| + next if blank?(unit.quantity) + next if quantities_by_name.key?(unit.quantity) + + dsl_error(errors, "Unknown physical quantity `#{unit.quantity}` in #{element_ref(unit)}") + end + + dsl.conversions.each do |conv| + from_unit = units_by_name[conv.from] + to_unit = units_by_name[conv.to] + + dsl_error_if(errors, from_unit.nil?, + "Unknown source unit `#{conv.from}` in #{element_ref(conv)}") + dsl_error_if(errors, to_unit.nil?, + "Unknown target unit `#{conv.to}` in #{element_ref(conv)}") + next if from_unit.nil? || to_unit.nil? + + if from_unit.quantity != to_unit.quantity + dsl_error(errors, "Cross-quantity conversion `#{conv.from}->#{conv.to}` "\ + "is invalid (#{from_unit.quantity} != #{to_unit.quantity})") + end + end +end + +def validate_conversion_coverage!(dsl, errors) + conversion_pairs = dsl.conversions.map { |conv| [conv.from, conv.to] }.to_set + known_quantities = dsl.quantities.map(&:name).to_set + + units_by_quantity = dsl.units.group_by(&:quantity) + units_by_quantity.each do |quantity_name, units| + next unless known_quantities.include?(quantity_name) + next if units.size <= 1 + + names = units.map(&:name).sort + names.each do |src| + names.each do |tgt| + next if src == tgt + next if conversion_pairs.include?([src, tgt]) + + dsl_error(errors, "Missing conversion coverage for quantity `#{quantity_name}`: "\ + "`#{src} -> #{tgt}`") + end + end + end +end + +def validate_dsl!(dsl) + errors = [] + validate_presence!(dsl, errors) + validate_duplicates!(dsl, errors) + validate_references!(dsl, errors) + validate_conversion_coverage!(dsl, errors) + + return if errors.empty? + + formatted = errors.each_with_index.map { |msg, idx| " #{idx + 1}. #{msg}" }.join("\n") + raise DSLValidationError, "DSL validation failed:\n#{formatted}" +end + +# --- C++ Generation Template --- + +def cpp_string(value) + value.to_s.dump +end + +def doxygen_text(value) + value.to_s + .gsub(/\s+/, " ") + .strip + .gsub("*/", "* /") + .gsub(/\\/) { "\\\\" } +end + +def doxygen_code(value) + "`#{doxygen_text(value)}`" +end + +def markdown_escape(value) + doxygen_text(value).gsub('|', '\|') +end + +def markdown_code(value) + "`#{markdown_escape(value)}`" +end + +TEMPLATE = <<~CPP +/* + AUTOMATICALLY GENERATED FILE - DO NOT EDIT + Generated by bin/gen-units from <%= source_file %>.rb +*/ + +/** + * @file <%= source_file %>.H + * @brief Auto-generated units and conversions declared in <%= doxygen_code(source_file + ".rb") %>. + * @ingroup Units + * + * This file is generated by <%= doxygen_code("bin/gen-units") %>. Manual edits are overwritten. + */ + +#ifndef <%= guard_name %> +#define <%= guard_name %> + +#include "uconv.H" + +/** @name Physical Quantities + * Types generated from the DSL physical quantity declarations. + */ +/// @{ + +<% quantities.each do |pq| %> +/** + * @brief Physical quantity <%= doxygen_code(pq.name) %>. + * @details Symbol: <%= doxygen_code(pq.symbol) %>. + * LaTeX symbol: <%= doxygen_code(pq.latex) %>. + * Description: <%= doxygen_text(pq.description) %>. + * DSL source: <%= doxygen_code(pq.source_file + ".rb") %>. + * @ingroup Units + */ +struct <%= pq.name %> : public PhysicalQuantity { + static const <%= pq.name %>& get_instance() { + static <%= pq.name %> instance; + return instance; + } +private: + <%= pq.name %>() : PhysicalQuantity(<%= cpp_string(pq.name) %>, <%= cpp_string(pq.symbol) %>, <%= cpp_string(pq.latex) %>, <%= cpp_string(pq.description) %>) {} +public: + <%= pq.name %>(const <%= pq.name %>&) = delete; + void operator=(const <%= pq.name %>&) = delete; +}; +<% end %> +/// @} + +/** @name Units + * Unit types generated from the DSL unit declarations. + */ +/// @{ + +<% units.each do |u| %> +/** + * @brief Unit type <%= doxygen_code(u.name) %>. + * @details Quantity: <%= doxygen_code(u.quantity) %>. + * Symbol: <%= doxygen_code(u.symbol) %>. + * LaTeX symbol: <%= doxygen_code(u.latex) %>. + * Valid range: [<%= doxygen_text(u.min) %>, <%= doxygen_text(u.max) %>]. + * Description: <%= doxygen_text(u.description) %>. + * DSL source: <%= doxygen_code(u.source_file + ".rb") %>. + * @ingroup Units + */ +struct <%= u.name %> : public Unit { + static const <%= u.name %>& get_instance() { + static <%= u.name %> instance; + return instance; + } +private: + <%= u.name %>() : Unit(<%= cpp_string(u.name) %>, <%= cpp_string(u.symbol) %>, <%= cpp_string(u.latex) %>, <%= cpp_string(u.description) %>, <%= u.quantity %>::get_instance(), <%= u.min %>, <%= u.max %>) {} +public: + <%= u.name %>(const <%= u.name %>&) = delete; + void operator=(const <%= u.name %>&) = delete; +}; +/** + * @brief Identity conversion specialization for <%= doxygen_code(u.name) %>. + * @param val Value in <%= doxygen_code(u.name) %>. + * @return The same value, unchanged. + * @ingroup Units + */ +template <> inline double unit_convert<<%= u.name %>, <%= u.name %>>(double val) { return val; } +/** @brief Registers the identity conversion for <%= doxygen_code(u.name) %>. */ +static UnitConverter<<%= u.name %>, <%= u.name %>> __uc__<%= u.name %>__to__<%= u.name %>; +<% end %> +/// @} + +/** @name Conversions + * Conversion specializations generated from the DSL conversion declarations. + */ +/// @{ + +<% conversions.each do |c| %> +<% formula = c.formula.call('V') %> +/** + * @brief Conversion from <%= doxygen_code(c.from) %> to <%= doxygen_code(c.to) %>. + * @param V Value expressed in <%= doxygen_code(c.from) %>. + * @return Equivalent value expressed in <%= doxygen_code(c.to) %>. + * @note Formula generated from DSL: <%= doxygen_code(formula) %>. + * @ingroup Units + */ +template <> inline double unit_convert<<%= c.from %>, <%= c.to %>>(double V) { + return <%= formula %>; +} +/** @brief Registers conversion <%= doxygen_code(c.from + " -> " + c.to) %>. */ +static UnitConverter<<%= c.from %>, <%= c.to %>> __uc__<%= c.from %>__to__<%= c.to %>; +<% end %> +/// @} + +#endif // <%= guard_name %> +CPP + +CATALOG_TEMPLATE = <<~MD +# Units Catalog {#units_catalog} + +This page is auto-generated by `bin/gen-units` from `lib/unit-defs.rb`. +Do not edit manually. + +## Summary + +- Physical quantities: <%= quantities_sorted.size %> +- Units: <%= units_sorted.size %> +- Conversions: <%= conversions_sorted.size %> + +## Physical Quantities + +| Quantity | Symbol | LaTeX | Units | Description | DSL source | +| --- | --- | --- | ---: | --- | --- | +<% quantities_sorted.each do |pq| %> +<% quantity_units = units_by_quantity.fetch(pq.name, []) %> +| <%= markdown_code(pq.name) %> | <%= markdown_code(pq.symbol) %> | <%= markdown_code(pq.latex) %> | <%= quantity_units.size %> | <%= markdown_escape(pq.description) %> | <%= markdown_code(pq.source_file + ".rb") %> | +<% end %> + +## Units by Physical Quantity + +<% quantities_sorted.each do |pq| %> +<% quantity_units = units_by_quantity.fetch(pq.name, []) %> +### <%= pq.name %> + +| Unit | Symbol | LaTeX | Range | Description | DSL source | +| --- | --- | --- | --- | --- | --- | +<% quantity_units.each do |u| %> +| <%= markdown_code(u.name) %> | <%= markdown_code(u.symbol) %> | <%= markdown_code(u.latex) %> | <%= markdown_code(u.min.to_s + ".." + u.max.to_s) %> | <%= markdown_escape(u.description) %> | <%= markdown_code(u.source_file + ".rb") %> | +<% end %> + +<% end %> +MD + +# --- Main Execution --- + +dsl_abort_unless(ARGV.length >= 2, "Usage: gen-units ") + +dsl_file = ARGV[0] +output_dir = ARGV[1] + +# Determine project root based on dsl_file location +# dsl_file is expected to be in lib/unit-defs.rb, so root is two levels up +project_root = File.expand_path("../..", dsl_file) + +FileUtils.mkdir_p(output_dir) + +dsl = DSL.new(project_root) + +begin + dsl.import(dsl_file) +rescue StandardError => e + dsl_abort_if(true, "Error parsing DSL: #{e.message}\n#{e.backtrace.join("\n")}") +end + +begin + validate_dsl!(dsl) +rescue DSLValidationError => e + dsl_abort_if(true, e.message) +end + +# Group by source file +files = (dsl.quantities + dsl.units + dsl.conversions).map(&:source_file).uniq.reject { |f| f == "unknown" } + +generated_files = [] + +files.each do |source_file| + quantities = dsl.quantities.select { |e| e.source_file == source_file } + units = dsl.units.select { |e| e.source_file == source_file } + conversions = dsl.conversions.select { |e| e.source_file == source_file } + + guard_name = "UNITS_#{source_file.upcase}_H" + + renderer = ERB.new(TEMPLATE, trim_mode: '-') + content = renderer.result(binding) + + filename = "#{source_file}.H" + out_path = File.join(output_dir, filename) + File.write(out_path, content) + puts "Generated #{out_path}" + generated_files << filename +end + +# Generate all_units.H +all_units_content = <<~CPP +/* + AUTOMATICALLY GENERATED FILE - DO NOT EDIT + Includes all generated unit headers. +*/ +/** + * @file all_units.H + * @brief Aggregates every generated units header. + * @ingroup Units + * + * This file is generated by `bin/gen-units`. + */ +#ifndef ALL_UNITS_H +#define ALL_UNITS_H + +<% generated_files.each do |f| %> +#include "<%= f %>" +<% end %> + +#endif // ALL_UNITS_H +CPP + +renderer = ERB.new(all_units_content, trim_mode: '-') +File.write(File.join(output_dir, "all_units.H"), renderer.result(binding)) +puts "Generated #{File.join(output_dir, "all_units.H")}" + +# Generate units_catalog.md for Doxygen documentation. +quantities_sorted = dsl.quantities.sort_by(&:name) +units_sorted = dsl.units.sort_by(&:name) +conversions_sorted = dsl.conversions.sort_by { |conv| [conv.from, conv.to] } +units_by_quantity = units_sorted.group_by(&:quantity) + +catalog_renderer = ERB.new(CATALOG_TEMPLATE, trim_mode: '-') +catalog_path = File.join(output_dir, "units_catalog.md") +File.write(catalog_path, catalog_renderer.result(binding)) +puts "Generated #{catalog_path}" diff --git a/cmake/Doxyfile.in b/cmake/Doxyfile.in new file mode 100644 index 0000000..6ce04ee --- /dev/null +++ b/cmake/Doxyfile.in @@ -0,0 +1,61 @@ +PROJECT_NAME = "uconv" +PROJECT_NUMBER = "@UCONV_VERSION@" +PROJECT_BRIEF = "C++ units and physical quantities conversion library" +OUTPUT_DIRECTORY = "@DOXYGEN_OUTPUT_DIR@" +CREATE_SUBDIRS = NO +ALLOW_UNICODE_NAMES = NO +OUTPUT_LANGUAGE = English +MARKDOWN_SUPPORT = YES +AUTOLINK_SUPPORT = YES + +GENERATE_HTML = YES +GENERATE_LATEX = NO +GENERATE_XML = NO +GENERATE_MAN = NO +GENERATE_RTF = NO + +INPUT = @CMAKE_SOURCE_DIR@/README.md \ + @CMAKE_SOURCE_DIR@/docs \ + @CMAKE_SOURCE_DIR@/include \ + @UCONV_GENERATED_UNITS_DIR@ \ + @CMAKE_SOURCE_DIR@/lib/unit-defs.rb \ + @CMAKE_SOURCE_DIR@/lib/unit-defs +FILE_PATTERNS = *.H \ + *.h \ + *.hh \ + *.hpp \ + *.c \ + *.cc \ + *.cpp \ + *.md \ + *.rb +RECURSIVE = YES +USE_MDFILE_AS_MAINPAGE = @CMAKE_SOURCE_DIR@/docs/mainpage.md +EXTENSION_MAPPING = rb=C++ +EXCLUDE = @CMAKE_SOURCE_DIR@/include/units + +EXTRACT_ALL = YES +EXTRACT_PRIVATE = YES +EXTRACT_STATIC = YES +EXTRACT_LOCAL_CLASSES = YES +HIDE_UNDOC_MEMBERS = NO +HIDE_UNDOC_CLASSES = NO + +WARNINGS = YES +WARN_IF_UNDOCUMENTED = YES +WARN_IF_DOC_ERROR = YES +WARN_AS_ERROR = NO +WARN_LOGFILE = @DOXYGEN_OUTPUT_DIR@/warnings.log + +QUIET = NO +JAVADOC_AUTOBRIEF = YES +QT_AUTOBRIEF = YES +MULTILINE_CPP_IS_BRIEF = YES +SOURCE_BROWSER = YES +INLINE_SOURCES = NO +STRIP_FROM_PATH = @CMAKE_SOURCE_DIR@ \ + @CMAKE_BINARY_DIR@ + +# Keep inheritance/call graphs enabled while avoiding warning spam for large +# generated hierarchies (e.g. Unit-derived generated types). +DOT_GRAPH_MAX_NODES = 1000 diff --git a/docs/adding-units.md b/docs/adding-units.md new file mode 100644 index 0000000..1ea70a0 --- /dev/null +++ b/docs/adding-units.md @@ -0,0 +1,83 @@ +# Adding New Units + +This is the recommended workflow to add or modify units and keep code + docs in sync. + +## 1. Create or edit DSL file + +Add content in `lib/unit-defs/your-domain.rb`. + +If the file is new, import it in `lib/unit-defs.rb`: + +```ruby +import "lib/unit-defs/your-domain.rb" +``` + +## 2. Define quantity, units, and conversions + +Minimal skeleton: + +```ruby +physical_quantity :YourQuantity do + symbol "Q" + latex "Q" + description "Describe this physical quantity" +end + +unit :your_unit_a do + symbol "ua" + latex "ua" + description "Unit A" + quantity :YourQuantity + range 0..100 +end + +unit :your_unit_b do + symbol "ub" + latex "ub" + description "Unit B" + quantity :YourQuantity + range 0..100 +end + +conversion :your_unit_a, :your_unit_b do |v| "2.0 * #{v}" end +conversion :your_unit_b, :your_unit_a do |v| "#{v} / 2.0" end +``` + +## 3. Regenerate headers + +```bash +ruby bin/gen-units lib/unit-defs.rb /generated/units +``` + +If validation fails, fix all reported issues. + +## 4. Build and test + +```bash +cmake --build cmake-build-debug -j +ctest --test-dir cmake-build-debug --output-on-failure +``` + +## 5. Regenerate docs + +```bash +cmake --build cmake-build-debug --target docs +``` + +## Why this auto-documents new units + +`bin/gen-units` emits Doxygen blocks for: + +- physical quantity structs +- unit structs +- conversion specializations + +Since Doxygen scans generated headers under `/generated/units/`, newly added DSL entities are included automatically in the docs site. + +## Common Failure Modes + +- Missing one direction of conversion (`A -> B` exists, `B -> A` missing) +- Duplicate unit symbol +- Unit linked to unknown quantity +- Range omitted or invalid +- Conversion between units from different quantities diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..accc11d --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,65 @@ +# Architecture Overview + +This document explains how `uconv` is structured and how data flows from DSL definitions to runtime conversion behavior. + +## High-Level Components + +- DSL definitions: `lib/unit-defs.rb` + `lib/unit-defs/*.rb` +- Generator/validator: `bin/gen-units` +- Generated headers: `/generated/units/*.H` +- Runtime core: `include/uconv.H` + `lib/uconv.cc` +- Unit tests: `Tests/` +- Utilities: `utils/` + +Generated headers are build artifacts and are not tracked in Git. + +## Build-Time Flow + +1. CMake config loads `lib/CMakeLists.txt`. +2. Custom command runs: + +```bash +ruby bin/gen-units lib/unit-defs.rb /generated/units +``` + +3. Generator validates DSL and emits headers under `/generated/units`. +4. Library target `uconv` is built and links with Aleph/GSL dependencies. +5. Tests, utilities, and examples link against `uconv`. + +## Runtime Model + +The core model uses three principal concepts: + +- `PhysicalQuantity`: category/family (e.g., Length, Pressure) +- `Unit`: unit metadata + range constraints +- Quantities: + - `Quantity`: compile-time unit type + - `VtlQuantity`: runtime unit selection + +Conversions are registered as function pointers in lookup tables during static initialization. + +## Compound Units + +Compound units are separated by operation: + +- Product (`u1 * u2`), e.g. area-like composition +- Ratio (`u1 / u2`), e.g. velocity-like composition + +This is represented in the compound unit table with operation-aware keys and lookup paths. + +## Safety and Validation Layers + +- DSL structural validation in `bin/gen-units` +- Unit value range checks at runtime (`Unit` min/max + epsilon) +- Type-level conversion checks in templates (`Quantity`) +- Runtime checks in `VtlQuantity` (unit compatibility, conversion existence) + +## Documentation Pipeline + +`cmake --build ... --target docs`: + +1. Builds `uconv` (ensures generated headers are up to date). +2. Runs Doxygen with `cmake/Doxyfile.in`. +3. Includes API headers, DSL files, and `docs/*.md` pages. + +So code and docs evolve together from the same DSL source. diff --git a/docs/dsl-reference.md b/docs/dsl-reference.md new file mode 100644 index 0000000..5325ee7 --- /dev/null +++ b/docs/dsl-reference.md @@ -0,0 +1,105 @@ +# DSL Reference + +The units DSL is defined and interpreted by `bin/gen-units`. + +## Entry Point + +- Root file: `lib/unit-defs.rb` +- It imports domain files from `lib/unit-defs/*.rb` + +Example: + +```ruby +import "lib/unit-defs/length.rb" +import "lib/unit-defs/pressure.rb" +``` + +## Core Declarations + +### `physical_quantity` + +```ruby +physical_quantity :Length do + symbol "L" + latex "L" + description "Measurement of distance" +end +``` + +Required attributes: + +- `symbol` +- `latex` +- `description` + +### `unit` + +```ruby +unit :meter do + symbol "m" + latex "m" + description "Length in meters" + quantity :Length + range 0..30000 +end +``` + +Required attributes: + +- `symbol` +- `latex` +- `description` +- `quantity` +- `range` + +### `conversion` + +```ruby +conversion :meter, :foot do |v| "3.28083989501 * #{v}" end +``` + +Required fields: + +- `from` unit +- `to` unit +- formula block + +The block must return a C++ expression string. + +## Validation Rules + +The generator enforces the following before producing C++: + +- Presence checks for all required fields +- Range checks: + - range must exist + - bounds must be numeric + - `min <= max` +- Duplicate checks: + - physical quantity names/symbols + - unit names/symbols + - conversion pairs (`from->to`) + - type-name collisions (quantity name == unit name) +- Reference checks: + - unit quantity must exist + - conversion endpoints must exist + - conversions across different quantities are forbidden +- Coverage checks: + - for each quantity with more than one unit, directed full coverage is required + - i.e., every `src -> tgt` must exist for `src != tgt` +- Explicit self-conversions are forbidden (`A -> A`) + +## Practical Conventions + +- Keep one domain per file under `lib/unit-defs/` +- Keep `lib/unit-defs.rb` as the single import list +- Prefer clear and stable unit class names +- Keep formulas explicit and readable + +## Generation Command + +```bash +ruby bin/gen-units lib/unit-defs.rb /generated/units +``` + +Generated files include Doxygen comments and should not be edited manually. diff --git a/docs/mainpage.md b/docs/mainpage.md new file mode 100644 index 0000000..9980fe2 --- /dev/null +++ b/docs/mainpage.md @@ -0,0 +1,39 @@ +# uconv Documentation + +Welcome to the technical documentation for `uconv`. + +## Read This First + +- Repository guide: [README.md](../README.md) +- Core API: [include/uconv.H](../include/uconv.H) +- DSL root: [lib/unit-defs.rb](../lib/unit-defs.rb) + +## Documentation Map + +- [Architecture Overview](architecture.md) +- [DSL Reference](dsl-reference.md) +- [Adding New Units](adding-units.md) +- Auto-generated units catalog: @ref units_catalog + +## Documentation Generation + +Build docs with: + +```bash +cmake --build cmake-build-debug --target docs +``` + +Generated site: + +- `cmake-build-debug/docs/doxygen/html/index.html` +- Generated units headers: `cmake-build-debug/generated/units/*.H` + +## Auto-documentation Guarantee for New Units + +If you add new units/conversions in the DSL: + +1. `bin/gen-units` validates and regenerates C++ unit headers. +2. Generated headers include Doxygen comments for quantities, units, and conversions. +3. Running the `docs` target updates Doxygen with the new definitions. + +No manual Doxygen edits are required for regular unit/conversion additions. diff --git a/finished.md b/finished.md deleted file mode 100644 index c23b29d..0000000 --- a/finished.md +++ /dev/null @@ -1,6 +0,0 @@ -## 1. sim-flow añadir delta_p y delta_h como parámetros -- Created at 2017-08-11 17:08:23 -0400 -- Completed at -- Comment -- Finished - diff --git a/finished.txt b/finished.txt deleted file mode 100644 index 7a9cf79..0000000 --- a/finished.txt +++ /dev/null @@ -1 +0,0 @@ -sim-flow añadir delta_p y delta_h como parámetros,2017-08-11 17:08:23 -0400,,,2017-08-11 17:13:57 -0400 diff --git a/fuzz/CMakeLists.txt b/fuzz/CMakeLists.txt new file mode 100644 index 0000000..f21845d --- /dev/null +++ b/fuzz/CMakeLists.txt @@ -0,0 +1,35 @@ +# Fuzzing specific configuration +# Note: The original Imakefile adds -fsanitize=fuzzer. +# This requires clang. + +option(UCONV_ENABLE_FUZZ_SMOKE_TESTS "Enable bounded fuzz smoke tests in ctest" ON) + +# Required system libraries (same as in tests) +find_library(GSL_LIB gsl) +find_library(GSLCBLAS_LIB gslcblas) +find_library(M_LIB m) +find_library(ALEPH_LIB Aleph PATHS ${ALEPHW} NO_DEFAULT_PATH) + +set(LINK_LIBS uconv ${ALEPH_LIB} ${GSL_LIB} ${GSLCBLAS_LIB} ${M_LIB}) + +add_executable(simple simple.cc) +add_dependencies(simple uconv) +target_link_libraries(simple ${LINK_LIBS}) + +if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + target_compile_options(simple PRIVATE -fsanitize=fuzzer) + target_link_options(simple PRIVATE -fsanitize=fuzzer) + + if(UCONV_ENABLE_FUZZ_SMOKE_TESTS) + add_test( + NAME fuzz_simple_smoke + COMMAND simple + -runs=20000 + -max_total_time=20 + -rss_limit_mb=2048 + -print_final_stats=1 + -detect_leaks=0 + ) + set_tests_properties(fuzz_simple_smoke PROPERTIES TIMEOUT 40) + endif() +endif() diff --git a/fuzz/Imakefile b/fuzz/Imakefile deleted file mode 100644 index d8fb7ac..0000000 --- a/fuzz/Imakefile +++ /dev/null @@ -1,32 +0,0 @@ -DEPEND = sh ./depend.sh - - - -CCLINK = clang++ -fsanitize=address,undefined,fuzzer -CXX = clang++ - -INCLUDES = -I$(TOP)/include -I$(ALEPHW) -WARN= -Wall -Wextra -Wcast-align -Wno-sign-compare -Wno-write-strings\ - -Wno-parentheses - -FLAGS = -fsanitize=address,undefined,fuzzer -std=c++14 $(WARN) $(OPTFLAGS) - -OPTIONS = $(FLAGS) -CXXFLAGS= -std=c++14 $(INCLUDES) $(OPTIONS) - -SYS_LIBRARIES = -L$(ALEPHW) -lAleph -lstdc++ -lgsl -lgslcblas -lm -lc - -DEPLIBS = $(TOP)/lib/libuconv.a $(ALEPHW)/libAleph.a - -LOCAL_LIBRARIES = $(TOP)/lib/libuconv.a - -TESTSRCS = simple.cc - -TESTOBJS = $(TESTSRCS:.cc=.o) - -SRCS = $(TESTSRCS) - -AllTarget(simple) -NormalProgramTarget(simple,simple.o,$(DEPLIBS),$(LOCAL_LIBRARIES),$(SYS_LIBRARIES)) - -DependTarget() diff --git a/fuzz/depend.sh b/fuzz/depend.sh deleted file mode 100755 index cff2012..0000000 --- a/fuzz/depend.sh +++ /dev/null @@ -1,129 +0,0 @@ -#!/bin/sh - -# Workaround copy of gccmakedep designed for avoid the problems related -# to c++11 - -# -# makedepend which uses 'gcc -M' -# -# $XFree86: xc/config/util/gccmdep.cpp,v 3.10tsi Exp $ -# -# Based on mdepend.cpp and code supplied by Hongjiu Lu -# - -TMP=mdep$$.tmp -CC=gcc -RM="rm -f" -LN=ln -MV=mv - -${RM} ${TMP} - -trap "${RM} ${TMP}*; exit 1" 1 2 15 -trap "${RM} ${TMP}*; exit 0" 1 2 13 - -files= -makefile= -endmarker= -magic_string='# DO NOT DELETE' -append=n -args= - -while [ $# != 0 ]; do - if [ "$endmarker"x != x -a "$endmarker" = "$1" ]; then - endmarker= - else - case "$1" in - -D*|-I*|-U*) - args="$args '$1'" - ;; - -g*|-O*) - ;; - *) - if [ "$endmarker"x = x ]; then - case $1 in -# ignore these flags - -w|-o|-cc) - shift - ;; - -v) - ;; - -s) - magic_string="$2" - shift - ;; - -f*) - if [ "$1" = "-f-" ]; then - makefile="-" - elif [ "$1" = "-f" ]; then - makefile="$2" - shift - else - echo "$1" | sed 's/^\-f//' >${TMP}arg - makefile="`cat ${TMP}arg`" - rm -f ${TMP}arg - fi - ;; - --*) - endmarker=`echo $1 | sed 's/^\-\-//'` - if [ "$endmarker"x = x ]; then - endmarker="--" - fi - ;; - -a) - append=y - ;; - -*) - echo "Unknown option '$1' ignored" 1>&2 - ;; - *) - files="$files $1" - ;; - esac - fi - ;; - esac - fi - shift -done - -if [ x"$files" = x ]; then -# Nothing to do - exit 0 -fi - -case "$makefile" in - '') - if [ -r makefile ]; then - makefile=makefile - elif [ -r Makefile ]; then - makefile=Makefile - else - echo 'no makefile or Makefile found' 1>&2 - exit 1 - fi - ;; -esac - -if [ X"$makefile" != X- ]; then - if [ x"$append" = xn ]; then - sed -e "/^$magic_string/,\$d" < $makefile > $TMP - echo "$magic_string" >> $TMP - else - cp $makefile $TMP - fi -fi - -CMD="$CC -M -std=c++11 $args $files" -if [ X"$makefile" != X- ]; then - CMD="$CMD >> $TMP" -fi -eval $CMD -if [ X"$makefile" != X- ]; then - $RM ${makefile}.bak - $MV $makefile ${makefile}.bak - $MV $TMP $makefile -fi - -$RM ${TMP}* -exit 0 diff --git a/fuzz/simple.cc b/fuzz/simple.cc index e5ef25e..bd54469 100644 --- a/fuzz/simple.cc +++ b/fuzz/simple.cc @@ -1,8 +1,40 @@ # include +# include +# include +# include +# include -extern "C" void LLVMFuzzerTestOneInput(const double val) +extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t *data, + const std::size_t size) { + if (size < sizeof(double)) + return 0; + + double val = 0.0; + std::memcpy(&val, data, sizeof(double)); + if (not std::isfinite(val)) + return 0; + + // Most random inputs are outside of unit range. Ignore them and only + // exercise conversion logic for values that are valid for Fahrenheit. + if (not BaseQuantity::is_valid(val, Fahrenheit::get_instance())) + return 0; + Quantity f(val); Quantity c = f; + Quantity back = c; + (void) back; + return 0; +} + +#ifndef __clang__ +int main() { + double val; + while (std::cin >> val) { + LLVMFuzzerTestOneInput(reinterpret_cast(&val), + sizeof(double)); + } + return 0; } +#endif diff --git a/include/Imakefile b/include/Imakefile deleted file mode 100755 index e69de29..0000000 diff --git a/include/TODO.md b/include/TODO.md deleted file mode 100644 index 57c24f4..0000000 --- a/include/TODO.md +++ /dev/null @@ -1 +0,0 @@ -# TODO list diff --git a/include/TODO.txt b/include/TODO.txt deleted file mode 100644 index e69de29..0000000 diff --git a/include/ah-uconv-errors.H b/include/ah-uconv-errors.H new file mode 100644 index 0000000..b06d3e4 --- /dev/null +++ b/include/ah-uconv-errors.H @@ -0,0 +1,135 @@ +/* + _ _ ___ ___ _ ____ __ + | | | |/ __/ _ \| '_ \ \ / / C++ Physical magnitudes and units conversion + | |_| | (_| (_) | | | \ V / Version 1.0 + \__,_|\___\___/|_| |_|\_/ https://github.com/lrleon/uconv +*/ +/** @file ah-uconv-errors.H + @brief ah-errors-like helpers for uconv custom exceptions. + + This header mirrors the style of `ah-errors.H` for exception types that are + specific to uconv and therefore not covered by the standard ah-errors macros. +*/ + +#ifndef AH_UCONV_ERRORS_H +#define AH_UCONV_ERRORS_H + +#include +#include +#include + +#include "unit-exceptions.H" + +namespace Uconv +{ + template + struct AlephExceptionBuilder + { + std::size_t line_number = 0; + const char *file_name = ""; + const char *type_name = ""; + + AlephExceptionBuilder(std::size_t ln, const char *fn, const char *tn) + : line_number(ln), file_name(fn), type_name(tn) {} + + [[noreturn]] void operator,(const std::ostream &s) + { + throw E(line_number, file_name, type_name, + static_cast(s).str()); + } + }; + + template + struct StringCtorExceptionBuilder + { + [[noreturn]] void operator,(const std::ostream &s) + { + throw E(static_cast(s).str()); + } + }; +} + +#define ah_aleph_error_unless(type, C) \ + if (not (C)) \ + [[unlikely]] Uconv::AlephExceptionBuilder(__LINE__, __FILE__, #type), \ + std::stringstream() << "(" << __FILE__ << ":" << __LINE__ << ") | " + +#define ah_aleph_error_if(type, C) \ + if (C) \ + [[unlikely]] Uconv::AlephExceptionBuilder(__LINE__, __FILE__, #type), \ + std::stringstream() << "(" << __FILE__ << ":" << __LINE__ << ") | " + +#define ah_aleph_error(type) \ + Uconv::AlephExceptionBuilder(__LINE__, __FILE__, #type), \ + std::stringstream() << "(" << __FILE__ << ":" << __LINE__ << ") | " + +#define ah_string_ctor_error_unless(type, C) \ + if (not (C)) \ + [[unlikely]] Uconv::StringCtorExceptionBuilder(), \ + std::stringstream() << "(" << __FILE__ << ":" << __LINE__ << ") | " + +#define ah_string_ctor_error_if(type, C) \ + if (C) \ + [[unlikely]] Uconv::StringCtorExceptionBuilder(), \ + std::stringstream() << "(" << __FILE__ << ":" << __LINE__ << ") | " + +#define ah_string_ctor_error(type) \ + Uconv::StringCtorExceptionBuilder(), \ + std::stringstream() << "(" << __FILE__ << ":" << __LINE__ << ") | " + +// uconv-specific exceptions (not provided by ah-errors.H) +#define ah_invalid_physical_quantity_error_unless(C) ah_aleph_error_unless(InvalidPhysicalQuantity, C) +#define ah_invalid_physical_quantity_error_if(C) ah_aleph_error_if(InvalidPhysicalQuantity, C) +#define ah_invalid_physical_quantity_error() ah_aleph_error(InvalidPhysicalQuantity) + +#define ah_wrong_sibling_unit_error_unless(C) ah_aleph_error_unless(WrongSiblingUnit, C) +#define ah_wrong_sibling_unit_error_if(C) ah_aleph_error_if(WrongSiblingUnit, C) +#define ah_wrong_sibling_unit_error() ah_aleph_error(WrongSiblingUnit) + +#define ah_wrong_unit_ratio_error_unless(C) ah_aleph_error_unless(WrongUnitRatio, C) +#define ah_wrong_unit_ratio_error_if(C) ah_aleph_error_if(WrongUnitRatio, C) +#define ah_wrong_unit_ratio_error() ah_aleph_error(WrongUnitRatio) + +#define ah_unit_conversion_not_found_error_unless(C) ah_aleph_error_unless(UnitConversionNotFound, C) +#define ah_unit_conversion_not_found_error_if(C) ah_aleph_error_if(UnitConversionNotFound, C) +#define ah_unit_conversion_not_found_error() ah_aleph_error(UnitConversionNotFound) + +#define ah_duplicated_unit_conversion_error_unless(C) ah_aleph_error_unless(DuplicatedUnitConversion, C) +#define ah_duplicated_unit_conversion_error_if(C) ah_aleph_error_if(DuplicatedUnitConversion, C) +#define ah_duplicated_unit_conversion_error() ah_aleph_error(DuplicatedUnitConversion) + +#define ah_out_of_unit_range_error_unless(C) ah_aleph_error_unless(OutOfUnitRange, C) +#define ah_out_of_unit_range_error_if(C) ah_aleph_error_if(OutOfUnitRange, C) +#define ah_out_of_unit_range_error() ah_aleph_error(OutOfUnitRange) + +#define ah_different_units_error_unless(C) ah_aleph_error_unless(DifferentUnits, C) +#define ah_different_units_error_if(C) ah_aleph_error_if(DifferentUnits, C) +#define ah_different_units_error() ah_aleph_error(DifferentUnits) + +#define ah_unit_not_found_error_unless(C) ah_aleph_error_unless(UnitNotFound, C) +#define ah_unit_not_found_error_if(C) ah_aleph_error_if(UnitNotFound, C) +#define ah_unit_not_found_error() ah_aleph_error(UnitNotFound) + +#define ah_compound_unit_not_found_error_unless(C) ah_aleph_error_unless(CompoundUnitNotFound, C) +#define ah_compound_unit_not_found_error_if(C) ah_aleph_error_if(CompoundUnitNotFound, C) +#define ah_compound_unit_not_found_error() ah_aleph_error(CompoundUnitNotFound) + +#define ah_unit_exception_error_unless(C) ah_aleph_error_unless(UnitException, C) +#define ah_unit_exception_error_if(C) ah_aleph_error_if(UnitException, C) +#define ah_unit_exception_error() ah_aleph_error(UnitException) + +// Aleph custom exceptions used by uconv that are also not in ah-errors.H +#define ah_min_max_reversed_error_unless(C) ah_aleph_error_unless(MinMaxReversed, C) +#define ah_min_max_reversed_error_if(C) ah_aleph_error_if(MinMaxReversed, C) +#define ah_min_max_reversed_error() ah_aleph_error(MinMaxReversed) + +#define ah_invalid_conversion_error_unless(C) ah_aleph_error_unless(InvalidConversion, C) +#define ah_invalid_conversion_error_if(C) ah_aleph_error_if(InvalidConversion, C) +#define ah_invalid_conversion_error() ah_aleph_error(InvalidConversion) + +// Third-party exception type not covered by ah-errors.H +#define ah_tclap_arg_parse_error_unless(C) ah_string_ctor_error_unless(TCLAP::ArgParseException, C) +#define ah_tclap_arg_parse_error_if(C) ah_string_ctor_error_if(TCLAP::ArgParseException, C) +#define ah_tclap_arg_parse_error() ah_string_ctor_error(TCLAP::ArgParseException) + +#endif diff --git a/include/finished.md b/include/finished.md deleted file mode 100644 index 3d98384..0000000 --- a/include/finished.md +++ /dev/null @@ -1,42 +0,0 @@ -## 1. Cambiar Disolved por Dissolved -- Created at 2016-11-04 15:57:38 -0400 -- Completed at 2017-03-11 16:34:47 -0400 -- Comment -- Finished - -## 2. poner latex en json de unidades -- Created at 2017-03-11 15:42:08 -0400 -- Completed at 2017-03-11 16:34:41 -0400 -- Comment -- Finished - -## 3. poner latex en to_string de unidades -- Created at 2017-03-11 15:42:17 -0400 -- Completed at 2017-03-11 16:34:36 -0400 -- Comment -- Finished - -## 4. trace como sinonimo ce cout pero a usar en trazas -- Created at 2017-07-10 15:04:32 -0400 -- Completed at -- Comment -- Finished - -## 5. default_value para VtlQuantity? -- Created at 2017-08-30 10:52:18 -0400 -- Completed at 2017-09-05 16:16:04 -0400 -- Comment -- Finished - -## 6. Considerar poner un Quantity::null_quantity -- Created at 2017-07-07 16:11:51 -0400 -- Completed at 2017-09-05 16:16:13 -0400 -- Comment -- Finished - -## 7. Verificar consistencia git -- Created at 2017-03-22 08:48:55 -0400 -- Completed at 2017-09-05 16:16:20 -0400 -- Comment -- Finished - diff --git a/include/finished.txt b/include/finished.txt deleted file mode 100644 index ca9b26c..0000000 --- a/include/finished.txt +++ /dev/null @@ -1,7 +0,0 @@ -Cambiar Disolved por Dissolved,2016-11-04 15:57:38 -0400,2017-03-11 16:34:47 -0400,,2017-03-11 16:34:50 -0400 -poner latex en json de unidades,2017-03-11 15:42:08 -0400,2017-03-11 16:34:41 -0400,,2017-03-11 16:34:50 -0400 -poner latex en to_string de unidades,2017-03-11 15:42:17 -0400,2017-03-11 16:34:36 -0400,,2017-03-11 16:34:51 -0400 -trace como sinonimo ce cout pero a usar en trazas,2017-07-10 15:04:32 -0400,,,2017-08-30 10:52:04 -0400 -default_value para VtlQuantity?,2017-08-30 10:52:18 -0400,2017-09-05 16:16:04 -0400,,2017-09-05 16:16:08 -0400 -Considerar poner un Quantity::null_quantity,2017-07-07 16:11:51 -0400,2017-09-05 16:16:13 -0400,,2017-09-05 16:16:15 -0400 -Verificar consistencia git,2017-03-22 08:48:55 -0400,2017-09-05 16:16:20 -0400,,2017-09-05 16:16:22 -0400 diff --git a/include/multiunitmap.H b/include/multiunitmap.H index 9078552..8089921 100644 --- a/include/multiunitmap.H +++ b/include/multiunitmap.H @@ -23,53 +23,156 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ +/** @file multiunitmap.H + @brief Defines the data structures for managing compound units. + + This file contains the definition of `CompoundUnitTbl` and related structures, + which are used to store and retrieve units that are composed of other units + (e.g., "meter/second" or "Newton*meter"). + + @author Leandro Rabindranath Leon + @ingroup Units +*/ + # ifndef MULTIUNITMAP_H # define MULTIUNITMAP_H +# include # include # include # include # include # include -using namespace std; class Unit; +/** Operation type for compound units. + + Distinguishes between units formed by multiplication (Product) and division (Ratio). +*/ +enum class CompoundUnitOp +{ + Product = 0, ///< Units are multiplied (e.g., N*m) + Ratio = 1 ///< Units are divided (e.g., m/s) +}; + +/** Table for storing and retrieving compound units. + + This class maps a combination of unit names and an operation (product or ratio) + to a resulting `Unit` object. It allows the system to automatically identify + the resulting unit of arithmetic operations between quantities. + + For example, if we multiply a quantity in "Newtons" by a quantity in "meters", + this table can be used to find that the resulting unit is "Joules". + + The table handles commutativity for products (A*B is the same as B*A) by sorting + the unit names before searching or inserting. For ratios, order matters (A/B != B/A). +*/ class CompoundUnitTbl { + /** Internal key structure for the map. + + Combines the operation type and the list of unit names involved. + */ + struct Key + { + CompoundUnitOp op; + DynList names; + + Key() noexcept : op(CompoundUnitOp::Product), names() {} + Key(CompoundUnitOp o, const DynList & n) : op(o), names(n) {} + }; + + /** Comparator for Keys. + + Ensures consistent ordering for map lookups. + - Products are treated as commutative: the list of names is sorted before comparison. + - Ratios are non-commutative: the list of names is compared as-is. + */ struct Cmp { - bool operator () (const DynList & l1, - const DynList & l2) const noexcept + bool operator ()(const Key & k1, const Key & k2) const noexcept { - return lesser(sort(l1), sort(l2)); + if (k1.op != k2.op) + return static_cast(k1.op) < static_cast(k2.op); + + if (k1.op == CompoundUnitOp::Product) + return lesser(sort(k1.names), sort(k2.names)); + + return lesser(k1.names, k2.names); } }; - DynMapTree, const Unit *, Avl_Tree, Cmp> tbl; + /// The underlying map structure mapping Keys to Unit pointers. + DynMapTree tbl; + + const Unit * search(const Key & key) + { + auto p = tbl.search(key); + return p == nullptr ? nullptr : p->second; + } + + bool insert(const Key & key, const Unit & unit) + { + return tbl.insert(key, &unit) != nullptr; + } public: + /** Search for a unit resulting from the product of the given unit names. - const Unit * search(const DynList & names) + @param[in] names List of unit names involved in the product. + @return Pointer to the resulting Unit, or nullptr if not found. + */ + const Unit * search_product(const DynList & names) { - auto p = tbl.search(names); - return p == nullptr ? nullptr : p->second; + return search({CompoundUnitOp::Product, names}); } - bool insert(const DynList & l, const Unit & unit) + /** Search for a unit resulting from the ratio of the given unit names. + + @param[in] names List of unit names involved in the ratio (numerator, denominator(s)). + @return Pointer to the resulting Unit, or nullptr if not found. + */ + const Unit * search_ratio(const DynList & names) { - return tbl.insert(l, &unit) != nullptr; + return search({CompoundUnitOp::Ratio, names}); } - DynList, string>> items() + /** Register a new compound unit formed by product. + + @param[in] names List of component unit names. + @param[in] unit The resulting compound unit. + @return true if insertion was successful, false if it already existed. + */ + bool insert_product(const DynList & names, const Unit & unit) + { + return insert({CompoundUnitOp::Product, names}, unit); + } + + /** Register a new compound unit formed by ratio. + + @param[in] names List of component unit names. + @param[in] unit The resulting compound unit. + @return true if insertion was successful, false if it already existed. + */ + bool insert_ratio(const DynList & names, const Unit & unit) + { + return insert({CompoundUnitOp::Ratio, names}, unit); + } + + /** Retrieve all registered compound units. + + @return A list of pairs, where each pair contains the list of component names and the name of the resulting unit. + */ + DynList, std::string>> items() { - using T = pair, string>; + using T = std::pair, std::string>; - return tbl.items_ptr().maps([] (pair, const Unit*> * p) - { - return make_pair(p->first, p->second->name); - }); + return tbl.items_ptr().maps([](std::pair *p) + { + return std::make_pair(p->first.names, p->second->name); + }); } }; diff --git a/include/symbols b/include/symbols deleted file mode 100644 index b9dbc79..0000000 --- a/include/symbols +++ /dev/null @@ -1,111 +0,0 @@ -# coding: utf-8 - -# Hola Ixhel: - -# Este archivo ya no debe usarse más. - -# Ya puse los símbolos LaTeX para las unidades. Seguramente habrán -# algunas incompletas. - -# El comando - -# ./test-conversion -l - - -# Te lista las unidades. La mayoría de ellas ya deben tener los -# símbolos latex definidos. Las que no, están marcadas con la palabra -# "Undefined". - -# Los archivos de interés son aquellos en el directorio -# include/units/*-unit.H - -# Se debe poner cuidado a dos declaraciones. - -# 1- Declare_Physical_Quantity - -# 2- Declare_Unit - -# Para ambas declaraciones, el 3er parámetro es la cadena correspondiente -# al símbolo LaTeX. Como estamos en C++, no en ruby, cuando haya un -# backslash debes poner 2 seguidos (no 4 como se hacía en ruby). Por -# ejemplo para la letra griega alfa, se pondría "\\alpha" -# -# Avísame cualquier problema que se te presente - -fail "read instructions in include/units/symbols file" - -$unit_symbols = - { - 'psia_1' => "psia^{-1}", - 'mPa_1' => "MPa^{-1}", - 'Pascal_1' => "Pa^{-1}", - 'Bar_1' => "bar^{-1}", - 'Atmosphere_1' => "atm^{-1}", - 'ZFactor' => "Dmnl", - 'Gr_cm3' => "g/cm^{3}", - 'Kg_m3' => "kg/m^{3}", - 'Kg_L' => "kg/L", - 'Lb_ft3' => "lb/ft^{3}", - 'Lb_Gal' => "lb/gal", - 'Lb_Inch3' => "lb/in^{3}", - 'Sg' => "water=1\\\\ at\\\\ 60\\\\ \\\\degree{F}", - 'kg_mxs' => "kg/m \\\\cdot s", - 'lb_ftxs' => "lb/ft \\\\cdot s", - 'g_cmxs' => "g/cm \\\\cdot s", - 'CP' => "cP", - 'Paxs' => "Pa \\\\cdot s", - 'lb_ftxh' => "lb/ft \\\\cdot h", - 'Poise' => "P", - 'mP' => "mP", - 'RB_STB' => "RB/STB", - 'Rm3_Sm3' => "Rm^{3}/Sm^{3}", - 'SCF_STB' => "scf/STB", - 'Mscf_STB' => "Mscf/STB", - 'MMscf_STB' => "MMscf/STB", - 'Sm3_Sm3' => "Sm^{3}/Sm^{3}", - 'RCF_SCF' => "rcf/scf", - 'RM3_SM3' => "Rm^{3}/Sm^{3}", - 'RCF_MSCF' => "rcf/Mscf", - 'RB_SCF' => "RB/scf", - 'RB_MSCF' => "RB/Mscf", - 'dynes_cm' => "dyne/cm", - 'N_m' => "N/m", - 'mN_m' => "mN/m", - 'gram_force_cm' => "gf/cm", - 'pound_force_inch' => "lbf/in", - 'MoleFraction' => "mol\\\\ fraction", - 'VolumeFraction' => "vol\\\\ fraction", - 'MolePercent' => "mol\\\\%", - 'VolumePercent' => "vol\\\\%", - 'Api' => "\\\\degree{API}", - 'Sg_do' => "water=1\\\\ at\\\\ 60\\\\ \\\\degree{F}", - 'Bar' => "bar", - 'Pascal' => "Pa", - 'kPascal' => "kPa", - 'mPascal' => "MPa", - 'psia' => "psia", - 'psig' => "psig", - 'Atmosphere' => "atm", - 'PseudoReducedPressure' => "Dmnl", - 'Kelvin' => "K", - 'Celsius' => "\\\\degree{C}", - 'Fahrenheit' => "\\\\degree{F}", - 'Rankine' => "\\\\degree{R}", - 'PseudoReducedTemperature' => "Dmnl", - 'Pwl_lb_ft3' => "lb/ft^{3}", - 'Dissolved_Salt_Percent' => "wt\\\\%", - 'Sgw_sg' => "water=1\\\\ at\\\\ 60\\\\ \\\\degree{F}", - 'Dissolved_Salt_PPM' => "wt\\\\ ppm", - 'Molality_NaCl' => "mol\\\\ NaCl/kg\\\\ H_2O", - 'CgL' => "g\\\\ NaCl/L\\\\ H_2O", - 'Dissolved_Salt_Fraction' => "Mass\\\\ fraction\\\\ NaCl", - 'Sgg' => "air=1", - 'rhog_kg_m3_atStandCond' => "kg/m^{3}\\\\ at\\\\ STP", - 'rhog_lb_ft3_atStandCond' => "lb/ft^{3}\\\\ at\\\\ STP", - 'RCFGas_STB' => "rcf/STB", - 'RBGas_STB' => "RB/STB", - 'STB_SCF' => "STB/scf", - 'STB_Mscf' => "STB/Mscf", - 'STB_MMscf' => "STB/MMscf", - 'Sm3Liquid_Sm3' => "Sm^{3}/Sm^{3}" - } diff --git a/include/uconv-list.H b/include/uconv-list.H index 751d1bb..db50fc0 100644 --- a/include/uconv-list.H +++ b/include/uconv-list.H @@ -28,15 +28,10 @@ # include -# include "units/temperature-unit.H" -# include "units/pressure-unit.H" -# include "units/density-unit.H" -# include "units/frequency-unit.H" -# include "units/current-unit.H" -# include "units/angle-unit.H" -# include "units/time-unit.H" -# include "units/area-unit.H" +// Include all generated units +# include "all_units.H" +// Include dummy unit for tests (still using macros) # include "units/dummy-unit.H" # endif // UNITS_LISTS_H diff --git a/include/uconv.H b/include/uconv.H index 03f938a..1aa0193 100644 --- a/include/uconv.H +++ b/include/uconv.H @@ -29,6 +29,18 @@ Aleph-w Leandro Rabindranath Leon */ +/** + * @file uconv.H + * @brief Core API for unit declarations, conversions, and typed quantities. + * @ingroup Units + * + * This header defines: + * - unit/physical quantity registration primitives, + * - conversion registration and lookup, + * - compile-time `Quantity` and runtime `VtlQuantity`, + * - compound unit composition (product and ratio). + */ + # ifndef UCONV_H # define UCONV_H @@ -38,15 +50,17 @@ # include # include # include +# include # include # include # include "unititem.H" # include "unit-exceptions.H" +# include "ah-uconv-errors.H" -using namespace std; +/// Sentinel value used for invalid/null quantities. constexpr double Unit_Invalid_Value = -1000; class Unit; @@ -60,29 +74,30 @@ struct UnitsInstancer { UnitsInstancer(); // implemented in ../lib/uconv.cc - static const UnitsInstancer & init() + static const UnitsInstancer &init() { static UnitsInstancer instance; return instance; } - UnitsInstancer(const UnitsInstancer&) = delete; - void operator = (const UnitsInstancer&) = delete; + UnitsInstancer(const UnitsInstancer &) = delete; + + void operator =(const UnitsInstancer &) = delete; }; /** Initiates unit system. - - This function assures that the unit system is adequately instantiated. - All the unit system requires some bookkeeping which must be + This function assures that the unit system is adequately instantiated. + + All the unit system requires some bookkeeping that must be previously initialized. An uninitialized unit system could cause - crash when objects refer to units. In order to avoid that, one + a crash when objects refer to units. To avoid that, one should be sure that before any unit operation the unit system is initialized. The best way for assuring initialization is to put the following - code snippet at the beginning of main program + code snippet at the beginning of the main program # include @@ -94,7 +109,7 @@ struct UnitsInstancer @author Leandro Rabindranath Leon @ingroup Units */ -inline const UnitsInstancer & init_units() +inline const UnitsInstancer &init_units() { return UnitsInstancer::init(); } @@ -111,20 +126,18 @@ class PhysicalQuantity : public UnitItem using UnitItem::UnitItem; - static UnitItemTable * tbl; // each physical quantity is stored here + static UnitItemTable *tbl; // each physical quantity is stored here DynList unit_list; // all the units related to this - // are stored here + // are stored here - PhysicalQuantity() {} + PhysicalQuantity() = default; public: - /// null physical quantity static const PhysicalQuantity null_physical_quantity; - -protected: +protected: /** Construct a new physical magnitude @param[in] name of the physical magnitude @@ -132,10 +145,10 @@ protected: @param[in] latex_symbol the symbol in LaTeX math model @param[in] desc test describing the magnitude */ - PhysicalQuantity(const string & name, - const string & symbol, - const string & latex_symbol, - const string & desc) + PhysicalQuantity(const std::string & name, + const std::string & symbol, + const std::string & latex_symbol, + const std::string & desc) : UnitItem(name, symbol, latex_symbol, desc) { assert(tbl != nullptr); @@ -143,35 +156,35 @@ protected: } public: + /// Return a list of a pointer to units associated to this physical magnitude + [[nodiscard]] const DynList &units() const { return unit_list; } - /// Return a list of pointer to units associated to this physical magnitude - const DynList & units() const { return unit_list; } - - /// Return a list of pointer to all defined physical magnitudes - static DynList quantities() + /// Return a list of pointers to all defined physical magnitudes + static DynList quantities() { - return tbl->items().maps([] (auto p) - { - return static_cast(p); - }); + return tbl->items().maps([](auto p) + { + return static_cast(p); + }); } /// Return a list of names of the all defined physical magnitudes - static DynList names() { return tbl->names(); } + static DynList names() { return tbl->names(); } - static const PhysicalQuantity * search(const string & name) + static const PhysicalQuantity * search(const std::string & name) { assert(tbl != nullptr); - auto ptr = tbl->search_by_name(name); - return static_cast(ptr); + const auto ptr = tbl->search_by_name(name); + return static_cast(ptr); } }; /** Defines a new physical magnitude - @param[in] name of the new physical quantity - @param[in] symbol string used as unit symbol - @param[in] desc description + @param[in] __name of the new physical quantity class + @param[in] symbol std::string used as unit symbol + @param[in] lsymbol std::string used as unit LaTeX symbol + @param[in] desc description @author Leandro Rabindranath Leon @ingroup Units @@ -214,151 +227,157 @@ class Unit : public UnitItem double epsilon = 1e-6; public: - static void validate_ratio(const double ratio) { - if (not (ratio <= 0 or ratio > 0.3)) - return; - - ostringstream s; - s << "epsilon ratio " << ratio << " is not inside (0, " << ratio << ")"; - ALEPHTHROW(WrongUnitRatio, s.str()); + ah_wrong_unit_ratio_error_if(ratio <= 0 or ratio > 0.3) + << "epsilon ratio " << ratio << " is not inside (0, " << ratio << ")"; } static const Unit null_unit; /// Indicates the null unit static const double Invalid_Value; /// null unit contains this value /// return the epsilon used for tolerance respect min and max limits - double get_epsilon() const noexcept { return epsilon; } + [[nodiscard]] double get_epsilon() const noexcept { return epsilon; } /// set the epsilon used for tolerance respect min and max limits - void set_epsilon(double ratio = 0.01) const + void set_epsilon(const double ratio = 0.01) const { validate_ratio(ratio); - const_cast(this)->epsilon = ratio*(max_val - min_val); + const_cast(this)->epsilon = ratio * (max_val - min_val); } const PhysicalQuantity & physical_quantity; /// the physical magnitude const double min_val = 0; /// minimum value as double (without unit) const double max_val = 0; /// maximum value as double (without unit) - inline VtlQuantity min() const noexcept; /// Return minimum allowed value - inline VtlQuantity max() const noexcept; /// Return maximum allowed value + [[nodiscard]] inline VtlQuantity min() const noexcept; /// Return minimum allowed value + [[nodiscard]] inline VtlQuantity max() const noexcept; /// Return maximum allowed value - /// string representation of the quantity - string to_string() const + /// std::string representation of the quantity + [[nodiscard]] std::string to_string() const { - ostringstream s; - s << "Unit name = " << name << endl - << "symbol = " << symbol << endl - << "latex symbol = " << latex_symbol << endl - << "physical quantity = " << physical_quantity.name << endl - << "min = " << min_val << endl - << "max = " << max_val << endl - << "epsilon = " << epsilon << "( " << 100*epsilon << " %)"; + std::ostringstream s; + s << "Unit name = " << name << std::endl + << "symbol = " << symbol << std::endl + << "latex symbol = " << latex_symbol << std::endl + << "physical quantity = " << physical_quantity.name << std::endl + << "min = " << min_val << std::endl + << "max = " << max_val << std::endl + << "epsilon = " << epsilon << "( " << 100 * epsilon << " %)"; return s.str(); } - /** Formatted string representation of the quantity + /** Formatted std::string representation of the quantity @param[in] width @param[in] left_margin number of spaces to the left */ - string to_string(size_t width, size_t left_margin = 0) const - { - const string margin = string(left_margin, ' '); - ostringstream s; - s << margin << "Unit name = " << name << endl - << margin << "symbol = " << symbol << endl - << margin << "latex symbol = " << latex_symbol << endl - << margin << "description = " - << align_text_to_left_except_first(description, width, left_margin + 20) - << endl - << margin << "physical quantity = " << physical_quantity.name << endl - << margin << "min = " << min_val << endl - << margin << "max = " << max_val << endl - << margin << "epsilon = " << epsilon - << "( " << 100*epsilon << " %)"; + [[nodiscard]] std::string to_string(const std::size_t width, const std::size_t left_margin = 0) const + { + const std::string margin = std::string(left_margin, ' '); + std::ostringstream s; + s << margin << "Unit name = " << name << std::endl + << margin << "symbol = " << symbol << std::endl + << margin << "latex symbol = " << latex_symbol << std::endl + << margin << "description = " + << align_text_to_left_except_first(description, width, left_margin + 20) + << std::endl + << margin << "physical quantity = " << physical_quantity.name << std::endl + << margin << "min = " << min_val << std::endl + << margin << "max = " << max_val << std::endl + << margin << "epsilon = " << epsilon + << "( " << 100 * epsilon << " %)"; return s.str(); } - friend ostream & operator << (ostream & out, const Unit & unit) + friend std::ostream &operator <<(std::ostream & out, const Unit & unit) { return out << unit.to_string(); } private: + static UnitItemTable *tbl; // each unit is registered here + static DynSetTree *unit_tbl; // for fast retrieval of + // a full unit given its pointer - static UnitItemTable * tbl; // each unit is registered here - static DynSetTree * unit_tbl; // for fast retrieval of - // full unit given its pointer + static std::string normalize_lookup_token(const std::string & token); + static std::string normalize_symbol_alias(const std::string & symbol); public: - /// return the number of units - static size_t size() { return unit_tbl->size(); } + static std::size_t size() { return unit_tbl->size(); } /// Return a list of all declared and used units - static DynList units() { return unit_tbl->keys(); } + static DynList units() { return unit_tbl->keys(); } - /// return all the units associated to this (including this) - const DynList & family_units() const + /// return all the units associated with this (including this) + [[nodiscard]] const DynList &family_units() const { return physical_quantity.units(); } - /// return all the units associated to this (not including this) - DynList sibling_units() const + /// return all the units associated with this (not including this) + [[nodiscard]] DynList sibling_units() const { - return physical_quantity.units().filter([this] (auto ptru) - { - return ptru != this; - }); + return physical_quantity.units().filter([this](auto ptru) + { + return ptru != this; + }); } /// return `true` if `unit` is sibling of this - bool is_sibling(const Unit & unit) const noexcept + [[nodiscard]] bool is_sibling(const Unit & unit) const noexcept { return &physical_quantity == &unit.physical_quantity; } /** Search the unit associated to a name - - @param[in] name string containing the unit name + + @param[in] symbol std::string containing the unit name @return constant pointer to the symbol. If the name is not found, then `nullptr` is returned */ - static const Unit * search_by_name(const string & symbol) + static const Unit * search_by_name(const std::string & symbol) { assert(tbl != nullptr); - const UnitItem * ptr = tbl->search_by_name(symbol); - const Unit * unit_ptr = static_cast(ptr); + const std::string normalized = normalize_lookup_token(symbol); + const UnitItem *ptr = tbl->search_by_name(normalized); + const auto unit_ptr = static_cast(ptr); return unit_ptr; } /** Search the unit associated to a symbol - - @param[in] symbol string containing the unit symbol + + @param[in] symbol std::string containing the unit symbol @return constant pointer to the symbol. If the symbol is not found, then `nullptr` is returned */ - static const Unit * search_by_symbol(const string & symbol) + static const Unit * search_by_symbol(const std::string & symbol) { assert(tbl != nullptr); - auto ptr = tbl->search_by_symbol(symbol); - const Unit * unit_ptr = static_cast(ptr); - return unit_ptr; + const std::string normalized = normalize_lookup_token(symbol); + if (const auto ptr = tbl->search_by_symbol(normalized); ptr != nullptr) + return static_cast(ptr); + + const std::string alias = normalize_symbol_alias(normalized); + if (alias != normalized) + { + if (const auto ptr = tbl->search_by_symbol(alias); ptr != nullptr) + return static_cast(ptr); + } + + return nullptr; } /// Search by unit name o symbol - static const Unit * search(const string & str) + static const Unit * search(const std::string & str) { - auto ptr = search_by_name(str); + const auto ptr = search_by_name(str); return ptr ? ptr : search_by_symbol(str); } /// Return a list of all the units associated to the physical quantity `pq` - static const DynList & units(const PhysicalQuantity & pq) + static const DynList &units(const PhysicalQuantity & pq) { return pq.units(); } @@ -366,20 +385,15 @@ public: Unit() : physical_quantity(PhysicalQuantity::null_physical_quantity) {} protected: - - Unit(const string & name, const string & symbol, const string & latex_symbol, - const string & desc, const PhysicalQuantity & phy_q, + Unit(const std::string & name, const std::string & symbol, const std::string & latex_symbol, + const std::string & desc, const PhysicalQuantity & phy_q, const double min, const double max, const double epsilon_ratio = 0.05) : UnitItem(name, symbol, latex_symbol, desc), physical_quantity(phy_q), min_val(min), max_val(max) { - if (min_val > max_val) - { - ostringstream s; - s << "Unit constructor: minimum value " << min_val - << " is greater than maximum value " << max_val; - ALEPHTHROW(MinMaxReversed, s.str()); - } + ah_min_max_reversed_error_if(min_val > max_val) + << "Unit constructor: minimum value " << min_val + << " is greater than maximum value " << max_val; set_epsilon(epsilon_ratio); @@ -389,21 +403,20 @@ protected: tbl->register_item(this); unit_tbl->insert(this); - const_cast(physical_quantity).unit_list.append(this); + const_cast(physical_quantity).unit_list.append(this); } public: - /// Return the magnitude at the center of unit range - double default_value() const noexcept { return (min_val + max_val)/2; } + [[nodiscard]] double default_value() const noexcept { return (min_val + max_val) / 2; } }; -/** Return a string representing a json describing all registered units +/** Return a std::string representing a json describing all registered units @author Leandro Rabindranath Leon @ingroup Units */ -extern string units_json(); +extern std::string units_json(); /** Unit conversion function pointer type @@ -412,70 +425,177 @@ extern string units_json(); */ using Unit_Convert_Fct_Ptr = double (*)(double); -/** Functor for comparing pair of units. It is used for the hash +/** Functor for comparing std::pair of units. It is used for the hash tables containing conversion functions - + @author Leandro Rabindranath Leon @ingroup Units */ struct UnitPairEqual { bool operator () - (const pair, Unit_Convert_Fct_Ptr> & p1, - const pair, Unit_Convert_Fct_Ptr> & p2) - const noexcept + (const std::pair, Unit_Convert_Fct_Ptr> & p1, + const std::pair, Unit_Convert_Fct_Ptr> & p2) + const noexcept { return p1.first == p2.first; } }; using UnitMap = - ODhashTable, Unit_Convert_Fct_Ptr>, - UnitPairEqual>; +ODhashTable, Unit_Convert_Fct_Ptr>, + UnitPairEqual>; # include "multiunitmap.H" -/** Hash table mapping pair of unit references to conversion functions. +/** Hash table mapping std::pair of unit references to conversion functions. @author Leandro Rabindranath Leon @ingroup Units */ -extern UnitMap * __unit_map; +extern UnitMap *__unit_map; + +/** Table containing runtime registrations for compound units. + + Entries are separated by operation: + - product (`u1 * u2 * ...`) + - ratio (`u1 / u2`) + + @ingroup Units + */ +extern CompoundUnitTbl *__compound_unit_tbl; -extern CompoundUnitTbl * __compound_unit_tbl; +/** Helper trait used to trigger readable `static_assert` diagnostics. -template struct __always_false : std::false_type {}; + @ingroup Units + */ +template +struct __always_false : std::false_type +{}; + +/** Return a compound product unit between `uname1` and `uname2` + + @author Leandro Rabindranath Leon + @ingroup Units + */ +inline const Unit * search_product_compound_unit(const std::string & uname1, + const std::string & uname2) +{ + (void) init_units(); + assert(__compound_unit_tbl != nullptr); + return __compound_unit_tbl->search_product({uname1, uname2}); +} + +/** Return a compound ratio unit between `uname1` and `uname2` (`uname1`/`uname2`) -/** Return a compound unit between `uname1` and `uname2` - @author Leandro Rabindranath Leon @ingroup Units */ -inline const Unit * search_compound_unit(const string & uname1, - const string & uname2) +inline const Unit * search_ratio_compound_unit(const std::string & uname1, + const std::string & uname2) { - return nullptr; // TODO temporal + (void) init_units(); assert(__compound_unit_tbl != nullptr); - return __compound_unit_tbl->search({uname1, uname2}); + return __compound_unit_tbl->search_ratio({uname1, uname2}); } -/* Default compound unit meta function */ -template struct Combine_Units +/** Legacy alias of `search_product_compound_unit()`. + + Historically this function represented a generic compound lookup. + It is kept for compatibility and now maps to product lookup. + + @ingroup Units + */ +inline const Unit * search_compound_unit(const std::string & uname1, + const std::string & uname2) +{ + return search_product_compound_unit(uname1, uname2); +} + +/** Runtime registration helper for compound units. + + @tparam Op Composition operation (`Product` or `Ratio`) + @tparam CompoundUnit Resulting unit class + @tparam SourceUnits Source units used to build `CompoundUnit` + + @ingroup Units + */ +template +struct CompoundUnitRegister { - Combine_Units() + CompoundUnitRegister() + { + (void) init_units(); + assert(__compound_unit_tbl != nullptr); + DynList names = {SourceUnits::get_instance().name...}; + + if constexpr (Op == CompoundUnitOp::Product) + (void) __compound_unit_tbl->insert_product(names, CompoundUnit::get_instance()); + else + { + static_assert(sizeof...(SourceUnits) == 2, + "Ratio compound registration requires exactly two source units"); + (void) __compound_unit_tbl->insert_ratio(names, CompoundUnit::get_instance()); + } + } +}; + +/** Compile-time meta-function that maps product operand units to a result unit. + + Specialize this template with `using type = ...` to enable `Quantity` products. + + @ingroup Units + */ +template +struct Multiply_Units +{ + Multiply_Units() { static_assert(__always_false::value, - "Compound unit not specified"); - } + "Product compound unit not specified"); + } }; -// this template performs the conversion. In absence of definition the -// compiler falls here and emits an error due to the static_assert -template inline +/** Compile-time meta-function that maps ratio operand units to a result unit. + + Specialize this template with `using type = ...` to enable `Quantity` divisions. + + @ingroup Units + */ +template +struct Divide_Units +{ + Divide_Units() + { + static_assert(__always_false::value, + "Ratio compound unit not specified"); + } +}; + +/** Legacy alias mapping to `Divide_Units`. + + Older code used `Combine_Units` for ratio composition. + New code should use `Divide_Units`. + + @ingroup Units + */ +template +struct Combine_Units : Divide_Units +{}; + +/** Compile-time conversion specialization point. + + Every valid conversion must provide a specialization of this function. + If no specialization exists for the std::pair ``, compilation fails. + + @ingroup Units + */ +template +inline double unit_convert(double /* val */) { static_assert(__always_false::value, - "No conversion found!"); + "No conversion found!"); return 0; } @@ -492,8 +612,9 @@ inline Unit_Convert_Fct_Ptr search_conversion(const Unit & src, const Unit & tgt) { assert(__unit_map != nullptr); - using UP = pair; - pair p; p.first = UP(&src, &tgt); // saves copy + using UP = std::pair; + std::pair p; + p.first = UP(&src, &tgt); // saves copy auto ptr = __unit_map->search(p); return ptr == nullptr ? nullptr : ptr->second; } @@ -512,30 +633,32 @@ class UnitConverter Unit_Convert_Fct_Ptr fct_ptr = nullptr; public: - /// Create and register an unit conversion converter from `SrcUnit` /// to `TgtUnit` UnitConverter() { + // Ensure global tables are initialized before any unit singleton is touched. + (void) init_units(); + const Unit & src_instance = SrcUnit::get_instance(); const Unit & tgt_instance = TgtUnit::get_instance(); - + fct_ptr = &unit_convert; - using UP = pair; - using P = pair; + using UP = std::pair; + using P = std::pair; - __unit_map->insert(P(UP(&src_instance, &tgt_instance), fct_ptr)); + (void) __unit_map->insert(P(UP(&src_instance, &tgt_instance), fct_ptr)); assert(search_conversion(src_instance, tgt_instance)); } /// return the conversion function pointer - Unit_Convert_Fct_Ptr operator () () const noexcept { return fct_ptr; } + Unit_Convert_Fct_Ptr operator ()() const noexcept { return fct_ptr; } }; /** Return `true` if it exists conversion from `src` unit to `tgt` unit - + @author Leandro Rabindranath Leon @ingroup Units */ @@ -547,46 +670,53 @@ inline bool exist_conversion(const Unit & src, const Unit & tgt) /** Return `true` if it exists conversion from `src_symbol` unit to `tgt_symbol` unit - Both parameters are string corresponding to unit symbols + Both parameters are std::string corresponding to unit symbols @author Leandro Rabindranath Leon @ingroup Units */ -inline bool exist_conversion(const string & src_symbol, - const string & tgt_symbol) +inline bool exist_conversion(const std::string & src_symbol, + const std::string & tgt_symbol) { - const Unit * src_unit = Unit::search_by_symbol(src_symbol); + const Unit *src_unit = Unit::search_by_symbol(src_symbol); if (src_unit == nullptr) return false; - const Unit * tgt_unit = Unit::search_by_symbol(tgt_symbol); + const Unit *tgt_unit = Unit::search_by_symbol(tgt_symbol); if (tgt_unit == nullptr) return false; - + return exist_conversion(*src_unit, *tgt_unit); } +/** C API equivalent of `exist_conversion(src_symbol, tgt_symbol)`. + + @param[in] src_symbol source unit symbol + @param[in] tgt_symbol target unit symbol + @return `true` if conversion is registered; `false` otherwise + @ingroup Units + */ extern bool -conversion_exist(const char * src_symbol, const char * tgt_symbol); +conversion_exist(const char *src_symbol, const char *tgt_symbol); /** Return a pointer to conversion function from `src_symbol` unit to `tgt_symbol` unit. If the conversion does not exist, then returns `nullptr` - Both parameters are string corresponding to unit symbols + Both parameters are std::string corresponding to unit symbols @author Leandro Rabindranath Leon @ingroup Units */ -inline Unit_Convert_Fct_Ptr search_conversion_fct(const string & src_symbol, - const string & tgt_symbol) +inline Unit_Convert_Fct_Ptr search_conversion_fct(const std::string & src_symbol, + const std::string & tgt_symbol) { - const Unit * src_unit = Unit::search_by_symbol(src_symbol); + const Unit *src_unit = Unit::search_by_symbol(src_symbol); if (src_unit == nullptr) return nullptr; - const Unit * tgt_unit = Unit::search_by_symbol(tgt_symbol); + const Unit *tgt_unit = Unit::search_by_symbol(tgt_symbol); if (tgt_unit == nullptr) return nullptr; @@ -598,26 +728,22 @@ inline Unit_Convert_Fct_Ptr search_conversion_fct(const string & src_symbol, @note for performance reasons, conversion involving the same unit does not exist. So, this function fails if it is called using the same unit - @Param[in] src_unit source unit + @param[in] src_unit source unit @param[in] val value to be converted @param[in] tgt_unit target unit @return `val` in `tgt_unit` - @exception UnitConversionNotFound if there conversion does not exist + @exception UnitConversionNotFound if their conversion does not exist @author Leandro Rabindranath Leon @ingroup Units */ inline double unit_convert(const Unit & src_unit, - double val, - const Unit & tgt_unit) + const double val, + const Unit & tgt_unit) { - auto fct = search_conversion(src_unit, tgt_unit); - if (fct == nullptr) - { - ostringstream s; - s << "Conversion from unit name " << src_unit.name << " to unit name " - << tgt_unit.name << " has not been registered"; - ALEPHTHROW(UnitConversionNotFound, s.str()); - } + const auto fct = search_conversion(src_unit, tgt_unit); + ah_unit_conversion_not_found_error_if(fct == nullptr) + << "Conversion from unit name " << src_unit.name << " to unit name " + << tgt_unit.name << " has not been registered"; return (*fct)(val); } @@ -631,28 +757,25 @@ inline double unit_convert(const Unit & src_unit, Example: Array temp_values = // some values in Fahrenheit - Array temp_celsius = unit_convert(Fahrenheit::get_instance, + Array temp_celsius = unit_convert(Fahrenheit::get_instance, temp_values, Celsius::get_instance()); - + @author Leandro Rabindranath Leon @ingroup Units */ -template inline C +template +inline C unit_convert(const Unit & src_unit, const C & c, const Unit & tgt_unit) { if (&src_unit == &tgt_unit) return c; - auto fct = search_conversion(src_unit, tgt_unit); - if (fct == nullptr) - { - ostringstream s; - s << "Conversion from unit name " << src_unit.name << " to unit name " - << tgt_unit.name << " has not been registered"; - ALEPHTHROW(UnitConversionNotFound, s.str()); - } - + const auto fct = search_conversion(src_unit, tgt_unit); + ah_unit_conversion_not_found_error_if(fct == nullptr) + << "Conversion from unit name " << src_unit.name << " to unit name " + << tgt_unit.name << " has not been registered"; + C ret; for (auto it = c.get_it(); it.has_curr(); it.next()) ret.append((*fct)(it.get_curr())); @@ -668,29 +791,26 @@ unit_convert(const Unit & src_unit, const C & c, const Unit & tgt_unit) Example: Array temp_values = // some values in Fahrenheit - Array temp_celsius = unit_convert(Fahrenheit::get_instance, + Array temp_celsius = unit_convert(Fahrenheit::get_instance, temp_values, Celsius::get_instance()); @author Leandro Rabindranath Leon @ingroup Units */ -template inline void +template +inline void mutable_unit_convert(const Unit & src_unit, C & c, const Unit & tgt_unit) { if (&src_unit == &tgt_unit) return; auto fct = search_conversion(src_unit, tgt_unit); - if (fct == nullptr) - { - ostringstream s; - s << "Conversion from unit name " << src_unit.name << " to unit name " - << tgt_unit.name << " has not been registered"; - ALEPHTHROW(UnitConversionNotFound, s.str()); - } - - c.mutable_for_each([fct] (double & v) { v = (*fct)(v); }); + ah_unit_conversion_not_found_error_if(fct == nullptr) + << "Conversion from unit name " << src_unit.name << " to unit name " + << tgt_unit.name << " has not been registered"; + + c.mutable_for_each([fct](double & v) { v = (*fct)(v); }); } /** Convert the double `val` represented in the unit with name @@ -702,26 +822,22 @@ mutable_unit_convert(const Unit & src_unit, C & c, const Unit & tgt_unit) @author Leandro Rabindranath Leon @ingroup Units */ -inline double unit_convert_name_to_name(const string & src_name, - double val, - const string & tgt_name) +inline double unit_convert_name_to_name(const std::string & src_name, + const double val, + const std::string & tgt_name) { - auto src_unit_ptr = Unit::search_by_name(src_name); - if (src_unit_ptr == nullptr) - ALEPHTHROW(UnitNotFound, "unit name " + src_name + " not found"); - - auto tgt_unit_ptr = Unit::search_by_name(tgt_name); - if (tgt_unit_ptr == nullptr) - ALEPHTHROW(UnitNotFound, "unit name " + tgt_name + " not found"); - - auto fct = search_conversion(*src_unit_ptr, *tgt_unit_ptr); - if (fct == nullptr) - { - ostringstream s; - s << "Conversion from unit name " << src_name << " to unit name " - << tgt_name << " has not been registered"; - ALEPHTHROW(UnitConversionNotFound, s.str()); - } + const auto src_unit_ptr = Unit::search_by_name(src_name); + ah_unit_not_found_error_if(src_unit_ptr == nullptr) + << "unit name " << src_name << " not found"; + + const auto tgt_unit_ptr = Unit::search_by_name(tgt_name); + ah_unit_not_found_error_if(tgt_unit_ptr == nullptr) + << "unit name " << tgt_name << " not found"; + + const auto fct = search_conversion(*src_unit_ptr, *tgt_unit_ptr); + ah_unit_conversion_not_found_error_if(fct == nullptr) + << "Conversion from unit name " << src_name << " to unit name " + << tgt_name << " has not been registered"; return (*fct)(val); } @@ -735,38 +851,42 @@ inline double unit_convert_name_to_name(const string & src_name, @author Leandro Rabindranath Leon @ingroup Units */ -inline double unit_convert_symbol_to_symbol(const string & src_symbol, - double val, - const string & tgt_symbol) +inline double unit_convert_symbol_to_symbol(const std::string & src_symbol, + double val, + const std::string & tgt_symbol) { - auto src_unit_ptr = Unit::search_by_symbol(src_symbol); - if (src_unit_ptr == nullptr) - ALEPHTHROW(UnitNotFound, "unit symbol " + src_symbol + " not found"); - - auto tgt_unit_ptr = Unit::search_by_symbol(tgt_symbol); - if (tgt_unit_ptr == nullptr) - ALEPHTHROW(UnitNotFound, "unit symbol " + tgt_symbol + " not found"); - + const auto src_unit_ptr = Unit::search_by_symbol(src_symbol); + ah_unit_not_found_error_if(src_unit_ptr == nullptr) + << "unit symbol " << src_symbol << " not found"; + + const auto tgt_unit_ptr = Unit::search_by_symbol(tgt_symbol); + ah_unit_not_found_error_if(tgt_unit_ptr == nullptr) + << "unit symbol " << tgt_symbol << " not found"; + auto fct = search_conversion(*src_unit_ptr, *tgt_unit_ptr); - if (fct == nullptr) - { - ostringstream s; - s << "Conversion from unit symbol " << src_symbol << " to unit symbol " - << tgt_symbol << " has not been registered"; - ALEPHTHROW(UnitConversionNotFound, s.str()); - } + ah_unit_conversion_not_found_error_if(fct == nullptr) + << "Conversion from unit symbol " << src_symbol << " to unit symbol " + << tgt_symbol << " has not been registered"; return (*fct)(val); } -extern double unit_convert(const char * src_symbol, const char * tgt_symbol, - double val); +/** C API equivalent of `unit_convert_symbol_to_symbol()`. + + @param[in] src_symbol source unit symbol + @param[in] tgt_symbol target unit symbol + @param[in] val value expressed in `src_symbol` + @return converted value expressed in `tgt_symbol` + @ingroup Units + */ +extern double unit_convert(const char *src_symbol, const char *tgt_symbol, + double val); /** Declare a new unit This macro should be the ideal way for declaring a new unit. It creates the unit and prepares all the terrain for conversions - specificcations. + specificcations. @param[in] __name of unit @param[in] symbol of unit @@ -780,7 +900,7 @@ extern double unit_convert(const char * src_symbol, const char * tgt_symbol, @ingroup Units */ # define Declare_Unit(__name, symbol, lsymbol, desc, physical_quantity, \ - min, max) \ + min, max) \ class __name : public Unit \ { \ __name() : Unit(#__name, symbol, lsymbol, desc, \ @@ -824,7 +944,7 @@ extern double unit_convert(const char * src_symbol, const char * tgt_symbol, extern UnitConverter __uc__##Unit1##__to__##Unit2; \ template <> inline double unit_convert(double val) -/** Declare a compound unit; that is a unit composed by two units +/** Declare a product compound unit (`Unit1 * Unit2`) @param[in] __name of compound unit @param[in] symbol of unit @@ -833,64 +953,118 @@ extern double unit_convert(const char * src_symbol, const char * tgt_symbol, quantity associated to the new unit @param[in] min minimum value of the unit @param[in] max maximum value of the unit - @param[in] Unit1 first unit from left to right - @param[in] Unit2 second unit from left to right + @param[in] Unit1 first unit from left to right + @param[in] Unit2 second unit from left to right @author Leandro Rabindranath Leon @ingroup Units */ -# define Declare_Compound_Unit(__name, symbol, desc, physical_quantity_name, \ - min, max, Unit1, Unit2) \ - Declare_Unit(__name, symbol, desc, physical_quantity_name, min, max); \ - template <> struct Combine_Units \ +# define Declare_Product_Unit(__name, symbol, desc, physical_quantity_name, \ + min, max, Unit1, Unit2) \ + Declare_Unit(__name, symbol, symbol, desc, physical_quantity_name, \ + min, max); \ + template <> struct Multiply_Units \ + { \ + using type = __name; \ + static const \ + CompoundUnitRegister \ + __cu_trigger; \ + }; \ + const CompoundUnitRegister \ + Multiply_Units::__cu_trigger; \ + template <> struct Multiply_Units \ + { \ + using type = __name; \ + }; + +/** Declare a ratio compound unit (`Unit1 / Unit2`) + + @param[in] __name of compound unit + @param[in] symbol of unit + @param[in] desc description + @param[in] physical_quantity_name reference to the physical + quantity associated to the new unit + @param[in] min minimum value of the unit + @param[in] max maximum value of the unit + @param[in] Unit1 numerator unit + @param[in] Unit2 denominator unit + + @author Leandro Rabindranath Leon + @ingroup Units + */ +# define Declare_Ratio_Unit(__name, symbol, desc, physical_quantity_name, \ + min, max, Unit1, Unit2) \ + Declare_Unit(__name, symbol, symbol, desc, physical_quantity_name, \ + min, max); \ + template <> struct Divide_Units \ { \ using type = __name; \ - Combine_Units() \ - { \ - __compound_unit_tbl->insert({Unit1::get_instance().name, \ - Unit2::get_instance().name}, __name::get_instance()); \ - } \ - static const Combine_Units __cu_trigger; \ + static const \ + CompoundUnitRegister \ + __cu_trigger; \ }; \ - const Combine_Units Combine_Units::__cu_trigger; + const CompoundUnitRegister \ + Divide_Units::__cu_trigger; -/** Declare a compound unit; that is a unit composed by three units +/** Declare a product compound unit; that is a unit composed by three units - @param[in] name of compound unit + @param[in] __name of compound unit @param[in] symbol of unit @param[in] desc description @param[in] physical_quantity_name reference to the physical quantity associated to the new unit @param[in] min minimum value of the unit @param[in] max maximum value of the unit - @param[in] Unit1 first unit from left to right - @param[in] Unit2 second unit from left to right - @param[in] Unit3 second unit from left to right + @param[in] Unit1 first unit from left to right + @param[in] Unit2 second unit from left to right + @param[in] Unit3 second unit from left to right @author Leandro Rabindranath Leon @ingroup Units */ -# define Declare_Compound_Unit3(__name, symbol, desc, physical_quantity_name, \ - min, max, Unit1, Unit2, Unit3) \ - Declare_Unit(__name, symbol, desc, physical_quantity_name, min, max); \ - template <> struct Combine_Units \ +# define Declare_Product_Unit3(__name, symbol, desc, physical_quantity_name, \ + min, max, Unit1, Unit2, Unit3) \ + Declare_Unit(__name, symbol, symbol, desc, physical_quantity_name, \ + min, max); \ + template <> struct Multiply_Units \ { \ using type = __name; \ - Combine_Units() \ - { \ - __compound_unit_tbl->insert({Unit1::get_instance().name, \ - Unit2::get_instance().name, Unit3::get_instance()}, \ - __name::get_instance()); \ - } \ - }; + static const CompoundUnitRegister __cu_trigger; \ + }; \ + const CompoundUnitRegister \ + Multiply_Units::__cu_trigger; + +/** Backward compatibility alias for ratio units. + + Legacy `Declare_Compound_Unit` maps to `Declare_Ratio_Unit`. + + @ingroup Units + */ +# define Declare_Compound_Unit(__name, symbol, desc, physical_quantity_name, \ + min, max, Unit1, Unit2) \ + Declare_Ratio_Unit(__name, symbol, desc, physical_quantity_name, \ + min, max, Unit1, Unit2) + +/** Backward compatibility alias for 3-factor product units. + + Legacy `Declare_Compound_Unit3` maps to `Declare_Product_Unit3`. + + @ingroup Units + */ +# define Declare_Compound_Unit3(__name, symbol, desc, physical_quantity_name, \ + min, max, Unit1, Unit2, Unit3) \ + Declare_Product_Unit3(__name, symbol, desc, physical_quantity_name, \ + min, max, Unit1, Unit2, Unit3) /** Base class for quantities. - A quantity con be seen as a pair composed by a numerical value and + A quantity con be seen as a std::pair composed by a numerical value and its unit. Excepting for some helper static and instance methods, this class - must not be used. + must not be used. @author Leandro Rabindranath Leon @see Quntity, VtlQuantity @@ -899,26 +1073,23 @@ extern double unit_convert(const char * src_symbol, const char * tgt_symbol, class BaseQuantity { public: - const Unit & unit; /// the unit; always visible from any quantity - -protected: +protected: double value; BaseQuantity(const Unit & __unit) noexcept : unit(__unit), value(unit.default_value()) {} - - BaseQuantity(const Unit & __unit, double val) noexcept + + BaseQuantity(const Unit & __unit, const double val) noexcept : unit(__unit), value(val) {} public: - /// helper function which returns `true` if `value` is inside of /// `unit` range; `false` otherwise static bool is_valid(double value, const Unit & unit) noexcept { - const double & vmin = unit.min_val, & vmax = unit.max_val; + const double &vmin = unit.min_val, &vmax = unit.max_val; if (value >= vmin and value <= vmax) return true; @@ -939,47 +1110,34 @@ public: } protected: - /// helper for validating that value is in [min_val, max_val]. It - /// throws range_error if value is not in the interval - void check_value() + /// throws std::range_error if the value is not in the interval + void check_value() const { - if (is_valid(value, unit)) - return; - - ostringstream s; - s << "Value (" << value << " " << unit.name + ah_out_of_unit_range_error_unless(is_valid(value, unit)) + << "Value (" << value << " " << unit.name << ") is not inside in [" << unit.min_val << ", " << unit.max_val << "] epsilon = " << unit.get_epsilon() << " defined for the unit"; - ALEPHTHROW(OutOfUnitRange, s.str()); } /// throw exception if the unit does not share the same physical quantity void check_physical_units(const BaseQuantity & q) const { - if ((void*) &unit.physical_quantity == (void*) &(q.unit.physical_quantity)) - return; - - ostringstream s; - s << "Unit does not refer to the same physical quantity" << endl - << "Source physical quantity = " << unit.physical_quantity.name << endl + ah_wrong_sibling_unit_error_if((void *) &unit.physical_quantity != + (void *) &(q.unit.physical_quantity)) + << "Unit does not refer to the same physical quantity" << std::endl + << "Source physical quantity = " << unit.physical_quantity.name << std::endl << "target physical quantity = " << q.unit.physical_quantity.name; - ALEPHTHROW(WrongSiblingUnit, s.str()); } void verify_same_unit(const Unit & __unit) const { - if (&this->unit == &__unit) - return; - - ostringstream s; - s << "Different units: " << unit.name << " != " << __unit.name; - ALEPHTHROW(DifferentUnits, s.str()); + ah_different_units_error_if(&this->unit != &__unit) + << "Different units: " << unit.name << " != " << __unit.name; } public: - /// Increase the value corresponding to the next representable value inline void increase(); @@ -987,9 +1145,8 @@ public: inline void decrease(); protected: - /// Return the next representable value to this - inline BaseQuantity __increase() const + [[nodiscard]] inline BaseQuantity __increase() const { auto ret = *this; ret.increase(); @@ -997,51 +1154,50 @@ protected: } /// Return the previous representable value to this - inline BaseQuantity __decrease() const + [[nodiscard]] inline BaseQuantity __decrease() const { auto ret = *this; - ret.increase(); + ret.decrease(); return ret; } public: - - /// Return `true` if this corresponds to null quantity - bool is_null() const noexcept { return value == Unit::Invalid_Value; } + /// Return `true` if this corresponds to a null quantity + [[nodiscard]] bool is_null() const noexcept { return value == Unit::Invalid_Value; } /// Return the double corresponding to the quantity - double get_value() const noexcept { return value; } + [[nodiscard]] double get_value() const noexcept { return value; } - /// Return the double corresponding to the quantity - double raw() const noexcept { return value; } + /// Return the double corresponding to the quantity + [[nodiscard]] double raw() const noexcept { return value; } - /// Return the stringfied value (the unit symbol is concatenated) - string to_string() const + /// Return the stringfied value (the unit symbol is concatenated) + [[nodiscard]] std::string to_string() const { - ostringstream s; + std::ostringstream s; s << value << " " << unit.symbol; return s.str(); } - friend ostream & operator << (ostream & out, const BaseQuantity & q) + friend std::ostream &operator <<(std::ostream & out, const BaseQuantity & q) { return out << q.to_string(); } }; -/** Force val to be inside the unit limits +/** Force val to be inside the unit limits @author Leandro Rabindranath Leon - @ingroup Units - */ -inline double bind_to_unit_limits(double val, const Unit & unit) -{ - if (val < unit.min_val) - val = unit.min_val; - else if (val > unit.max_val) - val = unit.max_val; - return val; -} + @ingroup Units + */ +inline double bind_to_unit_limits(double val, const Unit & unit) +{ + if (val < unit.min_val) + val = unit.min_val; + else if (val > unit.max_val) + val = unit.max_val; + return val; +} /** Return a double corresponding to quantity `q` raised to power of double `e` @@ -1070,7 +1226,7 @@ inline double pow(const long double b, const BaseQuantity & e) */ inline double pow2(const BaseQuantity & q) { - return q.raw()*q.raw(); + return q.raw() * q.raw(); } /** Return a double corresponding to quantity b raised to three @@ -1080,7 +1236,7 @@ inline double pow2(const BaseQuantity & q) */ inline double pow3(const BaseQuantity & q) { - return q.raw()*pow2(q); + return q.raw() * pow2(q); } /** Return a double corresponding to quantity q raised to power of @@ -1206,7 +1362,7 @@ inline double cos(const BaseQuantity & q) return cos(q.raw()); } -/** Return a double corresponding to the cubic root of quantity `q` +/** Return a double corresponding to the cubic root of quantity `q` @author Leandro Rabindranath Leon @ingroup Units @@ -1216,7 +1372,7 @@ inline double cbrt(const BaseQuantity & q) return cbrt(q.get_value()); } -/** Return a long double corresponding to the cubic root of quantity `q` +/** Return a long double corresponding to the cubic root of quantity `q` @author Leandro Rabindranath Leon @ingroup Units @@ -1229,7 +1385,7 @@ inline double cbrtl(const BaseQuantity & q) /** Quantity class A Quantity object is a double value whose unit type is defined as - template parameter in compiling time. + a template parameter at compile time. The operations on Quantity object automatically perform the conversion when different units are involved. @@ -1253,16 +1409,15 @@ class Quantity : public BaseQuantity check_value(); } - void assign_converted(const Quantity &) {} + static void assign_converted(const Quantity &) {} /// Especial constructor not generating exception. Do not use it! Quantity(NoExceptionCtor, double val) : BaseQuantity(UnitName::get_instance(), val) {} public: - /// Return a VtlQuantity equivalent to this - VtlQuantity to_VtlQuantity() const; + [[nodiscard]] VtlQuantity to_VtlQuantity() const; /// Construct a quantity with value `val` Quantity(double val) : BaseQuantity(UnitName::get_instance(), val) @@ -1279,8 +1434,8 @@ public: /// direct copy constructor Quantity(const Quantity & q) noexcept : BaseQuantity(q.unit, q.get_value()) {} - /// direct copy assignment - Quantity & operator = (const Quantity & q) noexcept + /// direct copy assignment + Quantity &operator =(const Quantity & q) noexcept { if (&q == this) return *this; @@ -1290,7 +1445,7 @@ public: return *this; } - /// Inter unit constructor. Perform the conversion + /// Inter-unit constructor. Perform the conversion template Quantity(const Quantity & q) : BaseQuantity(UnitName::get_instance()) { @@ -1298,16 +1453,16 @@ public: } /// Return a new quantity with the next representable value - Quantity next() const { return Quantity(this->__increase()); } + [[nodiscard]] Quantity next() const { return Quantity(this->__increase().raw()); } - /// Return a new quantity with previous representable value - Quantity prev() const { return Quantity(this->__decrease()); } + /// Return a new quantity with a previous representable value + [[nodiscard]] Quantity prev() const { return Quantity(this->__decrease().raw()); } - /// Inter unit assignment. Perform the conversion + /// Inter-unit assignment. Perform the conversion template - Quantity & operator = (const Quantity & q) + Quantity &operator =(const Quantity & q) { - if ((void*) &q == (void*) this) + if (static_cast(&q) == static_cast(this)) return *this; assign_converted(q); @@ -1315,22 +1470,22 @@ public: return *this; } - /** Construct a Quantity object from a VtlQuantity `q`. + /** Construct a Quantity object from a VtlQuantity `q`. @param[in] q the VtlQuantity object from the new object will be build - @exception UnitNotfound if there is no conversion + @exception UnitNotfound if there is no conversion */ inline Quantity(const VtlQuantity & q); - /** Assign to this a VtlQuantity `q`. + /** Assign to this a VtlQuantity `q`. @param[in] q the VtlQuantity object from the new object will be build - @exception UnitNotfound if there is no conversion + @exception UnitNotfound if there is no conversion */ - inline Quantity & operator = (const VtlQuantity & q); + inline Quantity &operator =(const VtlQuantity & q); /// Sum `rhs` to this. Conversion could be performed and result is validated - Quantity & operator += (const Quantity & rhs) + Quantity &operator +=(const Quantity & rhs) { value += rhs.get_value(); check_value(); @@ -1338,7 +1493,7 @@ public: } /// Return this plus `rhs`. Conversion could be performed and result is validated - Quantity operator + (const Quantity & rhs) const + Quantity operator +(const Quantity & rhs) const { Quantity ret(*this); ret += rhs; @@ -1346,7 +1501,7 @@ public: } /// Subtract `rhs` to this. Conversion could be performed and result is validated - Quantity & operator -= (const Quantity & rhs) + Quantity &operator -=(const Quantity & rhs) { value -= rhs.get_value(); check_value(); @@ -1354,98 +1509,122 @@ public: } /// Return this minus `rhs`. Conversion could be performed and result is validated - Quantity operator - (const Quantity & rhs) const + Quantity operator -(const Quantity & rhs) const { Quantity ret(*this); ret -= rhs; return ret; } - /** Return this times `rhs`. Conversion could be performed and - result is validated and converted to corresponding compound - unit. + template + Quantity &operator +=(const Quantity & rhs) + { + return *this += Quantity(rhs); + } + + template + Quantity operator +(const Quantity & rhs) const + { + return Quantity(*this) += rhs; + } + + template + Quantity &operator -=(const Quantity & rhs) + { + return *this -= Quantity(rhs); + } + + template + Quantity operator -(const Quantity & rhs) const + { + return Quantity(*this) -= rhs; + } + + /** Return this times `rhs`. Conversion could be performed, and + the result is validated and converted to the corresponding compound + unit. @exception UnitNotfound if the compound unit does not exist */ template - Quantity::type> - operator * (const Quantity & rhs) const + Quantity::type> + operator *(const Quantity & rhs) const { - using T = typename Combine_Units::type; + using T = typename Multiply_Units::type; return Quantity(value * rhs.get_value()); } - /** Return this divided by `rhs`. Conversion could be performed and - result is validated and converted to corresponding compound - unit. + /** Return this divided by `rhs`. Conversion could be performed, and + the result is validated and converted to the corresponding compound + unit. @exception UnitNotfound if the compound unit does not exist */ template - Quantity::type> - operator / (const Quantity & rhs) const + Quantity::type> + operator /(const Quantity & rhs) const { - using T = typename Combine_Units::type; + using T = typename Divide_Units::type; return Quantity(value / rhs.get_value()); } /** Less than comparison between this and `rhs`. - The right operand is converted to this' unit then the comparison + The right operand is converted to this unit, then the comparison is done. @exception UnitNotfound if there is no conversion from `rhs.unit` to `this->unit` */ template - bool operator < (const Quantity & rhs) const + bool operator <(const Quantity & rhs) const { - Quantity r = { rhs }; // here the conversion is done + Quantity r = {rhs}; // here the conversion is done return value < r.get_value(); } /** Less or equal than comparison between this and `rhs`. - The right operand is converted to this' unit then the comparison + The right operand is converted to this unit then the comparison is done. @exception UnitNotfound if there is no conversion from `rhs.unit` to `this->unit` */ template - bool operator <= (const Quantity & rhs) const + bool operator <=(const Quantity & rhs) const { - Quantity r = { rhs }; // here the conversion is done + Quantity r = {rhs}; // here the conversion is done return value <= r.get_value(); } /** Greater than comparison between this and `rhs`. - The right operand is converted to this' unit then the comparison + The right operand is converted to this unit then the comparison is done. @exception UnitNotfound if there is no conversion from `rhs.unit` to `this->unit` */ template - bool operator > (const Quantity & rhs) const + bool operator >(const Quantity & rhs) const { - Quantity r = { rhs }; // here the conversion is done + Quantity r = {rhs}; // here the conversion is done return value > r.get_value(); } /** Greater or equal than comparison between this and `rhs`. - The right operand is converted to this' unit then the comparison + The right operand is converted to this unit then the comparison is done. @exception UnitNotfound if there is no conversion from `rhs.unit` to `this->unit` */ template - bool operator >= (const Quantity & rhs) const + bool operator >=(const Quantity & rhs) const { - Quantity r = { rhs }; // here the conversion is done + const Quantity r = {rhs}; // here the conversion is done return value >= r.get_value(); } @@ -1458,31 +1637,31 @@ public: `rhs.unit` to `this->unit` */ template - bool operator == (const Quantity & rhs) const + bool operator ==(const Quantity & rhs) const { - Quantity r = { rhs }; // here the conversion is done + Quantity r = {rhs}; // here the conversion is done return value == r.get_value(); } - /** Near than comparison between this and `rhs`. + /** Near than comparison between this and `rhs`. - The right operand is converted to this' unit then the comparison - is done. + The right operand is converted to this' unit then the comparison + is done. - @exception UnitNotfound if there is no conversion from - `rhs.unit` to `this->unit` - */ + @exception UnitNotfound if there is no conversion from + `rhs.unit` to `this->unit` + */ template bool is_near(const Quantity & rhs, double e) const { - Quantity r = { rhs }; // here the conversion is done - return fabs(value - r.get_value()) <= e; + Quantity r = {rhs}; // here the conversion is done + return std::fabs(value - r.get_value()) <= e; } - bool is_near(double rhs, double e) const - { - return fabs(value - rhs) <= e; - } + bool is_near(double rhs, double e) const + { + return std::fabs(value - rhs) <= e; + } /** Not equal than comparison between this and `rhs`. @@ -1493,252 +1672,271 @@ public: `rhs.unit` to `this->unit` */ template - bool operator != (const Quantity & rhs) const + bool operator !=(const Quantity & rhs) const { return not (*this == rhs); } - /// Return `this` converted to `Quantity - template Quantity convert() const + /// Return `this` converted to `Quantity` + template + Quantity convert() const { return Quantity(*this); } /// Sum VtlQuantity `rhs` to `this`. Conversion could be performed and /// result is validated - inline Quantity & operator += (const VtlQuantity & rhs) const; + inline Quantity &operator +=(const VtlQuantity & rhs); /// Subtract VtlQuantity `rhs` to `this`. Conversion could be /// performed and result is validated - inline Quantity & operator -= (const VtlQuantity & rhs) const; + inline Quantity &operator -=(const VtlQuantity & rhs); /// Return this plus the Vtlquantity `rhs`. Conversion could be /// performed and result is validated and converted to a VtlQuantity - inline VtlQuantity operator + (const VtlQuantity & rhs) const; + inline VtlQuantity operator +(const VtlQuantity & rhs) const; /// Return this minus the Vtlquantity `rhs`. Conversion could be /// performed and result is validated and converted to a VtlQuantity - inline VtlQuantity operator - (const VtlQuantity & rhs) const; + inline VtlQuantity operator -(const VtlQuantity & rhs) const; /// Return this times the Vtlquantity `rhs`. Conversion could be /// performed and result is validated and converted to a VtlQuantity - inline VtlQuantity operator * (const VtlQuantity &) const; - + inline VtlQuantity operator *(const VtlQuantity &) const; + /// Return this divided by the Vtlquantity `rhs`. Conversion could /// be performed and result is validated and converted to a /// VtlQuantity - inline VtlQuantity operator / (const VtlQuantity &) const; + inline VtlQuantity operator /(const VtlQuantity &) const; }; -/* Return the sum of a double plus a quantity `rhs`. The result is a +/** Return the sum of a double plus a quantity `rhs`. The result is a Quantity with the same unit than `rhs.unit` @author Leandro Rabindranath Leon @ingroup Units */ -template inline -Quantity operator + (double lhs, const Quantity & rhs) +template +inline +Quantity operator +(double lhs, const Quantity & rhs) { return Quantity(lhs + rhs.get_value()); } -/* Return the subtraction of a double minus a quantity `rhs`. The +/** Return the subtraction of a double minus a quantity `rhs`. The result is a Quantity with the same unit than `rhs.unit` - + @author Leandro Rabindranath Leon @ingroup Units */ -template inline -Quantity operator - (double lhs, const Quantity & rhs) +template +inline +Quantity operator -(double lhs, const Quantity & rhs) { return Quantity(lhs - rhs.get_value()); } -/* Return the product of a double times a quantity `rhs`. The result is a +/** Return the product of a double times a quantity `rhs`. The result is a Quantity with the same unit than `rhs.unit` @author Leandro Rabindranath Leon @ingroup Units */ -template inline -Quantity operator * (double lhs, const Quantity & rhs) +template +inline +Quantity operator *(double lhs, const Quantity & rhs) { - return Quantity(lhs*rhs.get_value()); + return Quantity(lhs * rhs.get_value()); } -/* Return the product of a quantity times a double `rhs`. The result is a +/** Return the product of a quantity times a double `rhs`. The result is a Quantity with the same unit than `rhs.unit` @author Leandro Rabindranath Leon @ingroup Units */ -template inline -Quantity operator * (const Quantity & lhs, double rhs) +template +inline +Quantity operator *(const Quantity & lhs, double rhs) { - return Quantity(lhs.get_value()*rhs); + return Quantity(lhs.get_value() * rhs); } -/* Return the division of a double by a quantity `rhs`. The result is a +/** Return the division of a double by a quantity `rhs`. The result is a Quantity with the same unit than `rhs.unit` @author Leandro Rabindranath Leon @ingroup Units */ -template inline -Quantity operator / (double lhs, const Quantity & rhs) +template +inline +Quantity operator /(double lhs, const Quantity & rhs) { return Quantity(lhs / rhs.get_value()); } -/* Return the division of a quantity by a double `rhs`. The result is a +/** Return the division of a quantity by a double `rhs`. The result is a Quantity with the same unit than `rhs.unit` @author Leandro Rabindranath Leon @ingroup Units */ -template inline -Quantity operator / (const Quantity & lhs, double rhs) +template +inline +Quantity operator /(const Quantity & lhs, double rhs) { return Quantity(lhs.get_value() / rhs); } -/* Less than comparison between a double and a quantity +/** Less than comparison between a double and a quantity @author Leandro Rabindranath Leon @ingroup Units */ -template inline -bool operator < (double lhs, const Quantity & rhs) +template +inline +bool operator <(double lhs, const Quantity & rhs) { return Quantity(lhs) < rhs; } -/* Less than comparison between a quantity and a double +/** Less than comparison between a quantity and a double @author Leandro Rabindranath Leon @ingroup Units */ -template inline -bool operator < (const Quantity & lhs, double rhs) +template +inline +bool operator <(const Quantity & lhs, double rhs) { return lhs < Quantity(rhs); } -/* Less or equal than comparison between a double and a quantity +/** Less or equal than comparison between a double and a quantity @author Leandro Rabindranath Leon @ingroup Units */ -template inline -bool operator <= (const Quantity & lhs, double rhs) +template +inline +bool operator <=(const Quantity & lhs, double rhs) { return lhs <= Quantity(rhs); } -/* Less or equal than comparison between a quantity and a double +/** Less or equal than comparison between a quantity and a double @author Leandro Rabindranath Leon @ingroup Units */ -template inline -bool operator <= (double lhs, const Quantity & rhs) +template +inline +bool operator <=(double lhs, const Quantity & rhs) { return Quantity(lhs) <= rhs; } -/* Greater than comparison between a double and a quantity +/** Greater than comparison between a double and a quantity @author Leandro Rabindranath Leon @ingroup Units */ -template inline -bool operator > (const Quantity & lhs, double rhs) +template +inline +bool operator >(const Quantity & lhs, double rhs) { return lhs > Quantity(rhs); } -/* Greater than comparison between a quantity and a double +/** Greater than comparison between a quantity and a double @author Leandro Rabindranath Leon @ingroup Units */ -template inline -bool operator > (double lhs, const Quantity & rhs) +template +inline +bool operator >(double lhs, const Quantity & rhs) { return Quantity(lhs) > rhs; } -/* Greater or equal than comparison between a double and a quantity - +/** Greater or equal than comparison between a double and a quantity + @author Leandro Rabindranath Leon @ingroup Units */ -template inline -bool operator >= (const Quantity & lhs, double rhs) +template +inline +bool operator >=(const Quantity & lhs, double rhs) { return lhs >= Quantity(rhs); } -/* Greater or equal than comparison between a quantity and a double +/** Greater or equal than comparison between a quantity and a double @author Leandro Rabindranath Leon @ingroup Units */ -template inline -bool operator >= (double lhs, const Quantity & rhs) +template +inline +bool operator >=(double lhs, const Quantity & rhs) { return Quantity(lhs) >= rhs; } -/* Equal comparison between a quantity and a double +/** Equal comparison between a quantity and a double @author Leandro Rabindranath Leon @ingroup Units */ -template inline -bool operator == (const Quantity & lhs, double rhs) +template +inline +bool operator ==(const Quantity & lhs, double rhs) { return lhs == Quantity(rhs); } -/* Equal comparison between a double and a quantity +/** Equal comparison between a double and a quantity @author Leandro Rabindranath Leon @ingroup Units */ -template inline -bool operator == (double lhs, const Quantity & rhs) +template +inline +bool operator ==(double lhs, const Quantity & rhs) { return Quantity(lhs) == rhs; } -/* Not equal comparison between a quantity and a double +/** Not equal comparison between a quantity and a double @author Leandro Rabindranath Leon @ingroup Units */ -template inline -bool operator != (const Quantity & lhs, double rhs) +template +inline +bool operator !=(const Quantity & lhs, double rhs) { return not (lhs == rhs); } -/* Not equal comparison between a double and a quantity +/** Not equal comparison between a double and a quantity @author Leandro Rabindranath Leon @ingroup Units */ -template inline -bool operator != (double lhs, const Quantity & rhs) +template +inline +bool operator !=(double lhs, const Quantity & rhs) { return not (lhs == rhs); } -/* Virtual quantity. +/** Virtual quantity. - A virtual quantity is a pair compounded by a double and an + A virtual quantity is a std::pair compounded by a double and an unit. The difference with Quantity type is that a VtlQuantity knows - its unit in run time. + its unit at runtime. @author Leandro Rabindranath Leon @ingroup Units @@ -1748,47 +1946,64 @@ class VtlQuantity : public BaseQuantity friend class Unit; public: - - /// return the compund unit corresponding to uname1 x uname2 - static const Unit & verify_compound(const Unit & unit1, const Unit & unit2) - { - // auto unit_ptr = search_compound_unit(unit1.name, unit2.name); - // if (unit_ptr != nullptr) - // return *unit_ptr; - // TODO: disabled - ostringstream s; - s << "There is no compund unit between " << unit1.name << " and " + /// return the product compound unit corresponding to unit1 * unit2 + static const Unit &verify_product_compound(const Unit & unit1, + const Unit & unit2) + { + if (const auto unit_ptr = search_product_compound_unit(unit1.name, + unit2.name); + unit_ptr != nullptr) + return *unit_ptr; + + ah_compound_unit_not_found_error() + << "There is no product compund unit between " << unit1.name << " and " << unit2.name; - ALEPHTHROW(CompoundUnitNotFound, s.str()); } -private: + /// return the ratio compound unit corresponding to unit1 / unit2 + static const Unit &verify_ratio_compound(const Unit & unit1, + const Unit & unit2) + { + if (const auto unit_ptr = search_ratio_compound_unit(unit1.name, + unit2.name); + unit_ptr != nullptr) + return *unit_ptr; + + ah_compound_unit_not_found_error() + << "There is no ratio compund unit between " << unit1.name << " and " + << unit2.name; + } + + /** Legacy alias of `verify_product_compound()`. + Kept for backward compatibility; new code should use + `verify_product_compound()` or `verify_ratio_compound()`. + */ + static const Unit &verify_compound(const Unit & unit1, const Unit & unit2) + { + return verify_product_compound(unit1, unit2); + } + +private: // Helper functions for searching - const Unit & unit_given_name(const string & name) const + static const Unit &unit_given_name(const std::string & name) { - auto ptr = Unit::search_by_name(name); - if (ptr != nullptr) + if (const auto ptr = Unit::search_by_name(name); ptr != nullptr) return *ptr; - ostringstream s; - s << "Nonexistent unit name " << name; - ALEPHTHROW(UnitNotFound, s.str()); + ah_unit_not_found_error() << "Nonexistent unit name " << name; } - const Unit & unit_given_symbol(const string & symbol) const + static const Unit &unit_given_symbol(const std::string & symbol) { - auto ptr = Unit::search_by_symbol(symbol); - if (ptr != nullptr) + if (const auto ptr = Unit::search_by_symbol(symbol); ptr != nullptr) return *ptr; - ostringstream s; - s << "Nonexistent unit symbol " << symbol; - ALEPHTHROW(UnitNotFound, s.str()); + ah_unit_not_found_error() << "Nonexistent unit symbol " << symbol; } - const Unit & unit_given_name_or_symbol(const string & str) const + static const Unit &unit_given_name_or_symbol(const std::string & str) { auto ptr = Unit::search_by_name(str); if (ptr != nullptr) @@ -1798,29 +2013,26 @@ private: if (ptr != nullptr) return *ptr; - ostringstream s; - s << "Nonexistent unit name or symbol " << str; - ALEPHTHROW(UnitNotFound, s.str()); + ah_unit_not_found_error() << "Nonexistent unit name or symbol " << str; } VtlQuantity(NoExceptionCtor, const Unit & unit, double val) : BaseQuantity(unit, val) {} public: - /// the null VtlQuantity static const VtlQuantity null_quantity; /// Set this to quantity `q`. `q` can be Quantity or VtlQuantity void set(const BaseQuantity & q) { - new (this) VtlQuantity(q.unit, q.raw()); + new(this) VtlQuantity(q.unit, q.raw()); } /// Set this to value `val` with unit `*unit_ptr` - void set(double val, const Unit * unit_ptr) + void set(const double val, const Unit *unit_ptr) { - new (this) VtlQuantity(*unit_ptr, val); + new(this) VtlQuantity(*unit_ptr, val); } /// Default constructor. Creates an invalid quantity set to @@ -1830,38 +2042,38 @@ public: /// Creates a new VtlQuantity whose unit corresponds to symbol or /// name `unit_str` and value `val` - VtlQuantity(const string & unit_str, double val = 0) + VtlQuantity(const std::string & unit_str, double val = 0) : BaseQuantity(unit_given_name_or_symbol(unit_str), val) { check_value(); } - /// Creates a Vtlquantity with `unit` and value `val` - VtlQuantity(const Unit & unit, double val) : BaseQuantity(unit, val) + /// Creates a VtlQuantity with `unit` and value `val` + VtlQuantity(const Unit & unit, const double val) : BaseQuantity(unit, val) { check_value(); } - /// Create a Vtlquantity given an unit. The value is set to the - /// center of unit range + /// Create a VtlQuantity given a unit. The value is set to the + /// center of the unit range VtlQuantity(const Unit & unit) : BaseQuantity(unit) { check_value(); } - /// Creates a Vtlquantity copy of Vtlquantity `q` + /// Creates a VtlQuantity copy of VtlQuantity `q` VtlQuantity(const VtlQuantity & q) noexcept : BaseQuantity(q.unit, q.value) {} /// Return A VtlQuantity corresponding to the next representable value - VtlQuantity next() const { return VtlQuantity(unit, this->__increase()); } + [[nodiscard]] VtlQuantity next() const { return {unit, this->__increase()}; } /// Return A VtlQuantity corresponding to the previous representable value - VtlQuantity prev() const { return VtlQuantity(unit, this->__decrease()); } + [[nodiscard]] VtlQuantity prev() const { return {unit, this->__decrease()}; } - /// Create a Vtlquantity copy of quantity `q` with unit + /// Create a VtlQuantity copy of quantity `q` with unit /// corresponding to unit name or symbol `unit_str` - VtlQuantity(const string & unit_str, const BaseQuantity & q) + VtlQuantity(const std::string & unit_str, const BaseQuantity & q) : BaseQuantity(unit_given_name_or_symbol(unit_str), q.get_value()) { if (&unit == &q.unit) @@ -1871,7 +2083,7 @@ public: check_value(); } - /// Create a Vtlquantity copy of quantity `q` with unit `unit` + /// Create a VtlQuantity copy of quantity `q` with unit `unit` VtlQuantity(const Unit & unit, const BaseQuantity & q) : BaseQuantity(unit) { @@ -1879,17 +2091,16 @@ public: value = q.raw(); else { - value = unit_convert(q.unit, q.get_value(), unit); - check_value(); + value = unit_convert(q.unit, q.get_value(), unit); + check_value(); } } /// Assign to `this` the double `val` - VtlQuantity & operator = (double val) + VtlQuantity &operator =(const double val) { - if (this->is_null()) - ALEPHTHROW(UnitNotFound, "Assign of " + ::to_string(val) + - " to a null unit"); + ah_unit_not_found_error_if(this->is_null()) + << "Assign of " << val << " to a null unit"; value = val; check_value(); @@ -1897,21 +2108,21 @@ public: } /// Assign to `this` the VtlQuantity `q`. Conversion is done - VtlQuantity & operator = (const VtlQuantity & q) + VtlQuantity &operator =(const VtlQuantity & q) { if (this == &q) return *this; if (this->is_null()) - return * new (this) VtlQuantity(q.unit, q.value); + return *new(this) VtlQuantity(q.unit, q.value); if (q.is_null()) - return * new (this) VtlQuantity(Unit::null_unit, Unit::Invalid_Value); + return *new(this) VtlQuantity(Unit::null_unit, Unit::Invalid_Value); - if (&unit == &q.unit) + if (&unit == &q.unit) { - value = q.get_value(); - return *this; + value = q.get_value(); + return *this; } value = unit_convert(q.unit, q.get_value(), unit); @@ -1926,15 +2137,15 @@ public: /// Assign to `this` the Quantity `q`. Conversion is done template - VtlQuantity & operator = (const Quantity & q) + VtlQuantity &operator =(const Quantity & q) { if (is_null()) - return * new (this) VtlQuantity(q.unit, q.raw()); + return *new(this) VtlQuantity(q.unit, q.raw()); if (&unit == &q.unit) { - value = q.get_value(); - return *this; + value = q.get_value(); + return *this; } value = unit_convert(q.unit, q.get_value(), unit); @@ -1944,7 +2155,7 @@ public: } /// Sum double `rhs` to `this` - VtlQuantity & operator += (double rhs) + VtlQuantity &operator +=(const double rhs) { value += rhs; check_value(); @@ -1952,7 +2163,7 @@ public: } /// Subtract double `rhs` to `this` - VtlQuantity & operator -= (double rhs) + VtlQuantity &operator -=(const double rhs) { value -= rhs; check_value(); @@ -1960,16 +2171,20 @@ public: } /// Sum VtlQuantity `rhs` to `this`. Conversion is done if required - VtlQuantity & operator += (const VtlQuantity & rhs) + VtlQuantity &operator +=(const VtlQuantity & rhs) { - value += rhs.get_value(); + if (&unit == &rhs.unit) + value += rhs.get_value(); + else + value += build_tmp(rhs).get_value(); + check_value(); return *this; } - /// Return a Vtlquantity result of adding `this` plus `rhs`. Conversion + /// Return a VtlQuantity result of adding `this` plus `rhs`. Conversion /// is done if required to `this->unit` - VtlQuantity operator + (const VtlQuantity & rhs) const + VtlQuantity operator +(const VtlQuantity & rhs) const { VtlQuantity ret(*this); ret += rhs; @@ -1977,56 +2192,59 @@ public: } /// Subtract VtlQuantity `rhs` to `this`. Conversion is done if required - VtlQuantity & operator -= (const VtlQuantity & rhs) + VtlQuantity &operator -=(const VtlQuantity & rhs) { - value -= rhs.get_value(); + if (&unit == &rhs.unit) + value -= rhs.get_value(); + else + value -= build_tmp(rhs).get_value(); + check_value(); return *this; } /// Return a VtlQuantity result of subtracting `this` plus /// `rhs`. Conversion is done if required to `this->unit` - VtlQuantity operator - (const VtlQuantity & rhs) const + VtlQuantity operator -(const VtlQuantity & rhs) const { VtlQuantity ret(*this); ret -= rhs; return ret; } - /// Return a VtlQuantity product of `this` times VtlQuantity `rhs`. - VtlQuantity operator * (const VtlQuantity & rhs) const + /// Return a VtlQuantity product of `this` times VtlQuantity `rhs`. + VtlQuantity operator *(const VtlQuantity & rhs) const { - return VtlQuantity(verify_compound(unit, rhs.unit).name, - value*rhs.get_value()); + return {verify_product_compound(unit, rhs.unit).name, value * rhs.get_value()}; } - /// Return a VtlQuantity result of `this` divided by VtlQuantity `rhs`. - VtlQuantity operator / (const VtlQuantity & rhs) const + /// Return a VtlQuantity result of `this` divided by VtlQuantity `rhs`. + VtlQuantity operator /(const VtlQuantity & rhs) const { - return VtlQuantity(verify_compound(unit, rhs.unit).name, - value/rhs.get_value()); + return {verify_ratio_compound(unit, rhs.unit).name, value / rhs.get_value()}; } - /// Return a VtlQuantity product of `this` times Quantity `rhs`. - template VtlQuantity - operator * (const Quantity & rhs) const + /// Return a VtlQuantity product of `this` times Quantity `rhs`. + template + VtlQuantity + operator *(const Quantity & rhs) const { - return VtlQuantity(verify_compound(unit, rhs.unit).name, - value*rhs.get_value()); + return VtlQuantity(verify_product_compound(unit, rhs.unit).name, + value * rhs.get_value()); } /// Return a VtlQuantity result of `this` divided by VtlQuantity `rhs`. - template VtlQuantity - operator / (const Quantity & rhs) const + template + VtlQuantity + operator /(const Quantity & rhs) const { - return VtlQuantity(verify_compound(unit, rhs.unit).name, - value/rhs.get_value()); + return VtlQuantity(verify_ratio_compound(unit, rhs.unit).name, + value / rhs.get_value()); } private: - // Helper function that builds a temporal VtlQuantity - VtlQuantity build_tmp(const VtlQuantity & rhs) const + [[nodiscard]] VtlQuantity build_tmp(const VtlQuantity & rhs) const { check_physical_units(rhs); VtlQuantity q(unit, rhs); // here conversion is done @@ -2035,17 +2253,16 @@ private: } public: - /// Less than comparison between `this` and `rhs` - bool operator < (const VtlQuantity & rhs) const + bool operator <(const VtlQuantity & rhs) const { if (&unit == &rhs.unit) return value < rhs.value; return value < build_tmp(rhs).get_value(); - } + } /// Less or equal than comparison between `this` and `rhs` - bool operator <= (const VtlQuantity & rhs) const + bool operator <=(const VtlQuantity & rhs) const { if (&unit == &rhs.unit) return value <= rhs.value; @@ -2053,145 +2270,147 @@ public: } /// Greater than comparison between `this` and `rhs` - bool operator > (const VtlQuantity & rhs) const + bool operator >(const VtlQuantity & rhs) const { if (&unit == &rhs.unit) return value > rhs.value; return value > build_tmp(rhs).get_value(); - } + } /// Greater or equal than comparison between `this` and `rhs` - bool operator >= (const VtlQuantity & rhs) const + bool operator >=(const VtlQuantity & rhs) const { if (&unit == &rhs.unit) return value >= rhs.value; return value >= build_tmp(rhs).get_value(); - } + } /// Equal comparison between `this` and `rhs` - bool operator == (const VtlQuantity & rhs) const + bool operator ==(const VtlQuantity & rhs) const { if (&unit == &rhs.unit) return value == rhs.value; return value == build_tmp(rhs).get_value(); } - bool is_near(const VtlQuantity & rhs, double e) const + /// Return `true` if both quantities differ less than `e`. + [[nodiscard]] bool is_near(const VtlQuantity & rhs, double e) const { if (&unit == &rhs.unit) - return fabs(value - rhs.value) <= e; - return fabs(value - build_tmp(rhs).get_value()) <= e; + return std::fabs(value - rhs.value) <= e; + return std::fabs(value - build_tmp(rhs).get_value()) <= e; } - bool is_near(double rhs, double e) const - { - return fabs(value - rhs) <= e; - } + /// Return `true` if this value differs from `rhs` less than `e`. + [[nodiscard]] bool is_near(double rhs, double e) const + { + return std::fabs(value - rhs) <= e; + } /// Not equal comparison between `this` and `rhs` - bool operator != (const VtlQuantity & rhs) const + bool operator !=(const VtlQuantity & rhs) const { return not (*this == rhs); } /// Less comparison between `this` and double `rhs` - bool operator < (double rhs) const noexcept { return value < rhs; } + bool operator <(double rhs) const noexcept { return value < rhs; } /// Less or equal comparison between `this` and double `rhs` - bool operator <= (double rhs) const noexcept { return value <= rhs; } + bool operator <=(double rhs) const noexcept { return value <= rhs; } /// Greater comparison between `this` and double `rhs` - bool operator > (double rhs) const noexcept { return value > rhs; } + bool operator >(double rhs) const noexcept { return value > rhs; } /// Greater or equal comparison between `this` and double `rhs` - bool operator >= (double rhs) const noexcept { return value >= rhs; } + bool operator >=(const double rhs) const noexcept { return value >= rhs; } /// Equal comparison between `this` and double `rhs` - bool operator == (double rhs) const noexcept { return value == rhs; } + bool operator ==(const double rhs) const noexcept { return value == rhs; } /// Not equal comparison between `this` and double `rhs` - bool operator != (double rhs) const noexcept { return value != rhs; } + bool operator !=(const double rhs) const noexcept { return value != rhs; } }; -/* Return a VtlQuantity result of adding double `lhs` plus VtlQuantity `rhs` - +/** Return a VtlQuantity result of adding double `lhs` plus VtlQuantity `rhs` + @author Leandro Rabindranath Leon @ingroup Units */ -inline VtlQuantity operator + (double lhs, const VtlQuantity & rhs) +inline VtlQuantity operator +(const double lhs, const VtlQuantity & rhs) { - return VtlQuantity(rhs.unit, lhs + rhs.get_value()); + return {rhs.unit, lhs + rhs.get_value()}; } -/* Return a VtlQuantity result of adding a VtlQuantity `lhs` plus +/** Return a VtlQuantity result of adding a VtlQuantity `lhs` plus double `rhs` - + @author Leandro Rabindranath Leon @ingroup Units */ -inline VtlQuantity operator + (const VtlQuantity & lhs, double rhs) +inline VtlQuantity operator +(const VtlQuantity & lhs, const double rhs) { - return VtlQuantity(lhs.unit, lhs.get_value() + rhs); + return {lhs.unit, lhs.get_value() + rhs}; } -/* Return a VtlQuantity result of subtracting double `lhs` minus +/** Return a VtlQuantity result of subtracting double `lhs` minus VtlQuantity `rhs` - + @author Leandro Rabindranath Leon @ingroup Units */ -inline VtlQuantity operator - (double lhs, const VtlQuantity & rhs) +inline VtlQuantity operator -(const double lhs, const VtlQuantity & rhs) { - return VtlQuantity(rhs.unit, lhs - rhs.get_value()); + return {rhs.unit, lhs - rhs.get_value()}; } -/* Return a VtlQuantity result of subtracting a VtlQuantity `lhs` minus +/** Return a VtlQuantity result of subtracting a VtlQuantity `lhs` minus double `rhs` @author Leandro Rabindranath Leon @ingroup Units */ -inline VtlQuantity operator - (const VtlQuantity & lhs, double rhs) +inline VtlQuantity operator -(const VtlQuantity & lhs, double rhs) { return VtlQuantity(lhs.unit, lhs.get_value() - rhs); } -/* Return the product of double `lhs` times the VtlQuantity `rhs`. - +/** Return the product of double `lhs` times the VtlQuantity `rhs`. + @author Leandro Rabindranath Leon @ingroup Units */ -inline VtlQuantity operator * (double lhs, const VtlQuantity & rhs) +inline VtlQuantity operator *(double lhs, const VtlQuantity & rhs) { - return VtlQuantity(rhs.unit, lhs*rhs.get_value()); + return VtlQuantity(rhs.unit, lhs * rhs.get_value()); } -/* Return the product of VtlQuantity `lhs` times the double `rhs`. - +/** Return the product of VtlQuantity `lhs` times the double `rhs`. + @author Leandro Rabindranath Leon @ingroup Units */ -inline VtlQuantity operator * (const VtlQuantity & lhs, double rhs) +inline VtlQuantity operator *(const VtlQuantity & lhs, double rhs) { - return VtlQuantity(lhs.unit, lhs.get_value()*rhs); + return VtlQuantity(lhs.unit, lhs.get_value() * rhs); } -/* Return the double `lhs` divided by the VtlQuantity `rhs`. - +/** Return the double `lhs` divided by the VtlQuantity `rhs`. + @author Leandro Rabindranath Leon @ingroup Units */ -inline VtlQuantity operator / (double lhs, const VtlQuantity & rhs) +inline VtlQuantity operator /(double lhs, const VtlQuantity & rhs) { return VtlQuantity(rhs.unit, lhs / rhs.get_value()); } -/* Return the double `lhs` divided by the VtlQuantity `rhs`. - +/** Return the double `lhs` divided by the VtlQuantity `rhs`. + @author Leandro Rabindranath Leon @ingroup Units */ -inline VtlQuantity operator / (const VtlQuantity & lhs, double rhs) +inline VtlQuantity operator /(const VtlQuantity & lhs, double rhs) { return VtlQuantity(lhs.unit, lhs.get_value() / rhs); } @@ -2207,7 +2426,7 @@ inline VtlQuantity Unit::max() const noexcept } /** Return the minimum allowed value for the unit - + @author Leandro Rabindranath Leon @ingroup Units */ @@ -2218,7 +2437,7 @@ inline VtlQuantity Unit::min() const noexcept /** Increase the value to the immediately superior according to the floating point representation - + @ingroup Units @author Leandro Rabindranath Leon */ @@ -2230,7 +2449,7 @@ inline void BaseQuantity::increase() /** Decrease the value to the immediately inferior according to the floating point representation - + @ingroup Units @author Leandro Rabindranath Leon */ @@ -2240,62 +2459,62 @@ inline void BaseQuantity::decrease() check_value(); } -/* Less comparison between a double `lhs` and a VtlQuantity `rhs` +/** Less comparison between a double `lhs` and a VtlQuantity `rhs` @author Leandro Rabindranath Leon @ingroup Units */ -inline bool operator < (double lhs, const VtlQuantity & rhs) noexcept +inline bool operator <(double lhs, const VtlQuantity & rhs) noexcept { return lhs < rhs.get_value(); } -/* Greater comparison between a double `lhs` and a VtlQuantity `rhs` +/** Greater comparison between a double `lhs` and a VtlQuantity `rhs` @author Leandro Rabindranath Leon @ingroup Units */ -inline bool operator > (double lhs, const VtlQuantity & rhs) noexcept +inline bool operator >(double lhs, const VtlQuantity & rhs) noexcept { return lhs > rhs.get_value(); } -/* Less or equal comparison between a double `lhs` and a VtlQuantity `rhs` +/** Less or equal comparison between a double `lhs` and a VtlQuantity `rhs` @author Leandro Rabindranath Leon @ingroup Units */ -inline bool operator <= (double lhs, const VtlQuantity & rhs) noexcept +inline bool operator <=(double lhs, const VtlQuantity & rhs) noexcept { return lhs <= rhs.get_value(); } -/* Greater or equal comparison between a double `lhs` and a VtlQuantity `rhs` +/** Greater or equal comparison between a double `lhs` and a VtlQuantity `rhs` @author Leandro Rabindranath Leon @ingroup Units */ -inline bool operator >= (double lhs, const VtlQuantity & rhs) noexcept +inline bool operator >=(double lhs, const VtlQuantity & rhs) noexcept { return lhs >= rhs.get_value(); } -/* Equal comparison between a double `lhs` and a VtlQuantity `rhs` +/** Equal comparison between a double `lhs` and a VtlQuantity `rhs` @author Leandro Rabindranath Leon @ingroup Units */ -inline bool operator == (double lhs, const VtlQuantity & rhs) noexcept +inline bool operator ==(double lhs, const VtlQuantity & rhs) noexcept { return lhs == rhs.get_value(); } -/* Not equal comparison between a double `lhs` and a VtlQuantity `rhs` +/** Not equal comparison between a double `lhs` and a VtlQuantity `rhs` @author Leandro Rabindranath Leon @ingroup Units */ -inline bool operator != (double lhs, const VtlQuantity & rhs) noexcept +inline bool operator !=(double lhs, const VtlQuantity & rhs) noexcept { return lhs != rhs.get_value(); } @@ -2321,7 +2540,7 @@ Quantity::Quantity(const VtlQuantity & q) } template -Quantity & Quantity::operator = (const VtlQuantity & q) +Quantity &Quantity::operator =(const VtlQuantity & q) { if (&unit == &q.unit) { @@ -2330,16 +2549,16 @@ Quantity & Quantity::operator = (const VtlQuantity & q) } if (q.is_null()) - ALEPHTHROW(InvalidConversion, - "Intent of assigning a Null Unit to a Quantity"); - + ah_invalid_conversion_error() << "Intent of assigning a Null Unit to a Quantity"; + value = unit_convert(q.unit, q.get_value(), unit); check_value(); return *this; } -template Quantity & -Quantity::operator += (const VtlQuantity & rhs) const +template +Quantity & +Quantity::operator +=(const VtlQuantity & rhs) { verify_same_unit(rhs.unit); value += rhs.get_value(); @@ -2347,8 +2566,9 @@ Quantity::operator += (const VtlQuantity & rhs) const return *this; } -template Quantity & -Quantity::operator -= (const VtlQuantity & rhs) const +template +Quantity & +Quantity::operator -=(const VtlQuantity & rhs) { verify_same_unit(rhs.unit); value -= rhs.get_value(); @@ -2356,8 +2576,9 @@ Quantity::operator -= (const VtlQuantity & rhs) const return *this; } -template VtlQuantity -Quantity::operator + (const VtlQuantity & rhs) const +template +VtlQuantity +Quantity::operator +(const VtlQuantity & rhs) const { verify_same_unit(rhs.unit); VtlQuantity ret(*this); @@ -2365,8 +2586,9 @@ Quantity::operator + (const VtlQuantity & rhs) const return ret; } -template VtlQuantity -Quantity::operator - (const VtlQuantity & rhs) const +template +VtlQuantity +Quantity::operator -(const VtlQuantity & rhs) const { verify_same_unit(rhs.unit); VtlQuantity ret(*this); @@ -2374,50 +2596,60 @@ Quantity::operator - (const VtlQuantity & rhs) const return ret; } -template VtlQuantity -Quantity::operator * (const VtlQuantity & rhs) const +template +VtlQuantity +Quantity::operator *(const VtlQuantity & rhs) const { - return VtlQuantity(VtlQuantity::verify_compound(unit, rhs.unit).name, - value*rhs.get_value()); + return VtlQuantity(VtlQuantity::verify_product_compound(unit, rhs.unit).name, + value * rhs.get_value()); } -template VtlQuantity -Quantity::operator / (const VtlQuantity & rhs) const +template +VtlQuantity +Quantity::operator /(const VtlQuantity & rhs) const { - return VtlQuantity(VtlQuantity::verify_compound(unit, rhs.unit).name, - value/rhs.get_value()); + return VtlQuantity(VtlQuantity::verify_ratio_compound(unit, rhs.unit).name, + value / rhs.get_value()); } -inline pair> - check_conversions(const PhysicalQuantity & pq) +/** Validate directed conversion coverage for all units in a physical quantity. + + @param[in] pq Physical quantity to inspect. + @return Pair where: + - `first` is `true` when coverage is complete. + - `second` contains human-readable messages for missing conversions. + @ingroup Units + */ +inline std::pair> +check_conversions(const PhysicalQuantity & pq) { - DynList missing; + DynList missing; auto units = Unit::units(pq); for (auto it1 = units.get_it(); it1.has_curr(); it1.next()) { auto src_unit = it1.get_curr(); for (auto it2 = units.get_it(); it2.has_curr(); it2.next()) - { - auto tgt_unit = it2.get_curr(); - if (src_unit == tgt_unit) - continue; - if (not exist_conversion(*src_unit, *tgt_unit)) - { - ostringstream s; - s << "Missing conversion from " << src_unit->name << "(" - << src_unit->symbol << ") to " << tgt_unit->name << "(" - << tgt_unit->symbol << ")"; - missing.append(s.str()); - } - } + { + auto tgt_unit = it2.get_curr(); + if (src_unit == tgt_unit) + continue; + if (not exist_conversion(*src_unit, *tgt_unit)) + { + std::ostringstream s; + s << "Missing conversion from " << src_unit->name << "(" + << src_unit->symbol << ") to " << tgt_unit->name << "(" + << tgt_unit->symbol << ")"; + missing.append(s.str()); + } + } } - return make_pair(missing.is_empty(), move(missing)); + return std::make_pair(missing.is_empty(), std::move(missing)); } -/** Return the value immediately superior according to the floating +/** Return the value immediately superior, according to the floating point representation - + @ingroup Units @author Leandro Rabindranath Leon */ @@ -2428,7 +2660,7 @@ inline VtlQuantity next_value(const VtlQuantity & val) /** Return the value immediately inferior according to the floating point representation - + @ingroup Units @author Leandro Rabindranath Leon */ @@ -2437,5 +2669,4 @@ inline VtlQuantity prev_value(const VtlQuantity & val) return VtlQuantity(val.unit, prev_value(val.raw())); } -# endif - +# endif diff --git a/include/unit-exceptions.H b/include/unit-exceptions.H index 9119328..ceb7062 100644 --- a/include/unit-exceptions.H +++ b/include/unit-exceptions.H @@ -22,31 +22,73 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -*/#ifndef UNIT_EXCEPTIONS_H +*/ +/** @file unit-exceptions.H + @brief Defines exception classes for the uconv library. + + This file uses the Aleph-w exception macros to define specific exceptions + that can be thrown during unit operations, such as invalid conversions, + out-of-range values, or mismatched units. + + @author Leandro Rabindranath Leon + @ingroup Units +*/ + +#ifndef UNIT_EXCEPTIONS_H #define UNIT_EXCEPTIONS_H # include +/** Thrown when a physical quantity is invalid or not recognized. + @ingroup Units +*/ DEFINE_ALEPH_EXCEPTION(InvalidPhysicalQuantity, "Invalid physical quantity") +/** Thrown when attempting an operation between units that do not belong to the same physical quantity. + @ingroup Units +*/ DEFINE_ALEPH_EXCEPTION(WrongSiblingUnit, "Sibling unit is not physically related"); +/** Thrown when an invalid ratio is provided for unit epsilon calculation. + @ingroup Units +*/ DEFINE_ALEPH_EXCEPTION(WrongUnitRatio, "wrong unit ratio"); +/** Thrown when a conversion between two units has not been registered. + @ingroup Units +*/ DEFINE_ALEPH_EXCEPTION(UnitConversionNotFound, "Unit conversion not found"); +/** Thrown when attempting to register a unit conversion that already exists. + @ingroup Units +*/ DEFINE_ALEPH_EXCEPTION(DuplicatedUnitConversion, "duplicated unit conversion") +/** Thrown when a value exceeds the defined valid range for a unit. + @ingroup Units +*/ DEFINE_ALEPH_EXCEPTION(OutOfUnitRange, "Value is out of unit range"); +/** Thrown when a binary operation (like addition) is attempted on quantities with different units without implicit conversion. + @ingroup Units +*/ DEFINE_ALEPH_EXCEPTION(DifferentUnits, "Binary Operation between involves different units"); +/** Thrown when a unit cannot be found by name or symbol. + @ingroup Units +*/ DEFINE_ALEPH_EXCEPTION(UnitNotFound, "unit not found"); +/** Thrown when a compound unit (product or ratio) cannot be found or derived. + @ingroup Units +*/ DEFINE_ALEPH_EXCEPTION(CompoundUnitNotFound, "compound unit not found"); +/** General base exception for unit-related errors. + @ingroup Units +*/ DEFINE_ALEPH_EXCEPTION(UnitException, "unit exception"); #endif diff --git a/include/unititem.H b/include/unititem.H index 45acceb..81334f2 100644 --- a/include/unititem.H +++ b/include/unititem.H @@ -23,32 +23,47 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -/* - Basic bookkeeping for uconv unit library - */ +/** @file unititem.H + @brief Basic bookkeeping structures for the uconv unit library. + + This file defines the `UnitItem` struct and `UnitItemTable` class, which are + fundamental for storing metadata about physical quantities and units, such as + names, symbols, and descriptions. + + @author Leandro Rabindranath Leon + @ingroup Units +*/ # ifndef DESC_TABLE_H # define DESC_TABLE_H # include # include # include +# include +# include +# include # include # include + /** This is a basic descriptor used for physical quantities and units. + It stores essential information like the name, symbol, LaTeX representation, + and a textual description. + @author Leandro Rabindranath Leon + @ingroup Units */ struct UnitItem { friend class PhysicalQuantity; friend class Unit; - const std::string name = "Undefined"; - const std::string symbol = "Undefined"; - const std::string description = "Undefined"; - const std::string latex_symbol = "Undefined"; + const std::string name = "Undefined"; ///< The full name of the item (e.g., "meter"). + const std::string symbol = "Undefined"; ///< The symbol of the item (e.g., "m"). + const std::string description = "Undefined"; ///< A textual description. + const std::string latex_symbol = "Undefined"; ///< The LaTeX representation of the symbol. private: @@ -56,6 +71,12 @@ private: public: + /** Constructor with name, symbol, and description. + + @param[in] __name The full name. + @param[in] __symbol The symbol. + @param[in] desc The description. + */ UnitItem(const std::string & __name, const std::string & __symbol, const std::string & desc) noexcept : name(__name), symbol(__symbol), description(desc) @@ -63,6 +84,13 @@ public: // empty } + /** Constructor with name, symbol, LaTeX symbol, and description. + + @param[in] name The full name. + @param[in] symbol The symbol. + @param[in] latex_symbol The LaTeX representation of the symbol. + @param[in] desc The description. + */ UnitItem(const std::string & name, const std::string & symbol, const std::string & latex_symbol, const std::string & desc) noexcept @@ -71,27 +99,41 @@ public: // empty } - string to_string() const + /** Convert the item to a string representation. + + @return A string containing the name, symbol, and description. + */ + std::string to_string() const { - ostringstream s; - s << "UnitItem name = " << name << endl - << "Symbol = " << symbol << endl + std::ostringstream s; + s << "UnitItem name = " << name << std::endl + << "Symbol = " << symbol << std::endl << "Description = " << description; return s.str(); } - string to_string(size_t width, size_t left_margin = 0) const + /** Convert the item to a formatted string representation. + + @param[in] width The width for text alignment. + @param[in] left_margin The number of spaces for the left margin. + @return A formatted string. + */ + std::string to_string(std::size_t width, std::size_t left_margin = 0) const { - const string margin(left_margin, ' '); - ostringstream s; - s << margin << "UnitItem name = " << name << endl - << margin << "Symbol = " << symbol << endl + const std::string margin(left_margin, ' '); + std::ostringstream s; + s << margin << "UnitItem name = " << name << std::endl + << margin << "Symbol = " << symbol << std::endl << margin << "Description = " << align_text_to_left_except_first(description, width, 16); return s.str(); } - friend ostream & operator << (ostream & out, const UnitItem & ui) + /** Stream insertion operator. + + Allows printing the UnitItem to an output stream. + */ + friend std::ostream & operator << (std::ostream & out, const UnitItem & ui) { return out << ui.to_string(); } @@ -99,7 +141,11 @@ public: /** Basic table storing physical quantities and unit descriptors. + This class maintains two indices for `UnitItem` objects: one by name and one by symbol. + It ensures uniqueness of names and symbols within the table. + @author Leandro Rabindranath Leon + @ingroup Units */ class UnitItemTable { @@ -115,11 +161,12 @@ public: } /// Return all the stored descriptor names. - DynList names() const { return name_tbl.keys(); } + DynList names() const { return name_tbl.keys(); } - DynList symbols() const { return symbol_tbl.keys(); } + /// Return all the stored descriptor symbols. + DynList symbols() const { return symbol_tbl.keys(); } - /// Return an iterator to a list of pair + /// Return an iterator to a list of std::pair /// items. First item is the descriptor name and the second one is a /// pointer to the descriptor. auto get_it() { return name_tbl.get_it(); } @@ -128,44 +175,36 @@ public: @param[in] ptr pointer to the descriptor. It must correctly allocated. @throw bad_alloc if there is no enough memory. - @throw domain_error if it already exists a descriptor with the + @throw std::domain_error if it already exists a descriptor with the same name or the same symbol. */ void register_item(const UnitItem * ptr) { - if (name_tbl.contains(ptr->name)) - { - ostringstream s; - s << " name " << ptr->name << " already exist"; - throw domain_error(s.str()); - } - - if (symbol_tbl.contains(ptr->symbol)) - { - ostringstream s; - s << "unit symbol " << ptr->symbol << " already exist"; - throw domain_error(s.str()); - } + ah_domain_error_if(name_tbl.contains(ptr->name)) + << " name " << ptr->name << " already exist"; + + ah_domain_error_if(symbol_tbl.contains(ptr->symbol)) + << "unit symbol " << ptr->symbol << " already exist"; name_tbl.insert(ptr->name, ptr); symbol_tbl.insert(ptr->symbol, ptr); } /// Return true if the table contains a descriptor with name `name`. - bool exists_name(const string & name) const noexcept + bool exists_name(const std::string & name) const noexcept { return name_tbl.contains(name); } /// Return true if the table contains a descriptor with symbol `symbol`. - bool exists_symbol(const string & symbol) const noexcept + bool exists_symbol(const std::string & symbol) const noexcept { return symbol_tbl.contains(symbol); } /// Return a pointer to the descriptor containing the name `name` if /// this exists; otherwise, it returns `nullptr` - const UnitItem * search_by_name(const string & name) const noexcept + const UnitItem * search_by_name(const std::string & name) const noexcept { auto p = name_tbl.search(name); return p ? p->second : nullptr; @@ -173,38 +212,30 @@ public: /// Return a pointer to the descriptor containing the symbol `symbol` if /// this exists; otherwise, it returns `nullptr` - const UnitItem * search_by_symbol(const string & symbol) const noexcept + const UnitItem * search_by_symbol(const std::string & symbol) const noexcept { auto p = symbol_tbl.search(symbol); return p ? p->second : nullptr; } /// Return the number of descriptors stored. - size_t size() const noexcept { return name_tbl.size(); } + std::size_t size() const noexcept { return name_tbl.size(); } - /// Throw `domain_error` exception if the descriptor pointed by - /// `ptr` already contains the string `str` as name or symbol. It is + /// Throw `std::domain_error` exception if the descriptor pointed by + /// `ptr` already contains the std::string `str` as name or symbol. It is /// a helper validation function. - void validate(const UnitItem * ptr, const string & str) + void validate(const UnitItem * ptr, const std::string & str) { { - const string & name = ptr->name; - if (exists_name(str)) - { - ostringstream s; - s << "Repeated " << str << " name " << name; - throw domain_error(s.str()); - } + const std::string & name = ptr->name; + ah_domain_error_if(exists_name(str)) + << "Repeated " << str << " name " << name; } { - const string & symbol = ptr->symbol; - if (exists_symbol(str)) - { - ostringstream s; - s << "Repeated " << str << " symbol " << symbol; - throw domain_error(s.str()); - } + const std::string & symbol = ptr->symbol; + ah_domain_error_if(exists_symbol(str)) + << "Repeated " << str << " symbol " << symbol; } } }; diff --git a/include/units/dummy-unit.H b/include/units/dummy-unit.H index 4c0214a..6c55133 100644 --- a/include/units/dummy-unit.H +++ b/include/units/dummy-unit.H @@ -30,6 +30,6 @@ Declare_Physical_Quantity(DummyUnit, "Dummy", "Dummy", "Only for testing"); Declare_Unit(TestUnit, "TU", "TU", "Only for testing", DummyUnit, - numeric_limits::min(), numeric_limits::max()); + std::numeric_limits::min(), std::numeric_limits::max()); # endif diff --git a/include/unittest-utils.H b/include/unittest-utils.H index 5aa4e54..fb51c64 100644 --- a/include/unittest-utils.H +++ b/include/unittest-utils.H @@ -29,10 +29,10 @@ struct UnitTest { - const string csv_name = "No-defined"; - const size_t csv_line = 0; - const string src_unit_name = "No-defined"; - const string tgt_unit_name = "No-defined"; + const std::string csv_name = "No-defined"; + const std::size_t csv_line = 0; + const std::string src_unit_name = "No-defined"; + const std::string tgt_unit_name = "No-defined"; const double value = 0; const double expected = 0; const double tolerance = 0; @@ -46,8 +46,8 @@ public: UnitTest() {} - UnitTest(const string & csv_name, size_t csv_line, - const string & src_unit_name, const string & tgt_unit_name, + UnitTest(const std::string & csv_name, std::size_t csv_line, + const std::string & src_unit_name, const std::string & tgt_unit_name, double value, double expected, double tolerance) : csv_name(csv_name), csv_line(csv_line), src_unit_name(src_unit_name), tgt_unit_name(tgt_unit_name), value(value), expected(expected), @@ -61,22 +61,22 @@ public: bool pass() { result = convert(); - error = fabs(expected - result)/expected; + error = std::fabs(expected - result)/expected; return error <= tolerance; } - string to_string() const + std::string to_string() const { - ostringstream s; - - s.precision(numeric_limits::max_digits10); - s << "line = " << csv_line << endl - << "source unit = " << src_unit_name << endl - << "target unit = " << tgt_unit_name << endl - << "value = " << value << endl - << "expected = " << expected << endl - << "result = " << result << endl - << "error = " << error << endl + std::ostringstream s; + + s.precision(std::numeric_limits::max_digits10); + s << "line = " << csv_line << std::endl + << "source unit = " << src_unit_name << std::endl + << "target unit = " << tgt_unit_name << std::endl + << "value = " << value << std::endl + << "expected = " << expected << std::endl + << "result = " << result << std::endl + << "error = " << error << std::endl << "tolerance = " << tolerance; return s.str(); @@ -86,13 +86,13 @@ public: class UnitTester { DynList test_list; - DynList failed_list; - DynList passed_list; + DynList failed_list; + DynList passed_list; public: - void define_test(const string & csv_name, size_t csv_line, - const string & src_unit_name, const string & tgt_unit_name, + void define_test(const std::string & csv_name, std::size_t csv_line, + const std::string & src_unit_name, const std::string & tgt_unit_name, double value, double expected, double tolerance) { test_list.emplace(csv_name, csv_line, src_unit_name, tgt_unit_name, @@ -111,11 +111,11 @@ public: } } - static void report(const DynList & l) + static void report(const DynList & l) { for (auto it = l.get_it(); it.has_curr(); it.next()) - cout << it.get_curr() << endl - << endl; + std::cout << it.get_curr() << std::endl + << std::endl; } void report_failed() const diff --git a/lib/80.txt b/lib/80.txt deleted file mode 100644 index a2d9b19..0000000 --- a/lib/80.txt +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt new file mode 100644 index 0000000..a691d8e --- /dev/null +++ b/lib/CMakeLists.txt @@ -0,0 +1,46 @@ +# Generation of units headers +# We use bin/gen-units to generate C++ headers in the build tree from the Ruby DSL + +set(GEN_UNITS ${CMAKE_SOURCE_DIR}/bin/gen-units) +set(UNITS_DSL ${CMAKE_SOURCE_DIR}/lib/unit-defs.rb) +set(UNITS_DSL_DIR ${CMAKE_SOURCE_DIR}/lib/unit-defs) +if(NOT DEFINED UCONV_GENERATED_UNITS_DIR) + set(UCONV_GENERATED_UNITS_DIR ${CMAKE_BINARY_DIR}/generated/units) +endif() +if(NOT DEFINED UCONV_GENERATED_DIR) + set(UCONV_GENERATED_DIR ${CMAKE_BINARY_DIR}/generated) +endif() +set(UNITS_OUTPUT_DIR ${UCONV_GENERATED_UNITS_DIR}) +set(ALL_UNITS_H ${UNITS_OUTPUT_DIR}/all_units.H) +set(UNITS_CATALOG_MD ${UNITS_OUTPUT_DIR}/units_catalog.md) +set(UCONV_CC ${CMAKE_CURRENT_SOURCE_DIR}/uconv.cc) + +# List all DSL files for dependency tracking +file(GLOB DSL_FILES "${UNITS_DSL_DIR}/*.rb") + +# Custom command to generate headers +add_custom_command( + OUTPUT ${ALL_UNITS_H} ${UNITS_CATALOG_MD} + COMMAND ruby ${GEN_UNITS} ${UNITS_DSL} ${UNITS_OUTPUT_DIR} + DEPENDS ${GEN_UNITS} ${UNITS_DSL} ${DSL_FILES} + COMMENT "Generating C++ units headers from Ruby DSL" +) + +# Ensure generated headers and catalog are always materialized before build/docs. +add_custom_target(uconv_units_generated DEPENDS ${ALL_UNITS_H} ${UNITS_CATALOG_MD}) + +# Create the static library uconv +# We include ALL_UNITS_H in sources to ensure the custom command runs +# uconv.cc now includes all_units.H directly +add_library(uconv STATIC ${UCONV_CC} ${ALL_UNITS_H}) +add_dependencies(uconv uconv_units_generated) + +# Include directories for the library +target_include_directories(uconv PUBLIC + ${UCONV_GENERATED_DIR} + ${UNITS_OUTPUT_DIR} + ${CMAKE_SOURCE_DIR}/include + ${ALEPHW} + ${JSON_PATH}/include + ${CMAKE_CURRENT_BINARY_DIR} +) diff --git a/lib/Imakefile b/lib/Imakefile deleted file mode 100755 index aa22e68..0000000 --- a/lib/Imakefile +++ /dev/null @@ -1,48 +0,0 @@ -DEPEND = sh ./depend.sh - -CXX = clang++ -AR = llvm-ar clq -RANLIB = llvm-ranlib - -ALEPHW = $(shell echo $$ALEPHW) -JSON = $(shell echo $$JSON)/include - -INCLUDES = -I$(TOP)/include -I$(ALEPHW) -I$(JSON) -WARN= -Wall -Wextra -Wcast-align -Wno-sign-compare -Wno-write-strings -Wno-parentheses - -# By default this lib is compiled with clang sanitizer options -# Comment the following line and uncoment the next one if you want -# full performamce -FLAGS = -std=c++14 $(WARN) -O0 -g -fsanitize=address,undefined -fno-omit-frame-pointer -#FLAGS = -std=c++14 $(WARN) -O2 - -OPTIONS = $(FLAGS) -CXXFLAGS= -std=c++14 $(INCLUDES) $(OPTIONS) - -LIBSRCS = units-vars.cc - -UNITSPATH = $(TOP)/include - -SRCS = uconv.cc -OBJS = uconv.o - -EXTRACT_CV = $(TOP)/bin/extract-cv - -units.cc: $(TOP)/include/uconv.H uconv.cc - $(RM) -f $@; \ @@\ - $(CP) preamble.txt $*.tmp; \ @@\ - $(CPP) $(CXXFLAGS) uconv.cc >> $*.tmp; \ @@\ - cat uconv.cc >> $@; \ @@\ - $(EXTRACT_CV) $*.tmp >> $@; \ @@\ - $(RM) $*.tmp - -units.o:: units.cc - -clean:: - $(RM) -f units.cc - -NormalLibraryObjectRule() -NormalLibraryTarget(uconv,units.o) -InstallLibrary(uconv,./) - -DependTarget() diff --git a/lib/TODO.md b/lib/TODO.md deleted file mode 100644 index 57c24f4..0000000 --- a/lib/TODO.md +++ /dev/null @@ -1 +0,0 @@ -# TODO list diff --git a/lib/TODO.txt b/lib/TODO.txt deleted file mode 100644 index e69de29..0000000 diff --git a/lib/depend.sh b/lib/depend.sh deleted file mode 100755 index cff2012..0000000 --- a/lib/depend.sh +++ /dev/null @@ -1,129 +0,0 @@ -#!/bin/sh - -# Workaround copy of gccmakedep designed for avoid the problems related -# to c++11 - -# -# makedepend which uses 'gcc -M' -# -# $XFree86: xc/config/util/gccmdep.cpp,v 3.10tsi Exp $ -# -# Based on mdepend.cpp and code supplied by Hongjiu Lu -# - -TMP=mdep$$.tmp -CC=gcc -RM="rm -f" -LN=ln -MV=mv - -${RM} ${TMP} - -trap "${RM} ${TMP}*; exit 1" 1 2 15 -trap "${RM} ${TMP}*; exit 0" 1 2 13 - -files= -makefile= -endmarker= -magic_string='# DO NOT DELETE' -append=n -args= - -while [ $# != 0 ]; do - if [ "$endmarker"x != x -a "$endmarker" = "$1" ]; then - endmarker= - else - case "$1" in - -D*|-I*|-U*) - args="$args '$1'" - ;; - -g*|-O*) - ;; - *) - if [ "$endmarker"x = x ]; then - case $1 in -# ignore these flags - -w|-o|-cc) - shift - ;; - -v) - ;; - -s) - magic_string="$2" - shift - ;; - -f*) - if [ "$1" = "-f-" ]; then - makefile="-" - elif [ "$1" = "-f" ]; then - makefile="$2" - shift - else - echo "$1" | sed 's/^\-f//' >${TMP}arg - makefile="`cat ${TMP}arg`" - rm -f ${TMP}arg - fi - ;; - --*) - endmarker=`echo $1 | sed 's/^\-\-//'` - if [ "$endmarker"x = x ]; then - endmarker="--" - fi - ;; - -a) - append=y - ;; - -*) - echo "Unknown option '$1' ignored" 1>&2 - ;; - *) - files="$files $1" - ;; - esac - fi - ;; - esac - fi - shift -done - -if [ x"$files" = x ]; then -# Nothing to do - exit 0 -fi - -case "$makefile" in - '') - if [ -r makefile ]; then - makefile=makefile - elif [ -r Makefile ]; then - makefile=Makefile - else - echo 'no makefile or Makefile found' 1>&2 - exit 1 - fi - ;; -esac - -if [ X"$makefile" != X- ]; then - if [ x"$append" = xn ]; then - sed -e "/^$magic_string/,\$d" < $makefile > $TMP - echo "$magic_string" >> $TMP - else - cp $makefile $TMP - fi -fi - -CMD="$CC -M -std=c++11 $args $files" -if [ X"$makefile" != X- ]; then - CMD="$CMD >> $TMP" -fi -eval $CMD -if [ X"$makefile" != X- ]; then - $RM ${makefile}.bak - $MV $makefile ${makefile}.bak - $MV $TMP $makefile -fi - -$RM ${TMP}* -exit 0 diff --git a/lib/uconv.cc b/lib/uconv.cc index 31e1113..5f721b6 100644 --- a/lib/uconv.cc +++ b/lib/uconv.cc @@ -23,17 +23,77 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */# include +# include +# include +# include # include # include -# include +// # include // Removed in favor of generated headers +# include // Include all generated units to ensure registration # include using json = nlohmann::json; -static void init_unit_converters(); +namespace +{ + std::string compact_symbol(const std::string & symbol) + { + std::string out; + out.reserve(symbol.size()); + for (unsigned char c : symbol) + if (not std::isspace(c)) + out.push_back(static_cast(c)); + return out; + } + + std::string to_lower_ascii(std::string s) + { + std::transform(s.begin(), s.end(), s.begin(), + [] (unsigned char c) { return static_cast(std::tolower(c)); }); + return s; + } + + const std::unordered_map & + unit_symbol_aliases() + { + static const std::unordered_map aliases = + { + {"s", "sec"}, + {"sec", "sec"}, + {"second", "sec"}, + {"seconds", "sec"}, + {"hr", "h"}, + {"hrs", "h"}, + {"hour", "h"}, + {"hours", "h"}, + {"d", "day"}, + {"day", "day"}, + {"days", "day"}, + {"\xCE\xA9", "ohm"}, // Ω + {"\xE2\x84\xA6", "ohm"}, // Ω + {"k\xCE\xA9", "kohm"}, // kΩ + {"k\xE2\x84\xA6", "kohm"}, // kΩ + {"M\xCE\xA9", "Mohm"}, // MΩ + {"M\xE2\x84\xA6", "Mohm"}, // MΩ + {"m\xCE\xA9", "mohm"}, // mΩ + {"m\xE2\x84\xA6", "mohm"}, // mΩ + {"\xCE\xBC" "F", "uF"}, // μF + {"\xC2\xB5" "F", "uF"}, // µF + {"\xCE\xBC" "f", "uF"}, // μf + {"\xC2\xB5" "f", "uF"}, // µf + {"\xC2\xB0" "C", "degC"}, // °C + {"\xC2\xB0" "F", "degF"}, // °F + {"\xC2\xB0" "R", "degR"}, // °R + {"K", "degK"} + }; + return aliases; + } +} + +// static void init_unit_converters(); // Removed // the following data is declared in units.H UnitItemTable * PhysicalQuantity::tbl = nullptr; @@ -42,14 +102,14 @@ UnitItemTable * Unit::tbl = nullptr; DynSetTree * Unit::unit_tbl = nullptr; -static size_t fst_unit_pair_hash -(const pair, Unit_Convert_Fct_Ptr> & p) +static std::size_t fst_unit_pair_hash +(const std::pair, Unit_Convert_Fct_Ptr> & p) { return dft_hash_fct(p.first); } -static size_t snd_unit_pair_hash -(const pair, Unit_Convert_Fct_Ptr> & p) +static std::size_t snd_unit_pair_hash +(const std::pair, Unit_Convert_Fct_Ptr> & p) { return snd_hash_fct(p.first); } @@ -61,7 +121,7 @@ const PhysicalQuantity PhysicalQuantity::null_physical_quantity; const Unit Unit::null_unit; -const double Unit::Invalid_Value = numeric_limits::max(); +const double Unit::Invalid_Value = std::numeric_limits::max(); const VtlQuantity VtlQuantity::null_quantity; @@ -76,7 +136,7 @@ const VtlQuantity VtlQuantity::null_quantity; */ UnitsInstancer::UnitsInstancer() { - static size_t count = 0; + static std::size_t count = 0; if (count++ > 0) return; @@ -94,20 +154,10 @@ UnitsInstancer::UnitsInstancer() static CompoundUnitTbl __compound_unit_tbl; ::__compound_unit_tbl = &__compound_unit_tbl; - new ((void*) &PhysicalQuantity::null_physical_quantity) - PhysicalQuantity("NullPhysicalQuantity", "NullPQ", - "Null" "Null Physical Quantity"); - - new ((void*) &Unit::null_unit) Unit("NullUnit", "Null Unit", "Null unit", "Null", - PhysicalQuantity::null_physical_quantity, - numeric_limits::min(), - numeric_limits::max()); + // Keep default-initialized null sentinels. Reconstructing them via placement-new + // here causes LSan-visible leaks in their internal string/list state at shutdown. - new ((void*) &VtlQuantity::null_quantity) - VtlQuantity(Unit::null_unit, numeric_limits::max()); - - init_unit_converters(); // this routine is generated by extract-cv - // script and concatenated to this file + // init_unit_converters(); // Removed } bool conversion_exist(const char * src_symbol, const char * tgt_symbol) @@ -147,7 +197,7 @@ static json to_json(const PhysicalQuantity * const pq) return j; } -string units_json() +std::string units_json() { json j; j["uconv_physical_quantities"] = @@ -158,4 +208,33 @@ string units_json() const UnitsInstancer & units_instance = UnitsInstancer::init(); -// The following global singleton variables are generated by extract-cv script +std::string Unit::normalize_lookup_token(const std::string & token) +{ + auto it = token.begin(); + auto et = token.end(); + while (it != et and std::isspace(static_cast(*it))) + ++it; + while (it != et and std::isspace(static_cast(*(et - 1)))) + --et; + + return std::string(it, et); +} + +std::string Unit::normalize_symbol_alias(const std::string & symbol) +{ + const std::string compacted = compact_symbol(symbol); + if (compacted.empty()) + return compacted; + + const auto & aliases = unit_symbol_aliases(); + if (const auto it = aliases.find(compacted); it != aliases.end()) + return it->second; + + const std::string lower = to_lower_ascii(compacted); + if (const auto it = aliases.find(lower); it != aliases.end()) + return it->second; + + return compacted; +} + +// The following global singleton variables are generated by extract-cv script and concatenated to this file diff --git a/lib/unit-defs.rb b/lib/unit-defs.rb new file mode 100644 index 0000000..a558833 --- /dev/null +++ b/lib/unit-defs.rb @@ -0,0 +1,27 @@ +import "lib/unit-defs/length.rb" +import "lib/unit-defs/temperature.rb" +import "lib/unit-defs/pressure.rb" +import "lib/unit-defs/mass.rb" +import "lib/unit-defs/volume.rb" +import "lib/unit-defs/density.rb" +import "lib/unit-defs/massflow.rb" +import "lib/unit-defs/dynamic_viscosity.rb" +import "lib/unit-defs/kinematic_viscosity.rb" +import "lib/unit-defs/acceleration.rb" +import "lib/unit-defs/frequency.rb" +import "lib/unit-defs/current.rb" +import "lib/unit-defs/voltage.rb" +import "lib/unit-defs/resistance.rb" +import "lib/unit-defs/capacitance.rb" +import "lib/unit-defs/charge.rb" +import "lib/unit-defs/angle.rb" +import "lib/unit-defs/time.rb" +import "lib/unit-defs/area.rb" +import "lib/unit-defs/power.rb" +import "lib/unit-defs/energy.rb" +import "lib/unit-defs/force.rb" +import "lib/unit-defs/torque.rb" +import "lib/unit-defs/diameter.rb" +import "lib/unit-defs/flowrate.rb" +import "lib/unit-defs/velocity.rb" +import "lib/unit-defs/dimensionless.rb" diff --git a/lib/unit-defs/acceleration.rb b/lib/unit-defs/acceleration.rb new file mode 100644 index 0000000..0ce19bc --- /dev/null +++ b/lib/unit-defs/acceleration.rb @@ -0,0 +1,57 @@ +physical_quantity :Acceleration do + symbol "a" + latex "a" + description "Rate of change of velocity with respect to time" +end + +unit :Meter_S2 do + symbol "m/s2" + latex "m/s^{2}" + description "Acceleration in meters per second squared" + quantity :Acceleration + range -1e6..1e6 +end + +unit :Ft_S2 do + symbol "ft/s2" + latex "ft/s^{2}" + description "Acceleration in feet per second squared" + quantity :Acceleration + range -3.280839895013123e6..3.280839895013123e6 +end + +unit :StandardGravity do + symbol "g0" + latex "g_{0}" + description "Standard gravity acceleration" + quantity :Acceleration + range -1.0197162129779283e5..1.0197162129779283e5 +end + +unit :Gal do + symbol "Gal" + latex "Gal" + description "Galileo (CGS acceleration unit), 1 Gal = 0.01 m/s2" + quantity :Acceleration + range -1e8..1e8 +end + +# To Meter_S2 +conversion :Ft_S2, :Meter_S2 do |v| "0.3048 * #{v}" end +conversion :StandardGravity, :Meter_S2 do |v| "9.80665 * #{v}" end +conversion :Gal, :Meter_S2 do |v| "1e-2 * #{v}" end + +# To Ft_S2 +conversion :Meter_S2, :Ft_S2 do |v| "3.280839895013123 * #{v}" end +conversion :StandardGravity, :Ft_S2 do |v| "32.17404855643044 * #{v}" end +conversion :Gal, :Ft_S2 do |v| "3.280839895013123e-2 * #{v}" end + +# To StandardGravity +conversion :Meter_S2, :StandardGravity do |v| "0.10197162129779283 * #{v}" end +conversion :Ft_S2, :StandardGravity do |v| "3.108094882748799e-2 * #{v}" end +conversion :Gal, :StandardGravity do |v| "1.0197162129779283e-3 * #{v}" end + +# To Gal +conversion :Meter_S2, :Gal do |v| "100 * #{v}" end +conversion :Ft_S2, :Gal do |v| "30.48 * #{v}" end +conversion :StandardGravity, :Gal do |v| "980.665 * #{v}" end diff --git a/lib/unit-defs/angle.rb b/lib/unit-defs/angle.rb new file mode 100644 index 0000000..60cd072 --- /dev/null +++ b/lib/unit-defs/angle.rb @@ -0,0 +1,25 @@ +physical_quantity :Angle do + symbol "angle" + latex "\\angle" + description "figure formed by two rays sharing a common endpoint" +end + +unit :Degree do + symbol "deg" + latex "\\degree" + description "Angle measured in Degrees" + quantity :Angle + range -90..90 +end + +unit :Radian do + symbol "rad" + latex "rad" + description "Angle measured in Radians" + quantity :Angle + range -1.5707963267948966..1.5707963267948966 +end + +# Conversions +conversion :Degree, :Radian do |val| "#{val}*M_PI/180" end +conversion :Radian, :Degree do |val| "#{val}*180/M_PI" end diff --git a/lib/unit-defs/area.rb b/lib/unit-defs/area.rb new file mode 100644 index 0000000..79313fe --- /dev/null +++ b/lib/unit-defs/area.rb @@ -0,0 +1,24 @@ +physical_quantity :Area do + symbol "A" + latex "A" + description "extent of a two-dimensional figure or shape" +end + +unit :Feet2 do + symbol "ft2" + latex "ft2" + description "feet square" + quantity :Area + range 0..1e7 +end + +unit :Inch2 do + symbol "in2" + latex "in2" + description "inch square" + quantity :Area + range 0..12*1e7 +end + +conversion :Feet2, :Inch2 do |a| "144*#{a}" end +conversion :Inch2, :Feet2 do |a| "#{a}/144" end diff --git a/lib/unit-defs/capacitance.rb b/lib/unit-defs/capacitance.rb new file mode 100644 index 0000000..db3721e --- /dev/null +++ b/lib/unit-defs/capacitance.rb @@ -0,0 +1,75 @@ +physical_quantity :Capacitance do + symbol "Ce" + latex "C" + description "Ability of a system to store electric charge" +end + +unit :Farad do + symbol "F" + latex "F" + description "SI unit of capacitance" + quantity :Capacitance + range 0..1e3 +end + +unit :MilliFarad do + symbol "mF" + latex "mF" + description "One thousandth of a farad" + quantity :Capacitance + range 0..1e6 +end + +unit :MicroFarad do + symbol "uF" + latex "\\mu F" + description "One millionth of a farad" + quantity :Capacitance + range 0..1e9 +end + +unit :NanoFarad do + symbol "nF" + latex "nF" + description "One billionth of a farad" + quantity :Capacitance + range 0..1e12 +end + +unit :PicoFarad do + symbol "pF" + latex "pF" + description "One trillionth of a farad" + quantity :Capacitance + range 0..1e15 +end + +# To Farad +conversion :MilliFarad, :Farad do |v| "1e-3 * #{v}" end +conversion :MicroFarad, :Farad do |v| "1e-6 * #{v}" end +conversion :NanoFarad, :Farad do |v| "1e-9 * #{v}" end +conversion :PicoFarad, :Farad do |v| "1e-12 * #{v}" end + +# To MilliFarad +conversion :Farad, :MilliFarad do |v| "1e3 * #{v}" end +conversion :MicroFarad, :MilliFarad do |v| "1e-3 * #{v}" end +conversion :NanoFarad, :MilliFarad do |v| "1e-6 * #{v}" end +conversion :PicoFarad, :MilliFarad do |v| "1e-9 * #{v}" end + +# To MicroFarad +conversion :Farad, :MicroFarad do |v| "1e6 * #{v}" end +conversion :MilliFarad, :MicroFarad do |v| "1e3 * #{v}" end +conversion :NanoFarad, :MicroFarad do |v| "1e-3 * #{v}" end +conversion :PicoFarad, :MicroFarad do |v| "1e-6 * #{v}" end + +# To NanoFarad +conversion :Farad, :NanoFarad do |v| "1e9 * #{v}" end +conversion :MilliFarad, :NanoFarad do |v| "1e6 * #{v}" end +conversion :MicroFarad, :NanoFarad do |v| "1e3 * #{v}" end +conversion :PicoFarad, :NanoFarad do |v| "1e-3 * #{v}" end + +# To PicoFarad +conversion :Farad, :PicoFarad do |v| "1e12 * #{v}" end +conversion :MilliFarad, :PicoFarad do |v| "1e9 * #{v}" end +conversion :MicroFarad, :PicoFarad do |v| "1e6 * #{v}" end +conversion :NanoFarad, :PicoFarad do |v| "1e3 * #{v}" end diff --git a/lib/unit-defs/charge.rb b/lib/unit-defs/charge.rb new file mode 100644 index 0000000..648f237 --- /dev/null +++ b/lib/unit-defs/charge.rb @@ -0,0 +1,41 @@ +physical_quantity :Charge do + symbol "Qe" + latex "Q" + description "Electric charge transported by current over time" +end + +unit :Coulomb do + symbol "C" + latex "C" + description "SI unit of electric charge" + quantity :Charge + range 0..1e10 +end + +unit :AmpereHour do + symbol "Ah" + latex "Ah" + description "Charge transported by one ampere in one hour" + quantity :Charge + range 0..2.777777777777778e6 +end + +unit :MilliAmpereHour do + symbol "mAh" + latex "mAh" + description "One thousandth of an ampere-hour" + quantity :Charge + range 0..2.777777777777778e9 +end + +# To Coulomb +conversion :AmpereHour, :Coulomb do |v| "3600 * #{v}" end +conversion :MilliAmpereHour, :Coulomb do |v| "3.6 * #{v}" end + +# To AmpereHour +conversion :Coulomb, :AmpereHour do |v| "#{v} / 3600" end +conversion :MilliAmpereHour, :AmpereHour do |v| "1e-3 * #{v}" end + +# To MilliAmpereHour +conversion :Coulomb, :MilliAmpereHour do |v| "#{v} / 3.6" end +conversion :AmpereHour, :MilliAmpereHour do |v| "1000 * #{v}" end diff --git a/lib/unit-defs/current.rb b/lib/unit-defs/current.rb new file mode 100644 index 0000000..dae4df1 --- /dev/null +++ b/lib/unit-defs/current.rb @@ -0,0 +1,13 @@ +physical_quantity :Current do + symbol "I" + latex "I" + description "Flow of electrical charge" +end + +unit :Ampere do + symbol "amp" + latex "amp" + description "One coulomb per second" + quantity :Current + range 0..1000 +end diff --git a/lib/unit-defs/density.rb b/lib/unit-defs/density.rb new file mode 100644 index 0000000..7fa12b0 --- /dev/null +++ b/lib/unit-defs/density.rb @@ -0,0 +1,117 @@ +physical_quantity :Density do + symbol "rho" + latex "\\rho" + description "The density of a substance is its mass per unit volume" +end + +unit :Gr_cm3 do + symbol "gr/cm3" + latex "g/cm^{3}" + description "unit of density declared as gr (mass in gram) per cm3 (unit volume in cubic centimeter)" + quantity :Density + range 0.0001..3.30 +end + +unit :Kg_m3 do + symbol "kg/m3" + latex "kg/m^{3}" + description "unit of density declared as kg (mass in kilogram) per m3 (unit volume in cubic meter)" + quantity :Density + range 0.1..3300 +end + +unit :Kg_L do + symbol "kg/L" + latex "kg/L" + description "unit of density declared as kg (mass in kilogram ) per L (unit volume in liter)" + quantity :Density + range 0.0001..3.30 +end + +unit :Lb_ft3 do + symbol "lb/ft3" + latex "lb/ft^{3}" + description "unit of density declared as lb (mass in pound ) per ft3 (unit volume in cubic feet)" + quantity :Density + range 0.006242782..206.0117994 +end + +unit :Lb_Gal do + symbol "lb/gal" + latex "lb/gal" + description "unit of density declared as lb (mass in pound ) per Gal (unit volume in Gallons)" + quantity :Density + range 0.000834538..27.5397507 +end + +unit :Lb_Inch3 do + symbol "lb/inch3" + latex "lb/in^{3}" + description "unit of density declared as lb (mass in pound ) per inch3 (unit volume in cubic inches)" + quantity :Density + range 0.000003613..0.119219793 +end + +unit :Sg do + symbol "sg" + latex "water=1\\ at\\ 60\\ \\degree{F}" + description "Specific gravity is the ratio of the density of a substance to the density of a reference substance water" + quantity :Density + range 0.00010009849692097025..3.303250398392018 +end + +# To Gr_cm3 +conversion :Kg_m3, :Gr_cm3 do |v| "0.001 * #{v}" end +conversion :Kg_L, :Gr_cm3 do |v| "#{v}" end +conversion :Lb_ft3, :Gr_cm3 do |v| "0.01601846337396014 * #{v}" end +conversion :Sg, :Gr_cm3 do |v| "0.999016 * #{v}" end +conversion :Lb_Gal, :Gr_cm3 do |v| "0.119826793 * #{v}" end +conversion :Lb_Inch3, :Gr_cm3 do |v| "27.679967537 * #{v}" end + +# To Kg_m3 +conversion :Gr_cm3, :Kg_m3 do |v| "1000 * #{v}" end +conversion :Kg_L, :Kg_m3 do |v| "1000 * #{v}" end +conversion :Lb_ft3, :Kg_m3 do |v| "16.018463374 * #{v}" end +conversion :Sg, :Kg_m3 do |v| "999.016 * #{v}" end +conversion :Lb_Gal, :Kg_m3 do |v| "119.826792768 * #{v}" end +conversion :Lb_Inch3, :Kg_m3 do |v| "27679.898580851597 * #{v}" end + +# To Kg_L +conversion :Gr_cm3, :Kg_L do |v| "#{v}" end +conversion :Kg_m3, :Kg_L do |v| "0.001 * #{v}" end +conversion :Lb_ft3, :Kg_L do |v| "0.01601846337396014 * #{v}" end +conversion :Sg, :Kg_L do |v| "0.999016 * #{v}" end +conversion :Lb_Gal, :Kg_L do |v| "0.119826792768 * #{v}" end +conversion :Lb_Inch3, :Kg_L do |v| "27.679967537 * #{v}" end + +# To Sg +conversion :Gr_cm3, :Sg do |v| "#{v} / 0.999016" end +conversion :Kg_m3, :Sg do |v| "#{v} / 999.016" end +conversion :Lb_ft3, :Sg do |v| "#{v} / 62.366389027" end +conversion :Kg_L, :Sg do |v| "#{v} / 0.999016" end +conversion :Lb_Gal, :Sg do |v| "#{v} / 8.337167147" end +conversion :Lb_Inch3, :Sg do |v| "#{v} / 0.036091661" end + +# To Lb_ft3 +conversion :Gr_cm3, :Lb_ft3 do |v| "62.4279605761446 * #{v}" end +conversion :Kg_m3, :Lb_ft3 do |v| "0.0624279606 * #{v}" end +conversion :Kg_L, :Lb_ft3 do |v| "62.4279605761446 * #{v}" end +conversion :Sg, :Lb_ft3 do |v| "62.366389027 * #{v}" end +conversion :Lb_Gal, :Lb_ft3 do |v| "7.480525210 * #{v}" end +conversion :Lb_Inch3, :Lb_ft3 do |v| "1727.999975642 * #{v}" end + +# To Lb_Gal +conversion :Kg_m3, :Lb_Gal do |v| "0.0083453790 * #{v}" end +conversion :Gr_cm3, :Lb_Gal do |v| "8.3453790000 * #{v}" end +conversion :Kg_L, :Lb_Gal do |v| "8.3453790000 * #{v}" end +conversion :Lb_ft3, :Lb_Gal do |v| "0.1336804532 * #{v}" end +conversion :Sg, :Lb_Gal do |v| "8.3371671471 * #{v}" end +conversion :Lb_Inch3, :Lb_Gal do |v| "230.9998198034 * #{v}" end + +# To Lb_Inch3 +conversion :Kg_m3, :Lb_Inch3 do |v| "0.0000361273 * #{v}" end +conversion :Gr_cm3, :Lb_Inch3 do |v| "0.0361272100 * #{v}" end +conversion :Kg_L, :Lb_Inch3 do |v| "0.0361272100 * #{v}" end +conversion :Lb_ft3, :Lb_Inch3 do |v| "0.0005787037 * #{v}" end +conversion :Sg, :Lb_Inch3 do |v| "0.0360910828 * #{v}" end +conversion :Lb_Gal, :Lb_Inch3 do |v| "0.0043290077 * #{v}" end diff --git a/lib/unit-defs/diameter.rb b/lib/unit-defs/diameter.rb new file mode 100644 index 0000000..15f5282 --- /dev/null +++ b/lib/unit-defs/diameter.rb @@ -0,0 +1,75 @@ +physical_quantity :Diameter do + symbol "D" + latex "D" + description "The diameter is the measurement of a straight line passing from side to side through the center of a body or figure, especially a circle. It is used to designate the pipe size. The diameter is denoted as D." +end + +unit :Foot_D do + symbol "ft_d" + latex "ft" + description "The diameter measured in feet" + quantity :Diameter + range 8.3333333333333333e-2..25 +end + +unit :Inch_D do + symbol "in_d" + latex "in" + description "The diameter measured in inches" + quantity :Diameter + range 1..300 +end + +unit :Meter_D do + symbol "m_d" + latex "m" + description "The diameter measured in meters" + quantity :Diameter + range 0.0254..7.62 +end + +unit :Centimeter_D do + symbol "cm" + latex "cm" + description "The diameter measured in centimeters" + quantity :Diameter + range 2.54..762 +end + +unit :Millimeter_D do + symbol "mm" + latex "mm" + description "The diameter measured in millimeters" + quantity :Diameter + range 25.4..7620 +end + +# To Foot_D +conversion :Inch_D, :Foot_D do |v| "8.3333333333333333e-2 * #{v}" end +conversion :Meter_D, :Foot_D do |v| "3.28083989501 * #{v}" end +conversion :Centimeter_D, :Foot_D do |v| "3.28083989501e-2 * #{v}" end +conversion :Millimeter_D, :Foot_D do |v| "3.28083989501e-3 * #{v}" end + +# To Inch_D +conversion :Foot_D, :Inch_D do |v| "12 * #{v}" end +conversion :Meter_D, :Inch_D do |v| "39.3700787402 * #{v}" end +conversion :Centimeter_D, :Inch_D do |v| "39.3700787402e-2 * #{v}" end +conversion :Millimeter_D, :Inch_D do |v| "39.3700787402e-3 * #{v}" end + +# To Meter_D +conversion :Foot_D, :Meter_D do |v| "0.3048 * #{v}" end +conversion :Inch_D, :Meter_D do |v| "0.0254 * #{v}" end +conversion :Centimeter_D, :Meter_D do |v| "1e-2 * #{v}" end +conversion :Millimeter_D, :Meter_D do |v| "1e-3 * #{v}" end + +# To Centimeter_D +conversion :Foot_D, :Centimeter_D do |v| "30.48 * #{v}" end +conversion :Inch_D, :Centimeter_D do |v| "2.54 * #{v}" end +conversion :Meter_D, :Centimeter_D do |v| "1e2 * #{v}" end +conversion :Millimeter_D, :Centimeter_D do |v| "0.1 * #{v}" end + +# To Millimeter_D +conversion :Foot_D, :Millimeter_D do |v| "304.8 * #{v}" end +conversion :Inch_D, :Millimeter_D do |v| "25.4 * #{v}" end +conversion :Meter_D, :Millimeter_D do |v| "1e3 * #{v}" end +conversion :Centimeter_D, :Millimeter_D do |v| "10 * #{v}" end diff --git a/lib/unit-defs/dimensionless.rb b/lib/unit-defs/dimensionless.rb new file mode 100644 index 0000000..a0d36ae --- /dev/null +++ b/lib/unit-defs/dimensionless.rb @@ -0,0 +1,13 @@ +physical_quantity :Dimension_Less do + symbol "DimensionLess" + latex "Dmless" + description "TODO" +end + +unit :Dim_Less do + symbol "dmless" + latex "dmless" + description "TODO" + quantity :Dimension_Less + range 1e-7..1e7 +end diff --git a/lib/unit-defs/dynamic_viscosity.rb b/lib/unit-defs/dynamic_viscosity.rb new file mode 100644 index 0000000..8c6703e --- /dev/null +++ b/lib/unit-defs/dynamic_viscosity.rb @@ -0,0 +1,57 @@ +physical_quantity :DynamicViscosity do + symbol "mu" + latex "\\mu" + description "Resistance of a fluid to shear or flow" +end + +unit :PascalSecond do + symbol "Pa*s" + latex "Pa\\cdot s" + description "SI unit of dynamic viscosity" + quantity :DynamicViscosity + range 0..1e6 +end + +unit :Centipoise do + symbol "cP" + latex "cP" + description "Centipoise, equal to 1 mPa*s" + quantity :DynamicViscosity + range 0..1e9 +end + +unit :Poise do + symbol "P" + latex "P" + description "Poise, CGS unit of dynamic viscosity" + quantity :DynamicViscosity + range 0..1e7 +end + +unit :Lb_Ft_S do + symbol "lb/ft*s" + latex "lb/(ft\\cdot s)" + description "Pound mass per foot-second" + quantity :DynamicViscosity + range 0..671968.9751393054 +end + +# To PascalSecond +conversion :Centipoise, :PascalSecond do |v| "1e-3 * #{v}" end +conversion :Poise, :PascalSecond do |v| "0.1 * #{v}" end +conversion :Lb_Ft_S, :PascalSecond do |v| "1.48816394357 * #{v}" end + +# To Centipoise +conversion :PascalSecond, :Centipoise do |v| "1000 * #{v}" end +conversion :Poise, :Centipoise do |v| "100 * #{v}" end +conversion :Lb_Ft_S, :Centipoise do |v| "1488.16394357 * #{v}" end + +# To Poise +conversion :PascalSecond, :Poise do |v| "10 * #{v}" end +conversion :Centipoise, :Poise do |v| "1e-2 * #{v}" end +conversion :Lb_Ft_S, :Poise do |v| "14.8816394357 * #{v}" end + +# To Lb_Ft_S +conversion :PascalSecond, :Lb_Ft_S do |v| "0.6719689751393054 * #{v}" end +conversion :Centipoise, :Lb_Ft_S do |v| "6.719689751393054e-4 * #{v}" end +conversion :Poise, :Lb_Ft_S do |v| "6.719689751393054e-2 * #{v}" end diff --git a/lib/unit-defs/energy.rb b/lib/unit-defs/energy.rb new file mode 100644 index 0000000..721d5f9 --- /dev/null +++ b/lib/unit-defs/energy.rb @@ -0,0 +1,57 @@ +physical_quantity :Energy do + symbol "E" + latex "E" + description "Capacity to do work" +end + +unit :Joule do + symbol "J" + latex "J" + description "SI unit of energy" + quantity :Energy + range 0..1e12 +end + +unit :KiloJoule do + symbol "kJ" + latex "kJ" + description "One thousand joules" + quantity :Energy + range 0..1e9 +end + +unit :WattHour do + symbol "Wh" + latex "Wh" + description "Energy of one watt sustained for one hour" + quantity :Energy + range 0..2.7777777777777778e8 +end + +unit :BTU do + symbol "BTU" + latex "BTU" + description "British thermal unit" + quantity :Energy + range 0..9.478171203132992e8 +end + +# To Joule +conversion :KiloJoule, :Joule do |v| "1000 * #{v}" end +conversion :WattHour, :Joule do |v| "3600 * #{v}" end +conversion :BTU, :Joule do |v| "1055.05585262 * #{v}" end + +# To KiloJoule +conversion :Joule, :KiloJoule do |v| "1e-3 * #{v}" end +conversion :WattHour, :KiloJoule do |v| "3.6 * #{v}" end +conversion :BTU, :KiloJoule do |v| "1.05505585262 * #{v}" end + +# To WattHour +conversion :Joule, :WattHour do |v| "2.7777777777777778e-4 * #{v}" end +conversion :KiloJoule, :WattHour do |v| "0.2777777777777778 * #{v}" end +conversion :BTU, :WattHour do |v| "0.2930710701722222 * #{v}" end + +# To BTU +conversion :Joule, :BTU do |v| "9.478171203132992e-4 * #{v}" end +conversion :KiloJoule, :BTU do |v| "0.9478171203132992 * #{v}" end +conversion :WattHour, :BTU do |v| "3.412141633127942 * #{v}" end diff --git a/lib/unit-defs/flowrate.rb b/lib/unit-defs/flowrate.rb new file mode 100644 index 0000000..ac01d2e --- /dev/null +++ b/lib/unit-defs/flowrate.rb @@ -0,0 +1,57 @@ +physical_quantity :FlowRate do + symbol "Q" + latex "Q" + description "Volume rate of some fluid flow that is transported through a given cross-sectional area" +end + +unit :BPD do + symbol "bpd" + latex "bpd" + description "Volume rate measured in barrels per day" + quantity :FlowRate + range 0..1e10 +end + +unit :GPM do + symbol "gpm" + latex "gpm" + description "Volume rate measured in U.S. Gallon Per Minute" + quantity :FlowRate + range 0..291666666.667 +end + +unit :CMD do + symbol "cmd" + latex "cmd" + description "Volume rate measured in cubic meter per day" + quantity :FlowRate + range 0..1589872949.28 +end + +unit :CMS do + symbol "cms" + latex "cms" + description "Volume rate measured in cubic meter per second" + quantity :FlowRate + range 0..18401.3072833 +end + +# To BPD +conversion :GPM, :BPD do |v| "34.2857142857 * #{v}" end +conversion :CMD, :BPD do |v| "6.28981077043 * #{v}" end +conversion :CMS, :BPD do |v| "543439.650565 * #{v}" end + +# To GPM +conversion :BPD, :GPM do |v| "2.91666666667e-2 * #{v}" end +conversion :CMD, :GPM do |v| "0.183452814138 * #{v}" end +conversion :CMS, :GPM do |v| "15850.3231415 * #{v}" end + +# To CMD +conversion :BPD, :CMD do |v| "0.158987294928 * #{v}" end +conversion :GPM, :CMD do |v| "5.45099296896 * #{v}" end +conversion :CMS, :CMD do |v| "86400 * #{v}" end + +# To CMS +conversion :BPD, :CMS do |v| "1.84013072833e-6 * #{v}" end +conversion :CMD, :CMS do |v| "1.15740740741e-5 * #{v}" end +conversion :GPM, :CMS do |v| "0.0000630901964 * #{v}" end diff --git a/lib/unit-defs/force.rb b/lib/unit-defs/force.rb new file mode 100644 index 0000000..2dff6c9 --- /dev/null +++ b/lib/unit-defs/force.rb @@ -0,0 +1,57 @@ +physical_quantity :Force do + symbol "F" + latex "F" + description "Interaction that changes the motion of an object" +end + +unit :Newton do + symbol "N" + latex "N" + description "SI unit of force" + quantity :Force + range 0..1e9 +end + +unit :kNewton do + symbol "kN" + latex "kN" + description "One thousand newtons" + quantity :Force + range 0..1e6 +end + +unit :PoundForce do + symbol "lbf" + latex "lbf" + description "Imperial unit of force" + quantity :Force + range 0..2.2480894387096265e8 +end + +unit :Dyne do + symbol "dyn" + latex "dyn" + description "CGS unit of force (1 dyn = 1e-5 N)" + quantity :Force + range 0..1e14 +end + +# To Newton +conversion :kNewton, :Newton do |v| "1000 * #{v}" end +conversion :PoundForce, :Newton do |v| "4.4482216152605 * #{v}" end +conversion :Dyne, :Newton do |v| "1e-5 * #{v}" end + +# To kNewton +conversion :Newton, :kNewton do |v| "1e-3 * #{v}" end +conversion :PoundForce, :kNewton do |v| "4.4482216152605e-3 * #{v}" end +conversion :Dyne, :kNewton do |v| "1e-8 * #{v}" end + +# To PoundForce +conversion :Newton, :PoundForce do |v| "0.22480894387096263 * #{v}" end +conversion :kNewton, :PoundForce do |v| "224.80894387096263 * #{v}" end +conversion :Dyne, :PoundForce do |v| "2.2480894387096265e-6 * #{v}" end + +# To Dyne +conversion :Newton, :Dyne do |v| "1e5 * #{v}" end +conversion :kNewton, :Dyne do |v| "1e8 * #{v}" end +conversion :PoundForce, :Dyne do |v| "444822.16152605 * #{v}" end diff --git a/lib/unit-defs/frequency.rb b/lib/unit-defs/frequency.rb new file mode 100644 index 0000000..9f1fd64 --- /dev/null +++ b/lib/unit-defs/frequency.rb @@ -0,0 +1,27 @@ +physical_quantity :Frequency do + symbol "f" + latex "\\nu" + description "Number of occurrences of a repeating event per unit time" +end + +unit :Hertz do + symbol "Hz" + latex "Hz" + description "Unit of frequency in the International System of Units (SI) and is defined as one cycle per second" + quantity :Frequency + range 0..120 +end + +unit :Revolution_per_minute do + symbol "RPM" + latex "RPM" + description "Unit of frequency and is defined as revolution (cycle) per minute" + quantity :Frequency + range 0..7200 +end + +# To Revolution_per_minute +conversion :Hertz, :Revolution_per_minute do |v| "60 * #{v}" end + +# To Hertz +conversion :Revolution_per_minute, :Hertz do |v| "1.66666666666666667e-2 * #{v}" end diff --git a/lib/unit-defs/kinematic_viscosity.rb b/lib/unit-defs/kinematic_viscosity.rb new file mode 100644 index 0000000..d9ad475 --- /dev/null +++ b/lib/unit-defs/kinematic_viscosity.rb @@ -0,0 +1,57 @@ +physical_quantity :KinematicViscosity do + symbol "nu" + latex "\\nu" + description "Ratio of dynamic viscosity to density" +end + +unit :Meter2_S do + symbol "m2/s" + latex "m^{2}/s" + description "SI unit of kinematic viscosity" + quantity :KinematicViscosity + range 0..1e4 +end + +unit :CentiStokes do + symbol "cSt" + latex "cSt" + description "Centistokes, equal to 1e-6 m2/s" + quantity :KinematicViscosity + range 0..1e10 +end + +unit :Stokes do + symbol "St" + latex "St" + description "Stokes, equal to 1e-4 m2/s" + quantity :KinematicViscosity + range 0..1e8 +end + +unit :Feet2_S do + symbol "ft2/s" + latex "ft^{2}/s" + description "Square feet per second" + quantity :KinematicViscosity + range 0..107639.10416709722 +end + +# To Meter2_S +conversion :CentiStokes, :Meter2_S do |v| "1e-6 * #{v}" end +conversion :Stokes, :Meter2_S do |v| "1e-4 * #{v}" end +conversion :Feet2_S, :Meter2_S do |v| "9.290304e-2 * #{v}" end + +# To CentiStokes +conversion :Meter2_S, :CentiStokes do |v| "1e6 * #{v}" end +conversion :Stokes, :CentiStokes do |v| "100 * #{v}" end +conversion :Feet2_S, :CentiStokes do |v| "92903.04 * #{v}" end + +# To Stokes +conversion :Meter2_S, :Stokes do |v| "1e4 * #{v}" end +conversion :CentiStokes, :Stokes do |v| "1e-2 * #{v}" end +conversion :Feet2_S, :Stokes do |v| "929.0304 * #{v}" end + +# To Feet2_S +conversion :Meter2_S, :Feet2_S do |v| "10.763910416709722 * #{v}" end +conversion :CentiStokes, :Feet2_S do |v| "1.0763910416709721e-5 * #{v}" end +conversion :Stokes, :Feet2_S do |v| "1.0763910416709721e-3 * #{v}" end diff --git a/lib/unit-defs/length.rb b/lib/unit-defs/length.rb new file mode 100644 index 0000000..86f2234 --- /dev/null +++ b/lib/unit-defs/length.rb @@ -0,0 +1,61 @@ +# Physical Quantities +physical_quantity :Length do + symbol "L" + latex "L" + description "The length is the measurement or extent of something. It may represent reservoir thickness, drainage radius, well radius, etc." +end + +# Units +unit :foot do + symbol "ft" + latex "ft" + description "The length measured in feet" + quantity :Length + range 0..98425.1968503937 +end + +unit :inch do + symbol "in" + latex "in" + description "The length measured in inches" + quantity :Length + range 0..1181102.36220472 +end + +unit :meter do + symbol "m" + latex "m" + description "The length measured in meters" + quantity :Length + range 0..30000 +end + +unit :yard do + symbol "yd" + latex "yd" + description "The length measured in yards" + quantity :Length + range 0..32808.3989501312 +end + +# Conversions + +# To foot +conversion :inch, :foot do |v| "8.3333333333333333e-2 * #{v}" end +conversion :meter, :foot do |v| "3.28083989501 * #{v}" end +conversion :yard, :foot do |v| "3 * #{v}" end + +# To inch +conversion :foot, :inch do |v| "12 * #{v}" end +conversion :meter, :inch do |v| "39.3700787402 * #{v}" end +conversion :yard, :inch do |v| "36 * #{v}" end + +# To meter +conversion :foot, :meter do |v| "0.3048 * #{v}" end +conversion :inch, :meter do |v| "0.0254 * #{v}" end +conversion :yard, :meter do |v| "0.9144 * #{v}" end + +# To yard +conversion :foot, :yard do |v| "0.33333333333333337 * #{v}" end +conversion :inch, :yard do |v| "0.027777777777777776 * #{v}" end +conversion :meter, :yard do |v| "1.0936132983377078 * #{v}" end diff --git a/lib/unit-defs/mass.rb b/lib/unit-defs/mass.rb new file mode 100644 index 0000000..12b6250 --- /dev/null +++ b/lib/unit-defs/mass.rb @@ -0,0 +1,57 @@ +physical_quantity :Mass do + symbol "M" + latex "M" + description "Amount of matter in a body" +end + +unit :Kilogram do + symbol "kg" + latex "kg" + description "SI base unit of mass" + quantity :Mass + range 0..1e8 +end + +unit :Gram do + symbol "g" + latex "g" + description "One thousandth of a kilogram" + quantity :Mass + range 0..1e11 +end + +unit :PoundMass do + symbol "lbm" + latex "lbm" + description "Avoirdupois pound as mass unit" + quantity :Mass + range 0..2.2046226218487757e8 +end + +unit :MetricTon do + symbol "t" + latex "t" + description "Metric ton (tonne), equal to 1000 kilograms" + quantity :Mass + range 0..1e5 +end + +# To Kilogram +conversion :Gram, :Kilogram do |v| "1e-3 * #{v}" end +conversion :PoundMass, :Kilogram do |v| "0.45359237 * #{v}" end +conversion :MetricTon, :Kilogram do |v| "1000 * #{v}" end + +# To Gram +conversion :Kilogram, :Gram do |v| "1000 * #{v}" end +conversion :PoundMass, :Gram do |v| "453.59237 * #{v}" end +conversion :MetricTon, :Gram do |v| "1e6 * #{v}" end + +# To PoundMass +conversion :Kilogram, :PoundMass do |v| "2.2046226218487757 * #{v}" end +conversion :Gram, :PoundMass do |v| "2.2046226218487757e-3 * #{v}" end +conversion :MetricTon, :PoundMass do |v| "2204.6226218487757 * #{v}" end + +# To MetricTon +conversion :Kilogram, :MetricTon do |v| "1e-3 * #{v}" end +conversion :Gram, :MetricTon do |v| "1e-6 * #{v}" end +conversion :PoundMass, :MetricTon do |v| "4.5359237e-4 * #{v}" end diff --git a/lib/unit-defs/massflow.rb b/lib/unit-defs/massflow.rb new file mode 100644 index 0000000..e4afe34 --- /dev/null +++ b/lib/unit-defs/massflow.rb @@ -0,0 +1,75 @@ +physical_quantity :MassFlow do + symbol "m_dot" + latex "\\dot{m}" + description "Mass transported per unit time" +end + +unit :Kg_S do + symbol "kg/s" + latex "kg/s" + description "Mass flow in kilograms per second" + quantity :MassFlow + range 0..1e7 +end + +unit :Kg_H do + symbol "kg/h" + latex "kg/h" + description "Mass flow in kilograms per hour" + quantity :MassFlow + range 0..3.6e10 +end + +unit :Lb_S do + symbol "lb/s" + latex "lb/s" + description "Mass flow in pounds mass per second" + quantity :MassFlow + range 0..2.2046226218487757e7 +end + +unit :Lb_H do + symbol "lb/h" + latex "lb/h" + description "Mass flow in pounds mass per hour" + quantity :MassFlow + range 0..7.936641438655593e10 +end + +unit :Tonne_Day do + symbol "t/d" + latex "t/d" + description "Mass flow in metric tonnes per day" + quantity :MassFlow + range 0..8.64e8 +end + +# To Kg_S +conversion :Kg_H, :Kg_S do |v| "2.777777777777778e-4 * #{v}" end +conversion :Lb_S, :Kg_S do |v| "0.45359237 * #{v}" end +conversion :Lb_H, :Kg_S do |v| "1.2599788055555556e-4 * #{v}" end +conversion :Tonne_Day, :Kg_S do |v| "1.1574074074074073e-2 * #{v}" end + +# To Kg_H +conversion :Kg_S, :Kg_H do |v| "3600 * #{v}" end +conversion :Lb_S, :Kg_H do |v| "1632.932532 * #{v}" end +conversion :Lb_H, :Kg_H do |v| "0.45359237 * #{v}" end +conversion :Tonne_Day, :Kg_H do |v| "41.666666666666664 * #{v}" end + +# To Lb_S +conversion :Kg_S, :Lb_S do |v| "2.2046226218487757 * #{v}" end +conversion :Kg_H, :Lb_S do |v| "6.123951727357711e-4 * #{v}" end +conversion :Lb_H, :Lb_S do |v| "2.777777777777778e-4 * #{v}" end +conversion :Tonne_Day, :Lb_S do |v| "2.5516465530657124e-2 * #{v}" end + +# To Lb_H +conversion :Kg_S, :Lb_H do |v| "7936.641438655593 * #{v}" end +conversion :Kg_H, :Lb_H do |v| "2.2046226218487757 * #{v}" end +conversion :Lb_S, :Lb_H do |v| "3600 * #{v}" end +conversion :Tonne_Day, :Lb_H do |v| "91.85927591036565 * #{v}" end + +# To Tonne_Day +conversion :Kg_S, :Tonne_Day do |v| "86.4 * #{v}" end +conversion :Kg_H, :Tonne_Day do |v| "2.4e-2 * #{v}" end +conversion :Lb_S, :Tonne_Day do |v| "39.190380768000004 * #{v}" end +conversion :Lb_H, :Tonne_Day do |v| "1.088621688e-2 * #{v}" end diff --git a/lib/unit-defs/power.rb b/lib/unit-defs/power.rb new file mode 100644 index 0000000..fee49a6 --- /dev/null +++ b/lib/unit-defs/power.rb @@ -0,0 +1,27 @@ +physical_quantity :Power do + symbol "Pow" + latex "P" + description "power is the rate of doing work. It is the amount of energy consumed per unit time" +end + +unit :HorsePower do + symbol "hp" + latex "hp" + description "Unit of measurement of power hp" + quantity :Power + range 0..2000 +end + +unit :Watt do + symbol "W" + latex "W" + description "Unit of measurement of power watt" + quantity :Power + range 0..1491399.74316 +end + +# To HorsePower +conversion :Watt, :HorsePower do |v| "1.3410220896e-3 * #{v}" end + +# To Watt +conversion :HorsePower, :Watt do |v| "745.699871582 * #{v}" end diff --git a/lib/unit-defs/pressure.rb b/lib/unit-defs/pressure.rb new file mode 100644 index 0000000..669c297 --- /dev/null +++ b/lib/unit-defs/pressure.rb @@ -0,0 +1,117 @@ +physical_quantity :Pressure do + symbol "P" + latex "P" + description "Pressure is the force applied perpendicular to the surface of an object per unit area over which that force is distributed" +end + +unit :Bar do + symbol "bar" + latex "bar" + description "A bar is a metric measurement unit of pressure. One bar is equivalent to ten newtons (N) per square centimeter (cm2)" + quantity :Pressure + range 0..2068.43 +end + +unit :Pascal do + symbol "Pa" + latex "Pa" + description "The pascal is the SI derived unit of pressure used to quantify internal pressure, stress, Young's modulus and ultimate tensile strength. It is defined as one newton per square metre." + quantity :Pressure + range 0..206843405.31 +end + +unit :kPascal do + symbol "kPa" + latex "kPa" + description "a kPascal is a common multiple unit of the pascal 1 kPa = 1000 Pa," + quantity :Pressure + range 0..206843.41 +end + +unit :mPascal do + symbol "MPa" + latex "MPa" + description "a MPascal is a common multiple unit of the pascal 1 MPa = 1000000 Pa," + quantity :Pressure + range 0..206.843410 +end + +unit :psia do + symbol "psia" + latex "psia" + description "Pounds per square inch absolute (psia) is used to make it clear that the pressure is relative to a vacuum rather than the ambient atmospheric pressure. Since atmospheric pressure at sea level is around 14.7 psi" + quantity :Pressure + range 0..29985.3 +end + +unit :psig do + symbol "psig" + latex "psig" + description "Pound-force per square inch is a unit of pressure or of stress based on avoirdupois units. It is the pressure resulting from a force of one pound-force applied to an area of one square inch" + quantity :Pressure + range -14.69594877551..30000 +end + +unit :Atmosphere do + symbol "atm" + latex "atm" + description "Pound-force per square inch is a unit of pressure or of stress based on avoirdupois units. It is the pressure resulting from a force of one pound-force applied to an area of one square inch" + quantity :Pressure + range 0..2041.39 +end + +# To Pascal +conversion :Atmosphere, :Pascal do |v| "#{v} * 101324.99658" end +conversion :Bar, :Pascal do |v| "#{v} * 100000.0" end +conversion :psia, :Pascal do |v| "#{v} * 6894.757293178308" end +conversion :psig, :Pascal do |v| "(#{v} + 14.695948775) * 6894.757293178308" end +conversion :kPascal, :Pascal do |v| "#{v} * 1e3" end +conversion :mPascal, :Pascal do |v| "#{v} * 1e6" end + +# To Atmosphere +conversion :Bar, :Atmosphere do |v| "#{v} * 0.986923267" end +conversion :Pascal, :Atmosphere do |v| "#{v} * 9.86923267e-6" end +conversion :kPascal, :Atmosphere do |v| "#{v} * 9.86923267e-3" end +conversion :psia, :Atmosphere do |v| "#{v} / 14.69594877551" end +conversion :psig, :Atmosphere do |v| "(#{v} + 14.69594877551) / 14.69594877551" end +conversion :mPascal, :Atmosphere do |v| "unit_convert(unit_convert(#{v}))" end + +# To Bar +conversion :Atmosphere, :Bar do |v| "#{v} * 1.0132499658" end +conversion :Pascal, :Bar do |v| "#{v} * 1.0e-5" end +conversion :kPascal, :Bar do |v| "#{v} * 1.0e-2" end +conversion :psia, :Bar do |v| "#{v} * 0.0689475729" end +conversion :psig, :Bar do |v| "(#{v} + 14.695948775) * 0.0689475729" end +conversion :mPascal, :Bar do |v| "unit_convert(unit_convert(#{v}))" end + +# To kPascal +conversion :Atmosphere, :kPascal do |v| "#{v} * 101.32499658" end +conversion :Bar, :kPascal do |v| "#{v} * 100.0000" end +conversion :psia, :kPascal do |v| "#{v} * 6.894757293178308" end +conversion :psig, :kPascal do |v| "(#{v} + 14.695948775) * 6.894757293178308" end +conversion :Pascal, :kPascal do |v| "#{v} / 1000" end +conversion :mPascal, :kPascal do |v| "#{v} * 1000" end + +# To mPascal +conversion :Atmosphere, :mPascal do |v| "#{v} * 0.10132499658" end +conversion :Bar, :mPascal do |v| "#{v} * 0.1" end +conversion :psia, :mPascal do |v| "#{v} * 6.894757293178308e-3" end +conversion :psig, :mPascal do |v| "((#{v} + 14.695948775) * 6.894757293178308) / 1000" end +conversion :Pascal, :mPascal do |v| "#{v} / 1e6" end +conversion :kPascal, :mPascal do |v| "#{v} / 1e3" end + +# To psia +conversion :Atmosphere, :psia do |v| "#{v} * 14.695948775" end +conversion :Bar, :psia do |v| "#{v} * 14.503773773" end +conversion :Pascal, :psia do |v| "#{v} * 1.4503773773e-4" end +conversion :kPascal, :psia do |v| "#{v} * 1.4503773773e-1" end +conversion :psig, :psia do |v| "#{v} + 14.695948775" end +conversion :mPascal, :psia do |v| "unit_convert(unit_convert(#{v}))" end + +# To psig +conversion :psia, :psig do |v| "#{v} - 14.695948775" end +conversion :Atmosphere, :psig do |v| "#{v} * 14.695948775 - 14.695948775" end +conversion :Bar, :psig do |v| "#{v} * 14.503773773 - 14.695948775" end +conversion :Pascal, :psig do |v| "#{v} * 1.4503773773e-4 - 14.695948775" end +conversion :kPascal, :psig do |v| "#{v} * 1.4503773773e-1 - 14.695948775" end +conversion :mPascal, :psig do |v| "unit_convert(unit_convert(#{v}))" end diff --git a/lib/unit-defs/resistance.rb b/lib/unit-defs/resistance.rb new file mode 100644 index 0000000..f932f1d --- /dev/null +++ b/lib/unit-defs/resistance.rb @@ -0,0 +1,57 @@ +physical_quantity :Resistance do + symbol "R" + latex "R" + description "Opposition to electric current" +end + +unit :Ohm do + symbol "ohm" + latex "\\Omega" + description "SI unit of electrical resistance" + quantity :Resistance + range 0..1e9 +end + +unit :MilliOhm do + symbol "mohm" + latex "m\\Omega" + description "One thousandth of an ohm" + quantity :Resistance + range 0..1e12 +end + +unit :KiloOhm do + symbol "kohm" + latex "k\\Omega" + description "One thousand ohms" + quantity :Resistance + range 0..1e6 +end + +unit :MegaOhm do + symbol "Mohm" + latex "M\\Omega" + description "One million ohms" + quantity :Resistance + range 0..1e3 +end + +# To Ohm +conversion :MilliOhm, :Ohm do |v| "1e-3 * #{v}" end +conversion :KiloOhm, :Ohm do |v| "1e3 * #{v}" end +conversion :MegaOhm, :Ohm do |v| "1e6 * #{v}" end + +# To MilliOhm +conversion :Ohm, :MilliOhm do |v| "1e3 * #{v}" end +conversion :KiloOhm, :MilliOhm do |v| "1e6 * #{v}" end +conversion :MegaOhm, :MilliOhm do |v| "1e9 * #{v}" end + +# To KiloOhm +conversion :Ohm, :KiloOhm do |v| "1e-3 * #{v}" end +conversion :MilliOhm, :KiloOhm do |v| "1e-6 * #{v}" end +conversion :MegaOhm, :KiloOhm do |v| "1e3 * #{v}" end + +# To MegaOhm +conversion :Ohm, :MegaOhm do |v| "1e-6 * #{v}" end +conversion :MilliOhm, :MegaOhm do |v| "1e-9 * #{v}" end +conversion :KiloOhm, :MegaOhm do |v| "1e-3 * #{v}" end diff --git a/lib/unit-defs/temperature.rb b/lib/unit-defs/temperature.rb new file mode 100644 index 0000000..0ab0294 --- /dev/null +++ b/lib/unit-defs/temperature.rb @@ -0,0 +1,57 @@ +physical_quantity :Temperature do + symbol "T" + latex "T" + description "Average thermal energy of the particles in a substance" +end + +unit :Kelvin do + symbol "degK" + latex "K" + description "Absolute scale where the zero is the lowest temperature" + quantity :Temperature + range 0..672.03886667 +end + +unit :Celsius do + symbol "degC" + latex "\\degree{C}" + description "Scale based on water freezing (0) and boiling point (100)" + quantity :Temperature + range -273.15..398.88888889 +end + +unit :Fahrenheit do + symbol "degF" + latex "\\degree{F}" + description "Scale based on brine freezing (0) and boiling point (100)" + quantity :Temperature + range -459.67..750 +end + +unit :Rankine do + symbol "degR" + latex "\\degree{R}" + description "Absolute scale of temperature" + quantity :Temperature + range 0..1209.67 +end + +# To Fahrenheit +conversion :Kelvin, :Fahrenheit do |t| "1.8 * #{t} - 459.67" end +conversion :Celsius, :Fahrenheit do |t| "1.8 * #{t} + 32" end +conversion :Rankine, :Fahrenheit do |t| "#{t} - 459.67" end + +# To Celsius +conversion :Rankine, :Celsius do |t| "(#{t} - 491.67) / 1.8" end +conversion :Kelvin, :Celsius do |t| "#{t} - 273.15" end +conversion :Fahrenheit, :Celsius do |t| "(#{t} - 32) / 1.8" end + +# To Rankine +conversion :Fahrenheit, :Rankine do |t| "#{t} + 459.67" end +conversion :Celsius, :Rankine do |t| "(#{t} + 273.15) * 1.8" end +conversion :Kelvin, :Rankine do |t| "1.8 * #{t}" end + +# To Kelvin +conversion :Fahrenheit, :Kelvin do |t| "(#{t} + 459.67) / 1.8" end +conversion :Rankine, :Kelvin do |t| "#{t} / 1.8" end +conversion :Celsius, :Kelvin do |t| "#{t} + 273.15" end diff --git a/lib/unit-defs/time.rb b/lib/unit-defs/time.rb new file mode 100644 index 0000000..498b999 --- /dev/null +++ b/lib/unit-defs/time.rb @@ -0,0 +1,57 @@ +physical_quantity :Time do + symbol "t" + latex "t" + description "The mythical and mysterious fourth dimension" +end + +unit :day do + symbol "day" + latex "d" + description "twenty four hours" + quantity :Time + range 0..365*24 +end + +unit :hour do + symbol "h" + latex "h" + description "sixty minutes" + quantity :Time + range 0..10*365*24 +end + +unit :minute do + symbol "min" + latex "min" + description "sixty seconds" + quantity :Time + range 0..10*365*24*60 +end + +unit :second do + symbol "sec" + latex "seconds" + description "VIRGINIA" + quantity :Time + range 0..10*365*24*60*60 +end + +# To hour +conversion :day, :hour do |val| "24*#{val}" end +conversion :minute, :hour do |val| "#{val}/60" end +conversion :second, :hour do |val| "#{val}/(60*60)" end + +# To minute +conversion :day, :minute do |val| "24*60*#{val}" end +conversion :hour, :minute do |val| "60*#{val}" end +conversion :second, :minute do |val| "#{val}/60" end + +# To second +conversion :day, :second do |val| "24*60*60*#{val}" end +conversion :hour, :second do |val| "60*60*#{val}" end +conversion :minute, :second do |val| "60*#{val}" end + +# To day +conversion :hour, :day do |val| "#{val}/24" end +conversion :minute, :day do |val| "#{val}/(24*60)" end +conversion :second, :day do |val| "#{val}/(24*60*60)" end diff --git a/lib/unit-defs/torque.rb b/lib/unit-defs/torque.rb new file mode 100644 index 0000000..47c25e7 --- /dev/null +++ b/lib/unit-defs/torque.rb @@ -0,0 +1,57 @@ +physical_quantity :Torque do + symbol "Tau" + latex "\\tau" + description "Rotational equivalent of force" +end + +unit :NewtonMeter do + symbol "N*m" + latex "N\\cdot m" + description "SI unit of torque" + quantity :Torque + range 0..1e9 +end + +unit :PoundFoot do + symbol "lbf*ft" + latex "lbf\\cdot ft" + description "Imperial torque unit pound-force foot" + quantity :Torque + range 0..7.375621492772656e8 +end + +unit :PoundInch do + symbol "lbf*in" + latex "lbf\\cdot in" + description "Imperial torque unit pound-force inch" + quantity :Torque + range 0..8.850745791327183e9 +end + +unit :KilogramForceMeter do + symbol "kgf*m" + latex "kgf\\cdot m" + description "Torque unit kilogram-force meter" + quantity :Torque + range 0..1.0197162129779283e8 +end + +# To NewtonMeter +conversion :PoundFoot, :NewtonMeter do |v| "1.3558179483314004 * #{v}" end +conversion :PoundInch, :NewtonMeter do |v| "0.1129848290276167 * #{v}" end +conversion :KilogramForceMeter, :NewtonMeter do |v| "9.80665 * #{v}" end + +# To PoundFoot +conversion :NewtonMeter, :PoundFoot do |v| "0.7375621492772656 * #{v}" end +conversion :PoundInch, :PoundFoot do |v| "#{v} / 12" end +conversion :KilogramForceMeter, :PoundFoot do |v| "7.233013851209894 * #{v}" end + +# To PoundInch +conversion :NewtonMeter, :PoundInch do |v| "8.850745791327183 * #{v}" end +conversion :PoundFoot, :PoundInch do |v| "12 * #{v}" end +conversion :KilogramForceMeter, :PoundInch do |v| "86.79616621451873 * #{v}" end + +# To KilogramForceMeter +conversion :NewtonMeter, :KilogramForceMeter do |v| "0.10197162129779283 * #{v}" end +conversion :PoundFoot, :KilogramForceMeter do |v| "0.138254954376 * #{v}" end +conversion :PoundInch, :KilogramForceMeter do |v| "1.1521246198e-2 * #{v}" end diff --git a/lib/unit-defs/velocity.rb b/lib/unit-defs/velocity.rb new file mode 100644 index 0000000..bfa3a96 --- /dev/null +++ b/lib/unit-defs/velocity.rb @@ -0,0 +1,27 @@ +physical_quantity :Velocity do + symbol "V" + latex "V" + description "Rate of change of position" +end + +unit :Ft_S do + symbol "ft/s" + latex "ft/s" + description "feet for second" + quantity :Velocity + range 0..1e6 +end + +unit :Meter_S do + symbol "m/s" + latex "m/s" + description "meter for second" + quantity :Velocity + range 0..304800 +end + +# To Ft_S +conversion :Meter_S, :Ft_S do |v| "3.28083989501312 * #{v}" end + +# To Meter_S +conversion :Ft_S, :Meter_S do |v| "0.3048 * #{v}" end diff --git a/lib/unit-defs/voltage.rb b/lib/unit-defs/voltage.rb new file mode 100644 index 0000000..7a1f39c --- /dev/null +++ b/lib/unit-defs/voltage.rb @@ -0,0 +1,57 @@ +physical_quantity :Voltage do + symbol "U" + latex "U" + description "Electric potential difference" +end + +unit :Volt do + symbol "V" + latex "V" + description "SI unit of electric potential" + quantity :Voltage + range 0..1e7 +end + +unit :MilliVolt do + symbol "mV" + latex "mV" + description "One thousandth of a volt" + quantity :Voltage + range 0..1e10 +end + +unit :KiloVolt do + symbol "kV" + latex "kV" + description "One thousand volts" + quantity :Voltage + range 0..1e4 +end + +unit :MegaVolt do + symbol "MV" + latex "MV" + description "One million volts" + quantity :Voltage + range 0..10 +end + +# To Volt +conversion :MilliVolt, :Volt do |v| "1e-3 * #{v}" end +conversion :KiloVolt, :Volt do |v| "1000 * #{v}" end +conversion :MegaVolt, :Volt do |v| "1e6 * #{v}" end + +# To MilliVolt +conversion :Volt, :MilliVolt do |v| "1000 * #{v}" end +conversion :KiloVolt, :MilliVolt do |v| "1e6 * #{v}" end +conversion :MegaVolt, :MilliVolt do |v| "1e9 * #{v}" end + +# To KiloVolt +conversion :Volt, :KiloVolt do |v| "1e-3 * #{v}" end +conversion :MilliVolt, :KiloVolt do |v| "1e-6 * #{v}" end +conversion :MegaVolt, :KiloVolt do |v| "1000 * #{v}" end + +# To MegaVolt +conversion :Volt, :MegaVolt do |v| "1e-6 * #{v}" end +conversion :MilliVolt, :MegaVolt do |v| "1e-9 * #{v}" end +conversion :KiloVolt, :MegaVolt do |v| "1e-3 * #{v}" end diff --git a/lib/unit-defs/volume.rb b/lib/unit-defs/volume.rb new file mode 100644 index 0000000..6dbf278 --- /dev/null +++ b/lib/unit-defs/volume.rb @@ -0,0 +1,57 @@ +physical_quantity :Volume do + symbol "Vol" + latex "V" + description "Three-dimensional space occupied by matter" +end + +unit :CubicMeter do + symbol "m3" + latex "m^{3}" + description "SI unit of volume" + quantity :Volume + range 0..1e9 +end + +unit :Liter do + symbol "L" + latex "L" + description "One thousandth of a cubic meter" + quantity :Volume + range 0..1e12 +end + +unit :USGallon do + symbol "galUS" + latex "gal_{US}" + description "US liquid gallon" + quantity :Volume + range 0..2.641720523581484e11 +end + +unit :Barrel do + symbol "bbl" + latex "bbl" + description "Oil barrel (42 US gallons)" + quantity :Volume + range 0..6.289810770432105e9 +end + +# To CubicMeter +conversion :Liter, :CubicMeter do |v| "1e-3 * #{v}" end +conversion :USGallon, :CubicMeter do |v| "3.785411784e-3 * #{v}" end +conversion :Barrel, :CubicMeter do |v| "0.158987294928 * #{v}" end + +# To Liter +conversion :CubicMeter, :Liter do |v| "1000 * #{v}" end +conversion :USGallon, :Liter do |v| "3.785411784 * #{v}" end +conversion :Barrel, :Liter do |v| "158.987294928 * #{v}" end + +# To USGallon +conversion :CubicMeter, :USGallon do |v| "264.1720523581484 * #{v}" end +conversion :Liter, :USGallon do |v| "0.2641720523581484 * #{v}" end +conversion :Barrel, :USGallon do |v| "42 * #{v}" end + +# To Barrel +conversion :CubicMeter, :Barrel do |v| "6.289810770432105 * #{v}" end +conversion :Liter, :Barrel do |v| "6.289810770432105e-3 * #{v}" end +conversion :USGallon, :Barrel do |v| "#{v} / 42" end diff --git a/tests/Imakefile b/tests/Imakefile deleted file mode 100644 index a307b5a..0000000 --- a/tests/Imakefile +++ /dev/null @@ -1,36 +0,0 @@ -DEPEND = sh ./depend.sh - -CCLINK = clang++ -fsanitize=address,undefined -CXX = clang++ - -INCLUDES = -I$(TOP)/include -I$(ALEPHW) -WARN= -Wall -Wextra -Wcast-align -Wno-sign-compare -Wno-write-strings \ - -Wno-parentheses - -FLAGS = -std=c++14 $(WARN) -O0 -g -fsanitize=address,undefined -fno-omit-frame-pointer - -OPTIONS = $(FLAGS) -CXXFLAGS= -std=c++14 $(INCLUDES) $(OPTIONS) - -SYS_LIBRARIES = -L$(ALEPHW) -lAleph -lstdc++ -lgsl -lgslcblas -lm -lc - -DEPLIBS = $(TOP)/lib/libuconv.a $(ALEPHW)/libAleph.a - -LOCAL_LIBRARIES = $(TOP)/lib/libuconv.a - -TESTSRCS = test-all-units.cc convert.cc vector-convert.cc - -TESTOBJS = $(TESTSRCS:.cc=.o) - -SRCS = $(TESTSRCS) - -AllTarget(convert) -NormalProgramTarget(convert,convert.o,$(DEPLIBS),$(LOCAL_LIBRARIES),$(SYS_LIBRARIES)) - -AllTarget(vector-convert) -NormalProgramTarget(vector-convert,vector-convert.o,$(DEPLIBS),$(LOCAL_LIBRARIES),$(SYS_LIBRARIES)) - -AllTarget(test-all-units) -NormalProgramTarget(test-all-units,test-all-units.o,$(DEPLIBS),$(LOCAL_LIBRARIES),$(SYS_LIBRARIES)) - -DependTarget() diff --git a/tests/TODO.md b/tests/TODO.md deleted file mode 100644 index b32745f..0000000 --- a/tests/TODO.md +++ /dev/null @@ -1,5 +0,0 @@ -# TODO list -## 1. sks -- Created at 2018-05-11 20:38:35 -0400 -- Completed at 2018-05-11 20:38:38 -0400 - diff --git a/tests/TODO.txt b/tests/TODO.txt deleted file mode 100644 index 7046b1c..0000000 --- a/tests/TODO.txt +++ /dev/null @@ -1 +0,0 @@ -sks,2018-05-11 20:38:35 -0400,2018-05-11 20:38:38 -0400, diff --git a/tests/depend.sh b/tests/depend.sh deleted file mode 100755 index cff2012..0000000 --- a/tests/depend.sh +++ /dev/null @@ -1,129 +0,0 @@ -#!/bin/sh - -# Workaround copy of gccmakedep designed for avoid the problems related -# to c++11 - -# -# makedepend which uses 'gcc -M' -# -# $XFree86: xc/config/util/gccmdep.cpp,v 3.10tsi Exp $ -# -# Based on mdepend.cpp and code supplied by Hongjiu Lu -# - -TMP=mdep$$.tmp -CC=gcc -RM="rm -f" -LN=ln -MV=mv - -${RM} ${TMP} - -trap "${RM} ${TMP}*; exit 1" 1 2 15 -trap "${RM} ${TMP}*; exit 0" 1 2 13 - -files= -makefile= -endmarker= -magic_string='# DO NOT DELETE' -append=n -args= - -while [ $# != 0 ]; do - if [ "$endmarker"x != x -a "$endmarker" = "$1" ]; then - endmarker= - else - case "$1" in - -D*|-I*|-U*) - args="$args '$1'" - ;; - -g*|-O*) - ;; - *) - if [ "$endmarker"x = x ]; then - case $1 in -# ignore these flags - -w|-o|-cc) - shift - ;; - -v) - ;; - -s) - magic_string="$2" - shift - ;; - -f*) - if [ "$1" = "-f-" ]; then - makefile="-" - elif [ "$1" = "-f" ]; then - makefile="$2" - shift - else - echo "$1" | sed 's/^\-f//' >${TMP}arg - makefile="`cat ${TMP}arg`" - rm -f ${TMP}arg - fi - ;; - --*) - endmarker=`echo $1 | sed 's/^\-\-//'` - if [ "$endmarker"x = x ]; then - endmarker="--" - fi - ;; - -a) - append=y - ;; - -*) - echo "Unknown option '$1' ignored" 1>&2 - ;; - *) - files="$files $1" - ;; - esac - fi - ;; - esac - fi - shift -done - -if [ x"$files" = x ]; then -# Nothing to do - exit 0 -fi - -case "$makefile" in - '') - if [ -r makefile ]; then - makefile=makefile - elif [ -r Makefile ]; then - makefile=Makefile - else - echo 'no makefile or Makefile found' 1>&2 - exit 1 - fi - ;; -esac - -if [ X"$makefile" != X- ]; then - if [ x"$append" = xn ]; then - sed -e "/^$magic_string/,\$d" < $makefile > $TMP - echo "$magic_string" >> $TMP - else - cp $makefile $TMP - fi -fi - -CMD="$CC -M -std=c++11 $args $files" -if [ X"$makefile" != X- ]; then - CMD="$CMD >> $TMP" -fi -eval $CMD -if [ X"$makefile" != X- ]; then - $RM ${makefile}.bak - $MV $makefile ${makefile}.bak - $MV $TMP $makefile -fi - -$RM ${TMP}* -exit 0 diff --git a/tests/finished.md b/tests/finished.md deleted file mode 100644 index bce912f..0000000 --- a/tests/finished.md +++ /dev/null @@ -1,6 +0,0 @@ -## 1. fuzzy -- Created at 2018-05-11 20:38:19 -0400 -- Completed at -- Comment -- Finished - diff --git a/tests/finished.txt b/tests/finished.txt deleted file mode 100644 index e5f8c86..0000000 --- a/tests/finished.txt +++ /dev/null @@ -1 +0,0 @@ -fuzzy,2018-05-11 20:38:19 -0400,,,2018-05-11 20:38:23 -0400 diff --git a/update-copyright.sh b/update-copyright.sh index b64ad6f..bd67a27 100755 --- a/update-copyright.sh +++ b/update-copyright.sh @@ -1,6 +1,6 @@ #l=`ls include/units/*.H` -l=`ls include/*.H include/units/*.H tests/*.cc Tests/*.cc Tests/*.H lib/*.cc` +l=`ls include/*.H include/units/*.H utils/*.cc Tests/*.cc Tests/*.H lib/*.cc` for f in $l do diff --git a/tests/.clang_complete b/utils/.clang_complete similarity index 100% rename from tests/.clang_complete rename to utils/.clang_complete diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt new file mode 100644 index 0000000..93b83e6 --- /dev/null +++ b/utils/CMakeLists.txt @@ -0,0 +1,26 @@ +# Required system libraries +find_library(GSL_LIB gsl) +find_library(GSLCBLAS_LIB gslcblas) +find_library(M_LIB m) + +# Aleph library (assuming it is in ALEPHW) +find_library(ALEPH_LIB Aleph PATHS ${ALEPHW} NO_DEFAULT_PATH) + +set(LINK_LIBS uconv ${ALEPH_LIB} ${GSL_LIB} ${GSLCBLAS_LIB} ${M_LIB}) + +# Utility executables +add_executable(util-convert convert.cc) +add_dependencies(util-convert uconv) +target_link_libraries(util-convert ${LINK_LIBS}) + +add_executable(util-vector-convert vector-convert.cc) +add_dependencies(util-vector-convert uconv) +target_link_libraries(util-vector-convert ${LINK_LIBS}) + +add_executable(util-all-units all-units.cc) +add_dependencies(util-all-units uconv) +target_link_libraries(util-all-units ${LINK_LIBS}) + +add_executable(util-unit-table unit-table.cc) +add_dependencies(util-unit-table uconv) +target_link_libraries(util-unit-table ${LINK_LIBS}) diff --git a/tests/test-all-units.cc b/utils/all-units.cc similarity index 62% rename from tests/test-all-units.cc rename to utils/all-units.cc index fc4f5ab..81a357c 100644 --- a/tests/test-all-units.cc +++ b/utils/all-units.cc @@ -51,38 +51,39 @@ # include # include +# include # include # include # include # include +# include -using namespace std; using namespace TCLAP; using namespace Aleph; -size_t precision = 6; +std::size_t precision = 6; -DynList +DynList generate_row(const Unit & unit, double val, double epsilon, bool verbose) { - DynList conversions; + DynList conversions; VtlQuantity q(unit); try { if (verbose) - cout << " sample = " << val << " (" << unit.symbol << ")" << endl; + std::cout << " sample = " << val << " (" << unit.symbol << ")" << std::endl; q = VtlQuantity(unit, val); if (verbose) - cout << " Instantiated = " << q << endl; + std::cout << " Instantiated = " << q << std::endl; } - catch (exception & e) + catch (std::exception & e) { - cout << "Error with generated sample" << val << endl - << e.what() << endl + std::cout << "Error with generated sample" << val << std::endl + << e.what() << std::endl << "Sample " << val << " is not inside [" << unit.min_val - << ", " << unit.max_val << "]" << endl; + << ", " << unit.max_val << "]" << std::endl; abort(); } @@ -93,36 +94,32 @@ generate_row(const Unit & unit, double val, double epsilon, bool verbose) try { if (verbose) - cout << " Converting " << q << " to " - << unit_ptr->symbol << endl; + std::cout << " Converting " << q << " to " + << unit_ptr->symbol << std::endl; conv = VtlQuantity(*unit_ptr, q); // conversion if (verbose) - cout << " done = " << conv << endl; + std::cout << " done = " << conv << std::endl; } - catch (exception & e) + catch (std::exception & e) { - cout << "Error " << q << " to " << unit_ptr->name << endl - << e.what() << endl; + std::cout << "Error " << q << " to " << unit_ptr->name << std::endl + << e.what() << std::endl; abort(); } if (verbose) - cout << " Returning to " << unit.symbol << endl; + std::cout << " Returning to " << unit.symbol << std::endl; VtlQuantity inv = { unit, conv }; if (verbose) - cout << " done = " << inv << endl - << endl; - if (abs(q.get_value() - inv.get_value()) > epsilon) - { - ostringstream s; - s << "Conversion for value " << val << " from unit " - << unit.name << " to unit " << unit_ptr->name - << " does not satisfy epsilon threshold " << epsilon << endl - << "Original value = " << q << endl - << "Intermediate value = " << conv << endl - << "Returned value = " << inv << endl; - throw range_error(s.str()); - } + std::cout << " done = " << inv << std::endl + << std::endl; + ah_range_error_if(std::abs(q.get_value() - inv.get_value()) > epsilon) + << "Conversion for value " << val << " from unit " + << unit.name << " to unit " << unit_ptr->name + << " does not satisfy epsilon threshold " << epsilon << std::endl + << "Original value = " << q << std::endl + << "Intermediate value = " << conv << std::endl + << "Returned value = " << inv << std::endl; conversions.append(to_string(conv.get_value(), precision)); } @@ -130,18 +127,18 @@ generate_row(const Unit & unit, double val, double epsilon, bool verbose) return conversions; } -DynList> +DynList> test_extremes_conversions(const PhysicalQuantity & pq, bool verbose, double max_range, double epsilon) { // only the units related to physical quantity pq auto units = pq.units(); if (verbose) - cout << endl - << "Testing with extremes" << endl - << endl; + std::cout << std::endl + << "Testing with extremes" << std::endl + << std::endl; - DynList> rows; + DynList> rows; // generate the rows for (auto it = units.get_it(); it.has_curr(); it.next()) { @@ -150,115 +147,115 @@ test_extremes_conversions(const PhysicalQuantity & pq, bool verbose, const double urange = std::min(unit_ptr->max_val - min, max_range); const double max = min + urange; - DynList conversions; + DynList conversions; conversions.append(generate_row(*unit_ptr, min, epsilon, verbose)); conversions.append(generate_row(*unit_ptr, max, epsilon, verbose)); - DynList row; + DynList row; row.append(unit_ptr->name); - row.append(to_string(min)); - row.append(to_string(max)); + row.append(std::to_string(min)); + row.append(std::to_string(max)); row.append(conversions); rows.append(row); } // generate title row - DynList title = { "Unit name" }; - DynList vals = { "min", "max" }; + DynList title = { "Unit name" }; + DynList vals = { "min", "max" }; title.append(vals); for (auto it = vals.get_it(); it.has_curr(); it.next()) { - const string & val = it.get_curr(); + const std::string & val = it.get_curr(); for (auto uit = units.get_it(); uit.has_curr(); uit.next()) { - const string name = val + "-" + uit.get_curr()->symbol; + const std::string name = val + "-" + uit.get_curr()->symbol; title.append(name); } } - DynList> ret = { move(title) } ; + DynList> ret = { std::move(title) } ; ret.append(rows); return ret; } -DynList> +DynList> test_random_conversions(const PhysicalQuantity & pq, bool verbose, - const size_t nsamples, double max, double epsilon, + const std::size_t nsamples, double max, double epsilon, gsl_rng * r) { - using Puv = pair>; + using Puv = std::pair>; // only the units related to physical quantity pq auto units = pq.units(); if (verbose) - cout << "Generating random samples " << endl; + std::cout << "Generating random samples " << std::endl; // generate the random samples auto samples = units.maps([nsamples, r, max, verbose] (auto unit_ptr) { if (verbose) - cout << " For " << unit_ptr->name << ":"; + std::cout << " For " << unit_ptr->name << ":"; auto min = unit_ptr->min_val; auto urange = unit_ptr->max_val - min; urange = std::min(urange, max); DynList values; - for (size_t i = 0; i < nsamples; ++i) + for (std::size_t i = 0; i < nsamples; ++i) { auto val = min + urange*gsl_rng_uniform(r); values.append(val); if (verbose) - cout << " " << val; + std::cout << " " << val; } if (verbose) - cout << endl; + std::cout << std::endl; - return make_pair(unit_ptr, move(values)); + return std::make_pair(unit_ptr, std::move(values)); }); if (verbose) - cout << endl - << "Testing " << endl - << endl; + std::cout << std::endl + << "Testing " << std::endl + << std::endl; // generate the rows - auto rows = samples.maps>([epsilon, verbose] (Puv p) + auto rows = samples.maps>([epsilon, verbose] (Puv p) { - DynList conversions; + DynList conversions; const DynList & samples = p.second; for (auto it = samples.get_it(); it.has_curr(); it.next()) conversions.append(generate_row(*p.first, it.get_curr(), epsilon, verbose)); - DynList ret; + DynList ret; ret.append(p.first->name); - ret.append(samples.maps([] (auto v) { return to_string(v); })); + ret.append(samples.maps([] (auto v) { return std::to_string(v); })); ret.append(conversions); return ret; }); // generate title row - DynList title = { "Unit name" }; - DynList vals = range(nsamples).maps([] (auto i) - { return "sample-" + to_string(i); }); + DynList title = { "Unit name" }; + DynList vals = range(nsamples).maps([] (auto i) + { return "sample-" + std::to_string(i); }); title.append(vals); for (auto it = vals.get_it(); it.has_curr(); it.next()) { - const string & val = it.get_curr(); + const std::string & val = it.get_curr(); for (auto uit = units.get_it(); uit.has_curr(); uit.next()) { - const string name = val + "-" + uit.get_curr()->symbol; + const std::string name = val + "-" + uit.get_curr()->symbol; title.append(name); } } - DynList> ret = { move(title) } ; + DynList> ret = { std::move(title) } ; ret.append(rows); return ret; @@ -266,24 +263,24 @@ test_random_conversions(const PhysicalQuantity & pq, bool verbose, struct Epsilon { - string symbol; + std::string symbol; double epsilon; - Epsilon & operator = (const string & str) + Epsilon & operator = (const std::string & str) { - istringstream iss(str); - if (not (iss >> symbol >> epsilon)) - throw TCLAP::ArgParseException(str + " is not a pair unit-symbol epsilon"); + std::istringstream iss(str); + ah_tclap_arg_parse_error_unless(iss >> symbol >> epsilon) + << str << " is not a pair unit-symbol epsilon"; return *this; } - friend ostream& operator << (ostream &os, const Epsilon & a) + friend std::ostream& operator << (std::ostream &os, const Epsilon & a) { return os << a.symbol << " " << a.epsilon; } - ostream& print(ostream &os) const + std::ostream& print(std::ostream &os) const { return os << *this; } @@ -298,14 +295,14 @@ int main(int argc, char *argv[]) { CmdLine cmd(argv[0], ' ', "0"); - ValueArg nsamples = { "n", "num-samples", + ValueArg nsamples = { "n", "num-samples", "number of random samples", false, 3, "number of samples", cmd }; ValueArg m = { "m", "max", "maximum range of a unit", false, 1000, "maximum range of a unit", cmd }; - unsigned long dft_seed = time(nullptr); + unsigned long dft_seed = std::time(nullptr); ValueArg seed = { "s", "seed", "seed for random number generator", false, dft_seed, "random seed", cmd }; @@ -320,11 +317,11 @@ int main(int argc, char *argv[]) "epsilon of form \"unit-symbol epsilon\"", false, "epsilon threshold", cmd); - vector pq = to_vector(PhysicalQuantity::names()); + std::vector pq = to_vector(PhysicalQuantity::names()); PhysicalQuantity::quantities().for_each([&pq] (auto p) { pq.push_back(p->name); }); - ValuesConstraint allowed(pq); - ValueArg pm = { "Q", "physical-quantity", "name of physical quantity", + ValuesConstraint allowed(pq); + ValueArg pm = { "Q", "physical-quantity", "name of physical quantity", false, "", &allowed, cmd }; SwitchArg extremes = { "x", "extremes", "test units extremes", cmd, false}; @@ -336,46 +333,46 @@ int main(int argc, char *argv[]) SwitchArg ver = { "v", "verbose", "verbose mode", cmd, false }; - ValueArg d = { "d", "digits", "number of digits", false, 10, + ValueArg d = { "d", "digits", "number of digits", false, 10, "number of digits", cmd }; cmd.parse(argc, argv); if (print_pq.getValue()) { - PhysicalQuantity::names().for_each([] (const string & name) + PhysicalQuantity::names().for_each([] (const std::string & name) { - cout << name << endl; + std::cout << name << std::endl; }); exit(0); } if (not pm.isSet()) { - cout << "PARSE ERROR:" << endl + std::cout << "PARSE ERROR:" << std::endl << " Required argument missing: physical-quantity" - << endl; + << std::endl; abort(); } auto verbose = ver.getValue(); - unique_ptr + std::unique_ptr r(gsl_rng_alloc(gsl_rng_mt19937), gsl_rng_free); gsl_rng_set(r.get(), seed.getValue() % gsl_rng_max(r.get())); auto ptr = PhysicalQuantity::search(pm.getValue()); if (ptr == nullptr) { - cout << "Physical quantity " << pm.getValue() << " not found " << endl; + std::cout << "Physical quantity " << pm.getValue() << " not found " << std::endl; abort(); } auto p = check_conversions(*ptr); if (not p.first) { - cout << "Missing conversions: " << endl; - p.second.for_each([] (auto s) { cout << " " << s << endl; }); + std::cout << "Missing conversions: " << std::endl; + p.second.for_each([] (auto s) { std::cout << " " << s << std::endl; }); abort(); } @@ -384,16 +381,16 @@ int main(int argc, char *argv[]) auto unit_ptr = Unit::search_by_symbol(ep.symbol); if (unit_ptr == nullptr) { - cout << "In epsilon specification: unit symbol " << ep.symbol - << " not found" << endl; + std::cout << "In epsilon specification: unit symbol " << ep.symbol + << " not found" << std::endl; abort(); } if (ep.epsilon <= 0 or ep.epsilon >= 1) { - cout << "In epsilon specification of unit" << unit_ptr->name + std::cout << "In epsilon specification of unit" << unit_ptr->name << " (" << ep.symbol << "): value " << ep.epsilon - << " is not inside the range (0, 1)" << endl; + << " is not inside the range (0, 1)" << std::endl; abort(); } unit_ptr->set_epsilon(ep.epsilon); @@ -401,18 +398,18 @@ int main(int argc, char *argv[]) if (print.getValue()) { - cout << ptr->to_string(60) << endl; + std::cout << ptr->to_string(60) << std::endl; ptr->units().for_each([] (auto u) { - cout << *u << endl - << endl; + std::cout << *u << std::endl + << std::endl; }); } if (d.isSet()) precision = d.getValue(); - DynList> mat; + DynList> mat; if (extremes.getValue()) mat = test_extremes_conversions(*ptr, verbose, m.getValue(), epsilon.getValue()); @@ -420,12 +417,12 @@ int main(int argc, char *argv[]) mat = test_random_conversions(*ptr, verbose, nsamples.getValue(), m.getValue(), epsilon.getValue(), r.get()); - cout << "Seed = " << seed.getValue() << endl; + std::cout << "Seed = " << seed.getValue() << std::endl; if (csv.getValue()) - cout << to_string(format_string_csv(mat)) << endl; + std::cout << to_string(format_string_csv(mat)) << std::endl; else - cout << to_string(format_string(mat)) << endl; + std::cout << to_string(format_string(mat)) << std::endl; return 0; } diff --git a/tests/convert.cc b/utils/convert.cc similarity index 54% rename from tests/convert.cc rename to utils/convert.cc index 022aa3c..7f99a95 100644 --- a/tests/convert.cc +++ b/utils/convert.cc @@ -34,7 +34,7 @@ using namespace TCLAP; UnitsInstancer init; -const Unit * search_unit(const string & name) +const Unit * search_unit(const std::string & name) { auto unit_ptr = Unit::search_by_name(name); if (unit_ptr == nullptr) @@ -42,91 +42,95 @@ const Unit * search_unit(const string & name) unit_ptr = Unit::search_by_symbol(name); if (unit_ptr == nullptr) { - cout << "Unit " << name << " not found" << endl; + std::cout << "Unit " << name << " not found" << std::endl; abort(); } } return unit_ptr; } -void convert(const Unit * src_unit, const Unit * tgt_unit, istream & in) +void convert(const Unit * src_unit, const Unit * tgt_unit, std::istream & in) { double val; while (in >> val) - cout << unit_convert(*src_unit, val, *tgt_unit) << " "; - cout << endl; + std::cout << unit_convert(*src_unit, val, *tgt_unit) << " "; + std::cout << std::endl; } void list_all_units() { - DynList> rows; + DynList> rows; PhysicalQuantity::quantities().for_each([&rows] (auto & pq) { - rows.append(pq->units().template maps>([&pq] (auto p) + rows.append(pq->units().template maps>([&pq] (auto p) { - return DynList({ pq->name, pq->symbol, pq->latex_symbol, + return DynList({ pq->name, pq->symbol, pq->latex_symbol, p->name, p->symbol, p->latex_symbol }); })); }); rows.insert({"Physical-Quantity", "symbol", "LaTeX symbol", "Unit name", "Unit symbol", "LaTeX symbol"}); - cout << to_string(format_string(rows)) << endl; + std::cout << to_string(format_string(rows)) << std::endl; exit(0); } void ruby_hash() { - cout << "{" << endl; + std::cout << "{" << std::endl; Unit::units().for_each([] (const Unit * ptr) { - cout << " '" << ptr->name << "' => '" << ptr->symbol << "'," << endl; + std::cout << " '" << ptr->name << "' => '" << ptr->symbol << "'," << std::endl; }); - cout << "}" << endl; + std::cout << "}" << std::endl; exit(0); } void list_sibling_units(const Unit & unit) { - DynList> rows = unit.family_units().maps> + DynList> rows = unit.family_units().maps> ([] (auto ptr) { - return DynList({ptr->name, ptr->symbol, ptr->latex_symbol}); + return DynList({ptr->name, ptr->symbol, ptr->latex_symbol}); }); rows.insert({"name", "symbol", "LaTeX symbol"}); - cout << to_string(format_string(rows)) << endl; + std::cout << to_string(format_string(rows)) << std::endl; exit(0); } CmdLine cmd("conversion", ' ', "0"); -vector units = to_vector(flatten(Unit::units(). - maps>([] (auto u) - { return build_dynlist(u->name, u->symbol); }))); +// Helper to build units std::vector avoiding flatten issues +std::vector build_units_vector() { + DynList all_unit_strings; + Unit::units().for_each([&all_unit_strings](auto u) { + all_unit_strings.append(u->name); + all_unit_strings.append(u->symbol); + }); + return to_vector(all_unit_strings); +} -// vector units = -// to_vector(Unit::units().maps([] (auto u) -// { return u->symbol; })); +std::vector units = build_units_vector(); -ValuesConstraint allowed(units); -ValueArg unit = { "u", "unit-symbol", "symbol of unit", +ValuesConstraint allowed(units); +ValueArg unit = { "u", "unit-symbol", "symbol of unit", false, "", &allowed, cmd }; -MultiArg unit_desc = { "U", "Unit-symbol", "describe unit", +MultiArg unit_desc = { "U", "Unit-symbol", "describe unit", false, &allowed, cmd }; -vector pqs = - to_vector(PhysicalQuantity::quantities().maps([] (auto p) +std::vector pqs = + to_vector(PhysicalQuantity::quantities().maps([] (auto p) { return p->name; })); -ValuesConstraint pq_allowed(pqs); -ValueArg unit_list = { "L", "list-units", +ValuesConstraint pq_allowed(pqs); +ValueArg unit_list = { "L", "list-units", "list units associated to a physical quantity", false, "", &pq_allowed, cmd }; -ValueArg sample = {"s", "sample", "sample", false, 0, "sample", cmd}; +ValueArg sample_val = {"s", "sample", "sample", false, 0, "sample", cmd}; -ValueArg source = {"S", "source-unit", "source unit", false, +ValueArg source = {"S", "source-unit", "source unit", false, "", "source unit", cmd}; -ValueArg target = {"T", "target-unit", "target unit", false, +ValueArg target = {"T", "target-unit", "target unit", false, "", "target unit", cmd}; SwitchArg l = { "l", "list", "list all units", cmd, false }; @@ -137,10 +141,10 @@ SwitchArg json("j", "json", "json list of units", cmd, false); SwitchArg ruby("r", "ruby", "ruby list of units", cmd, false); -ValueArg file = { "f", "file", "input file name", false, "", +ValueArg file = { "f", "file", "input file name", false, "", "input file name", cmd }; -SwitchArg in_pipe = { "p", "pipe", "input by cin", cmd }; +SwitchArg in_pipe = { "p", "pipe", "input by std::cin", cmd }; void test(int argc, char *argv[]) { @@ -161,16 +165,16 @@ void test(int argc, char *argv[]) auto pq = PhysicalQuantity::search(pq_name); if (pq == nullptr) { - cout << "Physical quantity " << pq_name << " not found" << endl; + std::cout << "Physical quantity " << pq_name << " not found" << std::endl; abort(); } - pq->units().for_each([] (auto uptr) { cout << uptr->name << endl; }); + pq->units().for_each([] (auto uptr) { std::cout << uptr->name << std::endl; }); exit(0); } if (json.isSet()) { - cout << units_json() << endl; + std::cout << units_json() << std::endl; exit(0); } @@ -179,8 +183,8 @@ void test(int argc, char *argv[]) if (unit_desc.isSet()) { for (const auto & s : unit_desc.getValue()) - cout << Unit::search_by_symbol(s)->to_string(50, 2) << endl - << endl; + std::cout << Unit::search_by_symbol(s)->to_string(50, 2) << std::endl + << std::endl; exit(0); } @@ -192,13 +196,13 @@ void test(int argc, char *argv[]) if (file.isSet() or in_pipe.getValue()) { if (in_pipe.isSet()) - convert(src_ptr, tgt_ptr, cin); + convert(src_ptr, tgt_ptr, std::cin); else { - ifstream in(file.getValue()); + std::ifstream in(file.getValue()); if (not in.good()) { - cout << "Cannot open " << file.getValue() << endl; + std::cout << "Cannot open " << file.getValue() << std::endl; abort(); } convert(src_ptr, tgt_ptr, in); @@ -206,43 +210,43 @@ void test(int argc, char *argv[]) exit(0); } - VtlQuantity val(*src_ptr, sample.getValue()); - cout << VtlQuantity(*tgt_ptr, val).raw() << endl; + VtlQuantity val(*src_ptr, sample_val.getValue()); + std::cout << VtlQuantity(*tgt_ptr, val).raw() << std::endl; exit(0); } - if (not unit.isSet() and not sample.isSet()) + if (not unit.isSet() and not sample_val.isSet()) { - cout << "Flags -u and -s must be set" << endl; + std::cout << "Flags -u and -s must be set" << std::endl; abort(); } auto unit_ptr = search_unit(unit.getValue()); if (unit_ptr == nullptr) { - cout << "Unit symbol " << unit.getValue() << " not found" << endl; + std::cout << "Unit symbol " << unit.getValue() << " not found" << std::endl; abort(); } - cout << *unit_ptr << endl - << endl - << "Conversions:" << endl - << endl; + std::cout << *unit_ptr << std::endl + << std::endl + << "Conversions:" << std::endl + << std::endl; - VtlQuantity val(*unit_ptr, sample.getValue()); + VtlQuantity val(*unit_ptr, sample_val.getValue()); for (auto u : Unit::units(unit_ptr->physical_quantity)) { if (verbose) - cout << " " << val << " to " << u->symbol << endl; + std::cout << " " << val << " to " << u->symbol << std::endl; if (exist_conversion(*unit_ptr, *u)) - cout << " " << val << " = " << VtlQuantity(*u, val) << endl; + std::cout << " " << val << " = " << VtlQuantity(*u, val) << std::endl; else - cout << " " << val << " = " + std::cout << " " << val << " = " << "WARNING: there is no conversion from " << unit_ptr->symbol - << " to " << u->symbol << endl; + << " to " << u->symbol << std::endl; } - cout << endl; + std::cout << std::endl; } int main(int argc, char *argv[]) diff --git a/tests/test-unit-table.cc b/utils/unit-table.cc similarity index 100% rename from tests/test-unit-table.cc rename to utils/unit-table.cc diff --git a/tests/vector-convert.cc b/utils/vector-convert.cc similarity index 86% rename from tests/vector-convert.cc rename to utils/vector-convert.cc index 592008c..893c4f7 100644 --- a/tests/vector-convert.cc +++ b/utils/vector-convert.cc @@ -49,17 +49,17 @@ using namespace TCLAP; -CmdLine cmd = { "vector-conversion", ' ', "0" }; +CmdLine cmd = { "std::vector-conversion", ' ', "0" }; -ValueArg source = {"S", "source-unit", "source unit", true, +ValueArg source = {"S", "source-unit", "source unit", true, "", "source unit", cmd}; -ValueArg target = {"T", "target-unit", "target unit", true, +ValueArg target = {"T", "target-unit", "target unit", true, "", "target unit", cmd}; UnlabeledMultiArg vals = { "values", "list of values to convert", true, "list of values to convert", cmd }; -const Unit * search_unit(const string & name) +const Unit * search_unit(const std::string & name) { auto unit_ptr = Unit::search_by_name(name); if (unit_ptr == nullptr) @@ -67,7 +67,7 @@ const Unit * search_unit(const string & name) unit_ptr = Unit::search_by_symbol(name); if (unit_ptr == nullptr) { - cout << "Unit " << name << " not found" << endl; + std::cout << "Unit " << name << " not found" << std::endl; abort(); } } @@ -81,8 +81,8 @@ void convert() auto convert_fct = search_conversion(*src_unit, *tgt_unit); for (auto v : vals) - cout << (*convert_fct)(v) << " "; - cout << endl; + std::cout << (*convert_fct)(v) << " "; + std::cout << std::endl; } int main(int argc, char *argv[])