siderust-js 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +166 -0
- package/.gitmodules +9 -0
- package/CHANGELOG.md +26 -0
- package/LICENSE +661 -0
- package/README.md +138 -0
- package/package.json +12 -0
- package/qtty-js/.github/workflows/ci.yml +151 -0
- package/qtty-js/.gitmodules +3 -0
- package/qtty-js/CHANGELOG.md +31 -0
- package/qtty-js/LICENSE +661 -0
- package/qtty-js/README.md +132 -0
- package/qtty-js/package.json +20 -0
- package/qtty-js/qtty/.github/workflows/ci.yml +155 -0
- package/qtty-js/qtty/CHANGELOG.md +120 -0
- package/qtty-js/qtty/Cargo.lock +1462 -0
- package/qtty-js/qtty/Cargo.toml +12 -0
- package/qtty-js/qtty/LICENSE +661 -0
- package/qtty-js/qtty/README.md +9 -0
- package/qtty-js/qtty/qtty/Cargo.toml +41 -0
- package/qtty-js/qtty/qtty/README.md +8 -0
- package/qtty-js/qtty/qtty/examples/angles.rs +14 -0
- package/qtty-js/qtty/qtty/examples/astronomy.rs +17 -0
- package/qtty-js/qtty/qtty/examples/dimensional_arithmetic.rs +83 -0
- package/qtty-js/qtty/qtty/examples/python_integration.rs +61 -0
- package/qtty-js/qtty/qtty/examples/quickstart.rs +15 -0
- package/qtty-js/qtty/qtty/examples/ratios.rs +12 -0
- package/qtty-js/qtty/qtty/examples/serde_with_unit.rs +234 -0
- package/qtty-js/qtty/qtty/examples/serialization.rs +141 -0
- package/qtty-js/qtty/qtty/examples/serialization_advanced.rs +155 -0
- package/qtty-js/qtty/qtty/src/f32.rs +108 -0
- package/qtty-js/qtty/qtty/src/f64.rs +30 -0
- package/qtty-js/qtty/qtty/src/i128.rs +111 -0
- package/qtty-js/qtty/qtty/src/i16.rs +111 -0
- package/qtty-js/qtty/qtty/src/i32.rs +111 -0
- package/qtty-js/qtty/qtty/src/i64.rs +111 -0
- package/qtty-js/qtty/qtty/src/i8.rs +111 -0
- package/qtty-js/qtty/qtty/src/lib.rs +238 -0
- package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std/Cargo.lock +83 -0
- package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std/Cargo.toml +10 -0
- package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std/src/lib.rs +7 -0
- package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std-alloc/Cargo.lock +83 -0
- package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std-alloc/Cargo.toml +10 -0
- package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std-alloc/src/lib.rs +7 -0
- package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-std/Cargo.lock +83 -0
- package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-std/Cargo.toml +10 -0
- package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-std/src/lib.rs +5 -0
- package/qtty-js/qtty/qtty/tests/integration_tests.rs +529 -0
- package/qtty-js/qtty/qtty/tests/qtty_vec_feature_matrix.rs +58 -0
- package/qtty-js/qtty/qtty-core/Cargo.toml +41 -0
- package/qtty-js/qtty/qtty-core/README.md +8 -0
- package/qtty-js/qtty/qtty-core/examples/diesel_integration.rs +145 -0
- package/qtty-js/qtty/qtty-core/examples/quantity_db_serde.rs +215 -0
- package/qtty-js/qtty/qtty-core/src/dimension.rs +249 -0
- package/qtty-js/qtty/qtty-core/src/feature_diesel.rs +318 -0
- package/qtty-js/qtty/qtty-core/src/feature_pyo3.rs +27 -0
- package/qtty-js/qtty/qtty-core/src/feature_serde.rs +203 -0
- package/qtty-js/qtty/qtty-core/src/feature_tiberius.rs +28 -0
- package/qtty-js/qtty/qtty-core/src/lib.rs +744 -0
- package/qtty-js/qtty/qtty-core/src/macros.rs +93 -0
- package/qtty-js/qtty/qtty-core/src/quantity.rs +810 -0
- package/qtty-js/qtty/qtty-core/src/scalar.rs +1742 -0
- package/qtty-js/qtty/qtty-core/src/unit.rs +332 -0
- package/qtty-js/qtty/qtty-core/src/units/angular.rs +1228 -0
- package/qtty-js/qtty/qtty-core/src/units/area.rs +243 -0
- package/qtty-js/qtty/qtty-core/src/units/frequency.rs +179 -0
- package/qtty-js/qtty/qtty-core/src/units/length.rs +1270 -0
- package/qtty-js/qtty/qtty-core/src/units/mass.rs +488 -0
- package/qtty-js/qtty/qtty-core/src/units/mod.rs +26 -0
- package/qtty-js/qtty/qtty-core/src/units/power.rs +324 -0
- package/qtty-js/qtty/qtty-core/src/units/time.rs +667 -0
- package/qtty-js/qtty/qtty-core/src/units/unitless.rs +212 -0
- package/qtty-js/qtty/qtty-core/src/units/velocity.rs +210 -0
- package/qtty-js/qtty/qtty-core/src/units/volume.rs +269 -0
- package/qtty-js/qtty/qtty-core/tests/core.rs +628 -0
- package/qtty-js/qtty/qtty-core/tests/diesel.rs +461 -0
- package/qtty-js/qtty/qtty-core/tests/integers.rs +632 -0
- package/qtty-js/qtty/qtty-core/tests/no_cross_unit_ops.rs +35 -0
- package/qtty-js/qtty/qtty-core/tests/pyo3.rs +334 -0
- package/qtty-js/qtty/qtty-core/tests/quantity_f32.rs +276 -0
- package/qtty-js/qtty/qtty-core/tests/scalar_decimal.rs +258 -0
- package/qtty-js/qtty/qtty-core/tests/scalar_f32.rs +286 -0
- package/qtty-js/qtty/qtty-core/tests/scalar_f64_real.rs +287 -0
- package/qtty-js/qtty/qtty-core/tests/scalar_rational.rs +260 -0
- package/qtty-js/qtty/qtty-core/tests/serde.rs +256 -0
- package/qtty-js/qtty/qtty-core/tests/tiberius.rs +208 -0
- package/qtty-js/qtty/qtty-derive/Cargo.toml +23 -0
- package/qtty-js/qtty/qtty-derive/README.md +8 -0
- package/qtty-js/qtty/qtty-derive/src/lib.rs +340 -0
- package/qtty-js/qtty/qtty-ffi/ARCHITECTURE.md +3 -0
- package/qtty-js/qtty/qtty-ffi/Cargo.toml +31 -0
- package/qtty-js/qtty/qtty-ffi/README.md +9 -0
- package/qtty-js/qtty/qtty-ffi/build.rs +326 -0
- package/qtty-js/qtty/qtty-ffi/cbindgen.toml +105 -0
- package/qtty-js/qtty/qtty-ffi/include/qtty_ffi.h +1126 -0
- package/qtty-js/qtty/qtty-ffi/src/ffi.rs +1251 -0
- package/qtty-js/qtty/qtty-ffi/src/ffi_serde.rs +294 -0
- package/qtty-js/qtty/qtty-ffi/src/helpers.rs +310 -0
- package/qtty-js/qtty/qtty-ffi/src/lib.rs +229 -0
- package/qtty-js/qtty/qtty-ffi/src/macros.rs +121 -0
- package/qtty-js/qtty/qtty-ffi/src/registry.rs +274 -0
- package/qtty-js/qtty/qtty-ffi/src/types.rs +620 -0
- package/qtty-js/qtty/qtty-ffi/tests/integration_tests.rs +842 -0
- package/qtty-js/qtty/qtty-ffi/units.csv +156 -0
- package/qtty-js/qtty/qtty-ffi/units.csv.md +3 -0
- package/qtty-js/qtty-node/.prettierignore +6 -0
- package/qtty-js/qtty-node/.prettierrc.json +6 -0
- package/qtty-js/qtty-node/README.md +250 -0
- package/qtty-js/qtty-node/c8.config.json +11 -0
- package/qtty-js/qtty-node/eslint.config.js +31 -0
- package/qtty-js/qtty-node/examples/arithmetic.mjs +64 -0
- package/qtty-js/qtty-node/examples/astronomy.mjs +90 -0
- package/qtty-js/qtty-node/examples/quickstart.mjs +36 -0
- package/qtty-js/qtty-node/examples/serialization.mjs +125 -0
- package/qtty-js/qtty-node/examples/unit_factories.mjs +74 -0
- package/qtty-js/qtty-node/index.d.ts +219 -0
- package/qtty-js/qtty-node/index.js +323 -0
- package/qtty-js/qtty-node/lib/DerivedQuantity.js +122 -0
- package/qtty-js/qtty-node/lib/Quantity.js +151 -0
- package/qtty-js/qtty-node/lib/backend.js +25 -0
- package/qtty-js/qtty-node/native.cjs +306 -0
- package/qtty-js/qtty-node/package-lock.json +3223 -0
- package/qtty-js/qtty-node/package.json +70 -0
- package/qtty-js/qtty-node/units.d.ts +299 -0
- package/qtty-js/qtty-node/units.js +210 -0
- package/qtty-js/qtty-web/Cargo.lock +767 -0
- package/qtty-js/qtty-web/Cargo.toml +21 -0
- package/qtty-js/qtty-web/index.d.ts +140 -0
- package/qtty-js/qtty-web/index.js +20 -0
- package/qtty-js/qtty-web/lib/DerivedQuantity.js +58 -0
- package/qtty-js/qtty-web/lib/Quantity.js +75 -0
- package/qtty-js/qtty-web/lib/backend.js +80 -0
- package/qtty-js/qtty-web/package.json +45 -0
- package/qtty-js/qtty-web/src/lib.rs +111 -0
- package/qtty-js/scripts/ci.sh +73 -0
- package/scripts/ci.sh +123 -0
- package/siderust-core/Cargo.lock +787 -0
- package/siderust-core/Cargo.toml +18 -0
- package/siderust-core/DEDUPLICATION.md +124 -0
- package/siderust-core/src/body.rs +120 -0
- package/siderust-core/src/events.rs +184 -0
- package/siderust-core/src/lib.rs +20 -0
- package/siderust-core/src/observer.rs +55 -0
- package/siderust-core/src/position.rs +213 -0
- package/siderust-node/.prettierignore +7 -0
- package/siderust-node/.prettierrc.json +6 -0
- package/siderust-node/Cargo.lock +906 -0
- package/siderust-node/Cargo.toml +29 -0
- package/siderust-node/README.md +109 -0
- package/siderust-node/__test__/index.test.mjs +248 -0
- package/siderust-node/build.rs +5 -0
- package/siderust-node/c8.config.json +3 -0
- package/siderust-node/eslint.config.js +31 -0
- package/siderust-node/examples/01_basic_coordinates.mjs +24 -0
- package/siderust-node/examples/02_coordinate_transformations.mjs +25 -0
- package/siderust-node/examples/03_all_frames_conversions.mjs +26 -0
- package/siderust-node/examples/04_all_center_conversions.mjs +24 -0
- package/siderust-node/examples/05_target_tracking.mjs +22 -0
- package/siderust-node/examples/06_night_events.mjs +18 -0
- package/siderust-node/examples/07_moon_properties.mjs +21 -0
- package/siderust-node/examples/08_solar_system.mjs +19 -0
- package/siderust-node/examples/09_star_observability.mjs +22 -0
- package/siderust-node/examples/10_time_periods.mjs +9 -0
- package/siderust-node/examples/11_serialization.mjs +31 -0
- package/siderust-node/examples/12_runtime_ephemeris.mjs +27 -0
- package/siderust-node/examples/13_coordinate_operations.mjs +20 -0
- package/siderust-node/index.d.ts +623 -0
- package/siderust-node/index.js +79 -0
- package/siderust-node/lib/Observer.js +112 -0
- package/siderust-node/lib/Star.js +118 -0
- package/siderust-node/lib/backend.js +63 -0
- package/siderust-node/lib/wrappers.js +566 -0
- package/siderust-node/main.js +20 -0
- package/siderust-node/native.cjs +360 -0
- package/siderust-node/package-lock.json +3261 -0
- package/siderust-node/package.json +71 -0
- package/siderust-node/src/body.rs +74 -0
- package/siderust-node/src/coordinates.rs +372 -0
- package/siderust-node/src/ephemeris.rs +462 -0
- package/siderust-node/src/events.rs +577 -0
- package/siderust-node/src/lib.rs +43 -0
- package/siderust-node/src/observer.rs +132 -0
- package/siderust-node/src/phase.rs +218 -0
- package/siderust-node/src/position.rs +292 -0
- package/siderust-node/src/star.rs +200 -0
- package/siderust-web/Cargo.lock +855 -0
- package/siderust-web/Cargo.toml +34 -0
- package/siderust-web/README.md +100 -0
- package/siderust-web/__test__/index.test.mjs +118 -0
- package/siderust-web/examples/github-pages/README.md +31 -0
- package/siderust-web/examples/github-pages/index.html +135 -0
- package/siderust-web/index.d.ts +311 -0
- package/siderust-web/index.js +66 -0
- package/siderust-web/lib/Observer.js +103 -0
- package/siderust-web/lib/Star.js +116 -0
- package/siderust-web/lib/backend.js +400 -0
- package/siderust-web/lib/wrappers.js +512 -0
- package/siderust-web/package.json +55 -0
- package/siderust-web/src/body.rs +69 -0
- package/siderust-web/src/coordinates.rs +302 -0
- package/siderust-web/src/ephemeris.rs +456 -0
- package/siderust-web/src/events.rs +520 -0
- package/siderust-web/src/lib.rs +51 -0
- package/siderust-web/src/observer.rs +117 -0
- package/siderust-web/src/phase.rs +190 -0
- package/siderust-web/src/position.rs +291 -0
- package/siderust-web/src/star.rs +178 -0
- package/tempoch-js/.github/workflows/ci.yml +142 -0
- package/tempoch-js/.gitmodules +3 -0
- package/tempoch-js/CHANGELOG.md +25 -0
- package/tempoch-js/LICENSE +661 -0
- package/tempoch-js/README.md +126 -0
- package/tempoch-js/package.json +20 -0
- package/tempoch-js/scripts/ci.sh +73 -0
- package/tempoch-js/tempoch/.github/workflows/ci.yml +113 -0
- package/tempoch-js/tempoch/CHANGELOG.md +82 -0
- package/tempoch-js/tempoch/Cargo.lock +947 -0
- package/tempoch-js/tempoch/Cargo.toml +3 -0
- package/tempoch-js/tempoch/LICENSE +661 -0
- package/tempoch-js/tempoch/README.md +76 -0
- package/tempoch-js/tempoch/tempoch/Cargo.toml +27 -0
- package/tempoch-js/tempoch/tempoch/examples/periods.rs +45 -0
- package/tempoch-js/tempoch/tempoch/examples/quickstart.rs +13 -0
- package/tempoch-js/tempoch/tempoch/src/lib.rs +49 -0
- package/tempoch-js/tempoch/tempoch/tests/integration.rs +57 -0
- package/tempoch-js/tempoch/tempoch-core/Cargo.toml +24 -0
- package/tempoch-js/tempoch/tempoch-core/src/delta_t.rs +345 -0
- package/tempoch-js/tempoch/tempoch-core/src/instant.rs +811 -0
- package/tempoch-js/tempoch/tempoch-core/src/julian_date_ext.rs +142 -0
- package/tempoch-js/tempoch/tempoch-core/src/lib.rs +81 -0
- package/tempoch-js/tempoch/tempoch-core/src/period.rs +1168 -0
- package/tempoch-js/tempoch/tempoch-core/src/scales.rs +779 -0
- package/tempoch-js/tempoch/tempoch-ffi/Cargo.lock +889 -0
- package/tempoch-js/tempoch/tempoch-ffi/Cargo.toml +26 -0
- package/tempoch-js/tempoch/tempoch-ffi/build.rs +24 -0
- package/tempoch-js/tempoch/tempoch-ffi/cbindgen.toml +30 -0
- package/tempoch-js/tempoch/tempoch-ffi/src/error.rs +19 -0
- package/tempoch-js/tempoch/tempoch-ffi/src/lib.rs +82 -0
- package/tempoch-js/tempoch/tempoch-ffi/src/period.rs +101 -0
- package/tempoch-js/tempoch/tempoch-ffi/src/time.rs +711 -0
- package/tempoch-js/tempoch/tempoch-ffi/tests/ffi.rs +265 -0
- package/tempoch-js/tempoch-node/.prettierignore +6 -0
- package/tempoch-js/tempoch-node/.prettierrc.json +6 -0
- package/tempoch-js/tempoch-node/Cargo.lock +496 -0
- package/tempoch-js/tempoch-node/Cargo.toml +29 -0
- package/tempoch-js/tempoch-node/README.md +265 -0
- package/tempoch-js/tempoch-node/__test__/index.test.mjs +598 -0
- package/tempoch-js/tempoch-node/build.rs +5 -0
- package/tempoch-js/tempoch-node/c8.config.json +3 -0
- package/tempoch-js/tempoch-node/eslint.config.js +31 -0
- package/tempoch-js/tempoch-node/examples/periods.mjs +79 -0
- package/tempoch-js/tempoch-node/examples/quickstart.mjs +71 -0
- package/tempoch-js/tempoch-node/examples/timescales.mjs +92 -0
- package/tempoch-js/tempoch-node/index.d.ts +280 -0
- package/tempoch-js/tempoch-node/index.js +32 -0
- package/tempoch-js/tempoch-node/lib/JulianDate.js +176 -0
- package/tempoch-js/tempoch-node/lib/ModifiedJulianDate.js +156 -0
- package/tempoch-js/tempoch-node/lib/Period.js +133 -0
- package/tempoch-js/tempoch-node/lib/backend.js +38 -0
- package/tempoch-js/tempoch-node/lib/qttyCompat.js +92 -0
- package/tempoch-js/tempoch-node/native.cjs +317 -0
- package/tempoch-js/tempoch-node/package-lock.json +3223 -0
- package/tempoch-js/tempoch-node/package.json +56 -0
- package/tempoch-js/tempoch-node/src/lib.rs +573 -0
- package/tempoch-js/tempoch-web/Cargo.toml +23 -0
- package/tempoch-js/tempoch-web/index.d.ts +95 -0
- package/tempoch-js/tempoch-web/index.js +27 -0
- package/tempoch-js/tempoch-web/lib/JulianDate.js +170 -0
- package/tempoch-js/tempoch-web/lib/ModifiedJulianDate.js +145 -0
- package/tempoch-js/tempoch-web/lib/Period.js +121 -0
- package/tempoch-js/tempoch-web/lib/backend.js +118 -0
- package/tempoch-js/tempoch-web/package.json +46 -0
- package/tempoch-js/tempoch-web/src/lib.rs +184 -0
|
@@ -0,0 +1,1251 @@
|
|
|
1
|
+
//! Extern "C" API for FFI consumers.
|
|
2
|
+
//!
|
|
3
|
+
//! This module exposes `#[no_mangle] pub extern "C"` functions that form the stable C ABI
|
|
4
|
+
//! for `qtty-ffi`. These functions can be called from C/C++ code or any language with C FFI support.
|
|
5
|
+
//!
|
|
6
|
+
//! # Safety
|
|
7
|
+
//!
|
|
8
|
+
//! All functions in this module:
|
|
9
|
+
//! - Never panic across FFI boundaries (all panics are caught and converted to error codes)
|
|
10
|
+
//! - Validate all input pointers before use
|
|
11
|
+
//! - Return status codes to indicate success or failure
|
|
12
|
+
//!
|
|
13
|
+
//! # Status Codes
|
|
14
|
+
//!
|
|
15
|
+
//! - `QTTY_OK` (0): Success
|
|
16
|
+
//! - `QTTY_ERR_UNKNOWN_UNIT` (-1): Invalid or unrecognized unit ID
|
|
17
|
+
//! - `QTTY_ERR_INCOMPATIBLE_DIM` (-2): Units have different dimensions
|
|
18
|
+
//! - `QTTY_ERR_NULL_OUT` (-3): Required output pointer was null
|
|
19
|
+
//! - `QTTY_ERR_INVALID_VALUE` (-4): Invalid value (reserved)
|
|
20
|
+
|
|
21
|
+
use crate::registry;
|
|
22
|
+
use crate::types::{
|
|
23
|
+
DimensionId, QttyDerivedQuantity, QttyQuantity, UnitId, QTTY_ERR_BUFFER_TOO_SMALL,
|
|
24
|
+
QTTY_ERR_INCOMPATIBLE_DIM, QTTY_ERR_INVALID_VALUE, QTTY_ERR_NULL_OUT, QTTY_ERR_UNKNOWN_UNIT,
|
|
25
|
+
QTTY_FMT_LOWER_EXP, QTTY_FMT_UPPER_EXP, QTTY_OK,
|
|
26
|
+
};
|
|
27
|
+
use core::ffi::c_char;
|
|
28
|
+
use std::ffi::{CStr, CString};
|
|
29
|
+
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// Helper macro to catch panics
|
|
32
|
+
// =============================================================================
|
|
33
|
+
|
|
34
|
+
/// Catches any panic and returns an error code instead of unwinding across FFI.
|
|
35
|
+
macro_rules! catch_panic {
|
|
36
|
+
($default:expr, $body:expr) => {{
|
|
37
|
+
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| $body)) {
|
|
38
|
+
Ok(result) => result,
|
|
39
|
+
Err(_) => $default,
|
|
40
|
+
}
|
|
41
|
+
}};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// Unit Validation / Info Functions
|
|
46
|
+
// =============================================================================
|
|
47
|
+
|
|
48
|
+
/// Checks if a unit ID is valid (recognized by the registry).
|
|
49
|
+
///
|
|
50
|
+
/// # Arguments
|
|
51
|
+
///
|
|
52
|
+
/// * `unit` - The unit ID to validate
|
|
53
|
+
///
|
|
54
|
+
/// # Returns
|
|
55
|
+
///
|
|
56
|
+
/// `true` if the unit is valid, `false` otherwise.
|
|
57
|
+
///
|
|
58
|
+
/// # Safety
|
|
59
|
+
///
|
|
60
|
+
/// This function is safe to call from any context.
|
|
61
|
+
#[no_mangle]
|
|
62
|
+
pub extern "C" fn qtty_unit_is_valid(unit: UnitId) -> bool {
|
|
63
|
+
catch_panic!(false, registry::meta(unit).is_some())
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Gets the dimension of a unit.
|
|
67
|
+
///
|
|
68
|
+
/// # Arguments
|
|
69
|
+
///
|
|
70
|
+
/// * `unit` - The unit ID to query
|
|
71
|
+
/// * `out` - Pointer to store the dimension ID
|
|
72
|
+
///
|
|
73
|
+
/// # Returns
|
|
74
|
+
///
|
|
75
|
+
/// * `QTTY_OK` on success
|
|
76
|
+
/// * `QTTY_ERR_NULL_OUT` if `out` is null
|
|
77
|
+
/// * `QTTY_ERR_UNKNOWN_UNIT` if the unit is not recognized
|
|
78
|
+
///
|
|
79
|
+
/// # Safety
|
|
80
|
+
///
|
|
81
|
+
/// The caller must ensure that `out` points to valid, writable memory for a `DimensionId`,
|
|
82
|
+
/// or is null (in which case an error is returned).
|
|
83
|
+
#[no_mangle]
|
|
84
|
+
pub unsafe extern "C" fn qtty_unit_dimension(unit: UnitId, out: *mut DimensionId) -> i32 {
|
|
85
|
+
catch_panic!(QTTY_ERR_UNKNOWN_UNIT, {
|
|
86
|
+
if out.is_null() {
|
|
87
|
+
return QTTY_ERR_NULL_OUT;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
match registry::dimension(unit) {
|
|
91
|
+
Some(dim) => {
|
|
92
|
+
// SAFETY: We checked that `out` is not null
|
|
93
|
+
unsafe { *out = dim };
|
|
94
|
+
QTTY_OK
|
|
95
|
+
}
|
|
96
|
+
None => QTTY_ERR_UNKNOWN_UNIT,
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Checks if two units are compatible (same dimension).
|
|
102
|
+
///
|
|
103
|
+
/// # Arguments
|
|
104
|
+
///
|
|
105
|
+
/// * `a` - First unit ID
|
|
106
|
+
/// * `b` - Second unit ID
|
|
107
|
+
/// * `out` - Pointer to store the result
|
|
108
|
+
///
|
|
109
|
+
/// # Returns
|
|
110
|
+
///
|
|
111
|
+
/// * `QTTY_OK` on success
|
|
112
|
+
/// * `QTTY_ERR_NULL_OUT` if `out` is null
|
|
113
|
+
/// * `QTTY_ERR_UNKNOWN_UNIT` if either unit is not recognized
|
|
114
|
+
///
|
|
115
|
+
/// # Safety
|
|
116
|
+
///
|
|
117
|
+
/// The caller must ensure that `out` points to valid, writable memory for a `bool`,
|
|
118
|
+
/// or is null (in which case an error is returned).
|
|
119
|
+
#[no_mangle]
|
|
120
|
+
pub unsafe extern "C" fn qtty_units_compatible(a: UnitId, b: UnitId, out: *mut bool) -> i32 {
|
|
121
|
+
catch_panic!(QTTY_ERR_UNKNOWN_UNIT, {
|
|
122
|
+
if out.is_null() {
|
|
123
|
+
return QTTY_ERR_NULL_OUT;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Validate both units exist
|
|
127
|
+
if registry::meta(a).is_none() || registry::meta(b).is_none() {
|
|
128
|
+
return QTTY_ERR_UNKNOWN_UNIT;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// SAFETY: We checked that `out` is not null
|
|
132
|
+
unsafe { *out = registry::compatible(a, b) };
|
|
133
|
+
QTTY_OK
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// =============================================================================
|
|
138
|
+
// Quantity Construction and Conversion Functions
|
|
139
|
+
// =============================================================================
|
|
140
|
+
|
|
141
|
+
/// Creates a new quantity with the given value and unit.
|
|
142
|
+
///
|
|
143
|
+
/// # Arguments
|
|
144
|
+
///
|
|
145
|
+
/// * `value` - The numeric value
|
|
146
|
+
/// * `unit` - The unit ID
|
|
147
|
+
/// * `out` - Pointer to store the resulting quantity
|
|
148
|
+
///
|
|
149
|
+
/// # Returns
|
|
150
|
+
///
|
|
151
|
+
/// * `QTTY_OK` on success
|
|
152
|
+
/// * `QTTY_ERR_NULL_OUT` if `out` is null
|
|
153
|
+
/// * `QTTY_ERR_UNKNOWN_UNIT` if the unit is not recognized
|
|
154
|
+
///
|
|
155
|
+
/// # Safety
|
|
156
|
+
///
|
|
157
|
+
/// The caller must ensure that `out` points to valid, writable memory for a `QttyQuantity`,
|
|
158
|
+
/// or is null (in which case an error is returned).
|
|
159
|
+
#[no_mangle]
|
|
160
|
+
pub unsafe extern "C" fn qtty_quantity_make(
|
|
161
|
+
value: f64,
|
|
162
|
+
unit: UnitId,
|
|
163
|
+
out: *mut QttyQuantity,
|
|
164
|
+
) -> i32 {
|
|
165
|
+
catch_panic!(QTTY_ERR_UNKNOWN_UNIT, {
|
|
166
|
+
if out.is_null() {
|
|
167
|
+
return QTTY_ERR_NULL_OUT;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Validate unit exists
|
|
171
|
+
if registry::meta(unit).is_none() {
|
|
172
|
+
return QTTY_ERR_UNKNOWN_UNIT;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// SAFETY: We checked that `out` is not null
|
|
176
|
+
unsafe {
|
|
177
|
+
*out = QttyQuantity::new(value, unit);
|
|
178
|
+
}
|
|
179
|
+
QTTY_OK
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// Converts a quantity to a different unit.
|
|
184
|
+
///
|
|
185
|
+
/// # Arguments
|
|
186
|
+
///
|
|
187
|
+
/// * `src` - The source quantity
|
|
188
|
+
/// * `dst_unit` - The target unit ID
|
|
189
|
+
/// * `out` - Pointer to store the converted quantity
|
|
190
|
+
///
|
|
191
|
+
/// # Returns
|
|
192
|
+
///
|
|
193
|
+
/// * `QTTY_OK` on success
|
|
194
|
+
/// * `QTTY_ERR_NULL_OUT` if `out` is null
|
|
195
|
+
/// * `QTTY_ERR_UNKNOWN_UNIT` if either unit is not recognized
|
|
196
|
+
/// * `QTTY_ERR_INCOMPATIBLE_DIM` if units have different dimensions
|
|
197
|
+
///
|
|
198
|
+
/// # Safety
|
|
199
|
+
///
|
|
200
|
+
/// The caller must ensure that `out` points to valid, writable memory for a `QttyQuantity`,
|
|
201
|
+
/// or is null (in which case an error is returned).
|
|
202
|
+
#[no_mangle]
|
|
203
|
+
pub unsafe extern "C" fn qtty_quantity_convert(
|
|
204
|
+
src: QttyQuantity,
|
|
205
|
+
dst_unit: UnitId,
|
|
206
|
+
out: *mut QttyQuantity,
|
|
207
|
+
) -> i32 {
|
|
208
|
+
catch_panic!(QTTY_ERR_UNKNOWN_UNIT, {
|
|
209
|
+
if out.is_null() {
|
|
210
|
+
return QTTY_ERR_NULL_OUT;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
match registry::convert_value(src.value, src.unit, dst_unit) {
|
|
214
|
+
Ok(converted_value) => {
|
|
215
|
+
// SAFETY: We checked that `out` is not null
|
|
216
|
+
unsafe {
|
|
217
|
+
*out = QttyQuantity::new(converted_value, dst_unit);
|
|
218
|
+
}
|
|
219
|
+
QTTY_OK
|
|
220
|
+
}
|
|
221
|
+
Err(code) => code,
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/// Converts a value from one unit to another.
|
|
227
|
+
///
|
|
228
|
+
/// This is a convenience function that operates on raw values instead of `QttyQuantity` structs.
|
|
229
|
+
///
|
|
230
|
+
/// # Arguments
|
|
231
|
+
///
|
|
232
|
+
/// * `value` - The numeric value to convert
|
|
233
|
+
/// * `src_unit` - The source unit ID
|
|
234
|
+
/// * `dst_unit` - The target unit ID
|
|
235
|
+
/// * `out_value` - Pointer to store the converted value
|
|
236
|
+
///
|
|
237
|
+
/// # Returns
|
|
238
|
+
///
|
|
239
|
+
/// * `QTTY_OK` on success
|
|
240
|
+
/// * `QTTY_ERR_NULL_OUT` if `out_value` is null
|
|
241
|
+
/// * `QTTY_ERR_UNKNOWN_UNIT` if either unit is not recognized
|
|
242
|
+
/// * `QTTY_ERR_INCOMPATIBLE_DIM` if units have different dimensions
|
|
243
|
+
///
|
|
244
|
+
/// # Safety
|
|
245
|
+
///
|
|
246
|
+
/// The caller must ensure that `out_value` points to valid, writable memory for an `f64`,
|
|
247
|
+
/// or is null (in which case an error is returned).
|
|
248
|
+
#[no_mangle]
|
|
249
|
+
pub unsafe extern "C" fn qtty_quantity_convert_value(
|
|
250
|
+
value: f64,
|
|
251
|
+
src_unit: UnitId,
|
|
252
|
+
dst_unit: UnitId,
|
|
253
|
+
out_value: *mut f64,
|
|
254
|
+
) -> i32 {
|
|
255
|
+
catch_panic!(QTTY_ERR_UNKNOWN_UNIT, {
|
|
256
|
+
if out_value.is_null() {
|
|
257
|
+
return QTTY_ERR_NULL_OUT;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
match registry::convert_value(value, src_unit, dst_unit) {
|
|
261
|
+
Ok(converted) => {
|
|
262
|
+
// SAFETY: We checked that `out_value` is not null
|
|
263
|
+
unsafe {
|
|
264
|
+
*out_value = converted;
|
|
265
|
+
}
|
|
266
|
+
QTTY_OK
|
|
267
|
+
}
|
|
268
|
+
Err(code) => code,
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/// Gets the name of a unit as a NUL-terminated C string.
|
|
274
|
+
///
|
|
275
|
+
/// # Arguments
|
|
276
|
+
///
|
|
277
|
+
/// * `unit` - The unit ID to query
|
|
278
|
+
///
|
|
279
|
+
/// # Returns
|
|
280
|
+
///
|
|
281
|
+
/// A pointer to a static, NUL-terminated C string with the unit name,
|
|
282
|
+
/// or a null pointer if the unit is not recognized.
|
|
283
|
+
///
|
|
284
|
+
/// # Safety
|
|
285
|
+
///
|
|
286
|
+
/// The returned pointer points to static memory and is valid for the lifetime
|
|
287
|
+
/// of the program. The caller must not attempt to free or modify the returned string.
|
|
288
|
+
#[no_mangle]
|
|
289
|
+
pub extern "C" fn qtty_unit_name(unit: UnitId) -> *const c_char {
|
|
290
|
+
catch_panic!(core::ptr::null(), {
|
|
291
|
+
if registry::meta(unit).is_some() {
|
|
292
|
+
unit.name_cstr()
|
|
293
|
+
} else {
|
|
294
|
+
core::ptr::null()
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// =============================================================================
|
|
300
|
+
// String Formatting
|
|
301
|
+
// =============================================================================
|
|
302
|
+
|
|
303
|
+
/// Formats a quantity as a human-readable string into a caller-provided buffer.
|
|
304
|
+
///
|
|
305
|
+
/// Produces a string like `"1234.57 m"`, `"1.23e3 km"`, or `"1.23E3 km"` depending
|
|
306
|
+
/// on the `flags` parameter. The precision and format type mirror Rust's `{:.2}`,
|
|
307
|
+
/// `{:.4e}`, and `{:.4E}` format annotations, allowing callers to pass the same
|
|
308
|
+
/// format parameters that the Rust `Display`, `LowerExp`, and `UpperExp` trait impls
|
|
309
|
+
/// use internally.
|
|
310
|
+
///
|
|
311
|
+
/// # Arguments
|
|
312
|
+
///
|
|
313
|
+
/// * `qty` - The quantity (`value + unit`) to format.
|
|
314
|
+
/// * `precision` - Number of decimal digits after the point. Pass `-1` for the
|
|
315
|
+
/// default precision (shortest exact representation for floats).
|
|
316
|
+
/// * `flags` - Selects the notation:
|
|
317
|
+
/// - `QTTY_FMT_DEFAULT` (0): decimal notation, e.g. `"1234.568 m"`
|
|
318
|
+
/// - `QTTY_FMT_LOWER_EXP` (1): scientific with lowercase `e`, e.g. `"1.235e3 m"`
|
|
319
|
+
/// - `QTTY_FMT_UPPER_EXP` (2): scientific with uppercase `E`, e.g. `"1.235E3 m"`
|
|
320
|
+
/// * `buf` - Caller-allocated output buffer (must be non-null).
|
|
321
|
+
/// * `buf_len` - Size of `buf` in bytes (must include space for the NUL terminator).
|
|
322
|
+
///
|
|
323
|
+
/// # Returns
|
|
324
|
+
///
|
|
325
|
+
/// * Non-negative: number of bytes written, **excluding** the NUL terminator.
|
|
326
|
+
/// * `QTTY_ERR_NULL_OUT` if `buf` is null.
|
|
327
|
+
/// * `QTTY_ERR_UNKNOWN_UNIT` if `qty.unit` is not a recognized unit ID.
|
|
328
|
+
/// * `QTTY_ERR_BUFFER_TOO_SMALL` if `buf_len` is too small; the formatted string
|
|
329
|
+
/// (including the NUL terminator) requires `-return_value` bytes.
|
|
330
|
+
///
|
|
331
|
+
/// # Safety
|
|
332
|
+
///
|
|
333
|
+
/// The caller must ensure that `buf` points to a writable allocation of at least
|
|
334
|
+
/// `buf_len` bytes. The written string is always NUL-terminated on success.
|
|
335
|
+
#[no_mangle]
|
|
336
|
+
pub unsafe extern "C" fn qtty_quantity_format(
|
|
337
|
+
qty: QttyQuantity,
|
|
338
|
+
precision: i32,
|
|
339
|
+
flags: u32,
|
|
340
|
+
buf: *mut c_char,
|
|
341
|
+
buf_len: usize,
|
|
342
|
+
) -> i32 {
|
|
343
|
+
catch_panic!(QTTY_ERR_UNKNOWN_UNIT, {
|
|
344
|
+
if buf.is_null() || buf_len == 0 {
|
|
345
|
+
return QTTY_ERR_NULL_OUT;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if crate::registry::meta(qty.unit).is_none() {
|
|
349
|
+
return QTTY_ERR_UNKNOWN_UNIT;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
let symbol = qty.unit.symbol();
|
|
353
|
+
let formatted = match flags {
|
|
354
|
+
QTTY_FMT_LOWER_EXP => {
|
|
355
|
+
if precision >= 0 {
|
|
356
|
+
format!(
|
|
357
|
+
"{:.prec$e} {}",
|
|
358
|
+
qty.value,
|
|
359
|
+
symbol,
|
|
360
|
+
prec = precision as usize
|
|
361
|
+
)
|
|
362
|
+
} else {
|
|
363
|
+
format!("{:e} {}", qty.value, symbol)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
QTTY_FMT_UPPER_EXP => {
|
|
367
|
+
if precision >= 0 {
|
|
368
|
+
format!(
|
|
369
|
+
"{:.prec$E} {}",
|
|
370
|
+
qty.value,
|
|
371
|
+
symbol,
|
|
372
|
+
prec = precision as usize
|
|
373
|
+
)
|
|
374
|
+
} else {
|
|
375
|
+
format!("{:E} {}", qty.value, symbol)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// QTTY_FMT_DEFAULT or any unrecognised flag → decimal notation
|
|
379
|
+
_ => {
|
|
380
|
+
if precision >= 0 {
|
|
381
|
+
format!("{:.prec$} {}", qty.value, symbol, prec = precision as usize)
|
|
382
|
+
} else {
|
|
383
|
+
format!("{} {}", qty.value, symbol)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
let bytes = formatted.as_bytes();
|
|
389
|
+
let needed = bytes.len() + 1; // +1 for NUL terminator
|
|
390
|
+
|
|
391
|
+
if buf_len < needed {
|
|
392
|
+
return QTTY_ERR_BUFFER_TOO_SMALL;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// SAFETY: buf is non-null (checked above) and buf_len >= needed
|
|
396
|
+
unsafe {
|
|
397
|
+
core::ptr::copy_nonoverlapping(bytes.as_ptr() as *const c_char, buf, bytes.len());
|
|
398
|
+
*buf.add(bytes.len()) = 0; // NUL terminator
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
bytes.len() as i32
|
|
402
|
+
})
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// JSON Serialization / Deserialization via serde_json
|
|
406
|
+
//
|
|
407
|
+
// These helpers use serde for robust JSON serialization/deserialization.
|
|
408
|
+
// They produce/consume either a plain numeric value (e.g. "123.45") or an object
|
|
409
|
+
// with `value` and `unit` fields: {"value":123.45,"unit":"Meter"}
|
|
410
|
+
// =============================================================================
|
|
411
|
+
|
|
412
|
+
/// Frees a string previously allocated by one of the `qtty_*_to_json*` functions.
|
|
413
|
+
///
|
|
414
|
+
/// # Safety
|
|
415
|
+
///
|
|
416
|
+
/// The pointer must have been returned by a `qtty_*_to_json*` function and must
|
|
417
|
+
/// not have been freed previously. Passing a null pointer is safe (no-op).
|
|
418
|
+
#[no_mangle]
|
|
419
|
+
pub unsafe extern "C" fn qtty_string_free(s: *mut c_char) {
|
|
420
|
+
if s.is_null() {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
// Reclaim the CString to free the memory allocated by `into_raw`.
|
|
424
|
+
unsafe {
|
|
425
|
+
let _ = CString::from_raw(s);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/// Serializes a quantity's value as a plain JSON number string (e.g. "123.45").
|
|
430
|
+
///
|
|
431
|
+
/// # Safety
|
|
432
|
+
///
|
|
433
|
+
/// The caller must ensure that `out` points to valid, writable memory for a `*mut c_char`,
|
|
434
|
+
/// or is null (in which case an error is returned). The returned string must be freed
|
|
435
|
+
/// with [`qtty_string_free`].
|
|
436
|
+
#[no_mangle]
|
|
437
|
+
pub unsafe extern "C" fn qtty_quantity_to_json_value(
|
|
438
|
+
src: QttyQuantity,
|
|
439
|
+
out: *mut *mut c_char,
|
|
440
|
+
) -> i32 {
|
|
441
|
+
catch_panic!(QTTY_ERR_UNKNOWN_UNIT, {
|
|
442
|
+
if out.is_null() {
|
|
443
|
+
return QTTY_ERR_NULL_OUT;
|
|
444
|
+
}
|
|
445
|
+
let s = serde_json::to_string(&src.value).unwrap_or_default();
|
|
446
|
+
let c = CString::new(s).unwrap_or_default();
|
|
447
|
+
unsafe {
|
|
448
|
+
*out = c.into_raw();
|
|
449
|
+
}
|
|
450
|
+
QTTY_OK
|
|
451
|
+
})
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/// Deserializes a quantity from a plain JSON numeric string with an explicit unit.
|
|
455
|
+
///
|
|
456
|
+
/// # Safety
|
|
457
|
+
///
|
|
458
|
+
/// The caller must ensure that `json` points to a valid NUL-terminated C string,
|
|
459
|
+
/// and `out` points to valid, writable memory for a `QttyQuantity`.
|
|
460
|
+
#[no_mangle]
|
|
461
|
+
pub unsafe extern "C" fn qtty_quantity_from_json_value(
|
|
462
|
+
unit: UnitId,
|
|
463
|
+
json: *const c_char,
|
|
464
|
+
out: *mut QttyQuantity,
|
|
465
|
+
) -> i32 {
|
|
466
|
+
catch_panic!(QTTY_ERR_UNKNOWN_UNIT, {
|
|
467
|
+
if json.is_null() || out.is_null() {
|
|
468
|
+
return QTTY_ERR_NULL_OUT;
|
|
469
|
+
}
|
|
470
|
+
let cstr = unsafe { CStr::from_ptr(json) };
|
|
471
|
+
let s = match cstr.to_str() {
|
|
472
|
+
Ok(v) => v,
|
|
473
|
+
Err(_) => return QTTY_ERR_INVALID_VALUE,
|
|
474
|
+
};
|
|
475
|
+
let v: f64 = match serde_json::from_str(s) {
|
|
476
|
+
Ok(v) => v,
|
|
477
|
+
Err(_) => return QTTY_ERR_INVALID_VALUE,
|
|
478
|
+
};
|
|
479
|
+
if registry::meta(unit).is_none() {
|
|
480
|
+
return QTTY_ERR_UNKNOWN_UNIT;
|
|
481
|
+
}
|
|
482
|
+
unsafe {
|
|
483
|
+
*out = QttyQuantity::new(v, unit);
|
|
484
|
+
}
|
|
485
|
+
QTTY_OK
|
|
486
|
+
})
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/// Serializes a quantity to a full JSON object: `{"value":123.45,"unit":"Meter"}`.
|
|
490
|
+
///
|
|
491
|
+
/// # Safety
|
|
492
|
+
///
|
|
493
|
+
/// The caller must ensure that `out` points to valid, writable memory for a `*mut c_char`,
|
|
494
|
+
/// or is null (in which case an error is returned). The returned string must be freed
|
|
495
|
+
/// with [`qtty_string_free`].
|
|
496
|
+
#[no_mangle]
|
|
497
|
+
pub unsafe extern "C" fn qtty_quantity_to_json(src: QttyQuantity, out: *mut *mut c_char) -> i32 {
|
|
498
|
+
catch_panic!(QTTY_ERR_UNKNOWN_UNIT, {
|
|
499
|
+
if out.is_null() {
|
|
500
|
+
return QTTY_ERR_NULL_OUT;
|
|
501
|
+
}
|
|
502
|
+
let s = match serde_json::to_string(&src) {
|
|
503
|
+
Ok(s) => s,
|
|
504
|
+
Err(_) => return QTTY_ERR_INVALID_VALUE,
|
|
505
|
+
};
|
|
506
|
+
let c = CString::new(s).unwrap_or_default();
|
|
507
|
+
unsafe {
|
|
508
|
+
*out = c.into_raw();
|
|
509
|
+
}
|
|
510
|
+
QTTY_OK
|
|
511
|
+
})
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/// Deserializes a quantity from a JSON object: `{"value":123.45,"unit":"Meter"}`.
|
|
515
|
+
///
|
|
516
|
+
/// # Safety
|
|
517
|
+
///
|
|
518
|
+
/// The caller must ensure that `json` points to a valid NUL-terminated C string,
|
|
519
|
+
/// and `out` points to valid, writable memory for a `QttyQuantity`.
|
|
520
|
+
#[no_mangle]
|
|
521
|
+
pub unsafe extern "C" fn qtty_quantity_from_json(
|
|
522
|
+
json: *const c_char,
|
|
523
|
+
out: *mut QttyQuantity,
|
|
524
|
+
) -> i32 {
|
|
525
|
+
catch_panic!(QTTY_ERR_UNKNOWN_UNIT, {
|
|
526
|
+
if json.is_null() || out.is_null() {
|
|
527
|
+
return QTTY_ERR_NULL_OUT;
|
|
528
|
+
}
|
|
529
|
+
let cstr = unsafe { CStr::from_ptr(json) };
|
|
530
|
+
let s = match cstr.to_str() {
|
|
531
|
+
Ok(v) => v,
|
|
532
|
+
Err(_) => return QTTY_ERR_INVALID_VALUE,
|
|
533
|
+
};
|
|
534
|
+
let qty: QttyQuantity = match serde_json::from_str(s) {
|
|
535
|
+
Ok(v) => v,
|
|
536
|
+
Err(_) => return QTTY_ERR_INVALID_VALUE,
|
|
537
|
+
};
|
|
538
|
+
// Validate that the unit is known
|
|
539
|
+
if registry::meta(qty.unit).is_none() {
|
|
540
|
+
return QTTY_ERR_UNKNOWN_UNIT;
|
|
541
|
+
}
|
|
542
|
+
unsafe {
|
|
543
|
+
*out = qty;
|
|
544
|
+
}
|
|
545
|
+
QTTY_OK
|
|
546
|
+
})
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// =============================================================================
|
|
550
|
+
// Derived Quantity (Compound Unit) Functions
|
|
551
|
+
// =============================================================================
|
|
552
|
+
|
|
553
|
+
/// Creates a new derived quantity (compound unit like m/s).
|
|
554
|
+
///
|
|
555
|
+
/// # Safety
|
|
556
|
+
///
|
|
557
|
+
/// The caller must ensure that `out` points to valid, writable memory for a
|
|
558
|
+
/// `QttyDerivedQuantity`, or is null (in which case an error is returned).
|
|
559
|
+
#[no_mangle]
|
|
560
|
+
pub unsafe extern "C" fn qtty_derived_make(
|
|
561
|
+
value: f64,
|
|
562
|
+
numerator: UnitId,
|
|
563
|
+
denominator: UnitId,
|
|
564
|
+
out: *mut QttyDerivedQuantity,
|
|
565
|
+
) -> i32 {
|
|
566
|
+
catch_panic!(QTTY_ERR_UNKNOWN_UNIT, {
|
|
567
|
+
if out.is_null() {
|
|
568
|
+
return QTTY_ERR_NULL_OUT;
|
|
569
|
+
}
|
|
570
|
+
if registry::meta(numerator).is_none() || registry::meta(denominator).is_none() {
|
|
571
|
+
return QTTY_ERR_UNKNOWN_UNIT;
|
|
572
|
+
}
|
|
573
|
+
unsafe {
|
|
574
|
+
*out = QttyDerivedQuantity::new(value, numerator, denominator);
|
|
575
|
+
}
|
|
576
|
+
QTTY_OK
|
|
577
|
+
})
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/// Converts a derived quantity to different units.
|
|
581
|
+
///
|
|
582
|
+
/// The numerator and denominator are converted independently while preserving
|
|
583
|
+
/// the compound value. For example, 100 m/s → 360 km/h.
|
|
584
|
+
///
|
|
585
|
+
/// # Safety
|
|
586
|
+
///
|
|
587
|
+
/// The caller must ensure that `out` points to valid, writable memory for a
|
|
588
|
+
/// `QttyDerivedQuantity`, or is null (in which case an error is returned).
|
|
589
|
+
#[no_mangle]
|
|
590
|
+
pub unsafe extern "C" fn qtty_derived_convert(
|
|
591
|
+
src: QttyDerivedQuantity,
|
|
592
|
+
target_num: UnitId,
|
|
593
|
+
target_den: UnitId,
|
|
594
|
+
out: *mut QttyDerivedQuantity,
|
|
595
|
+
) -> i32 {
|
|
596
|
+
catch_panic!(QTTY_ERR_UNKNOWN_UNIT, {
|
|
597
|
+
if out.is_null() {
|
|
598
|
+
return QTTY_ERR_NULL_OUT;
|
|
599
|
+
}
|
|
600
|
+
match src.convert_to(target_num, target_den) {
|
|
601
|
+
Some(converted) => {
|
|
602
|
+
unsafe {
|
|
603
|
+
*out = converted;
|
|
604
|
+
}
|
|
605
|
+
QTTY_OK
|
|
606
|
+
}
|
|
607
|
+
None => {
|
|
608
|
+
// Determine a more specific error code
|
|
609
|
+
if registry::meta(src.numerator).is_none()
|
|
610
|
+
|| registry::meta(src.denominator).is_none()
|
|
611
|
+
|| registry::meta(target_num).is_none()
|
|
612
|
+
|| registry::meta(target_den).is_none()
|
|
613
|
+
{
|
|
614
|
+
QTTY_ERR_UNKNOWN_UNIT
|
|
615
|
+
} else {
|
|
616
|
+
QTTY_ERR_INCOMPATIBLE_DIM
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
})
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/// Serializes a derived quantity to a JSON object.
|
|
624
|
+
///
|
|
625
|
+
/// # Safety
|
|
626
|
+
///
|
|
627
|
+
/// The caller must ensure that `out` points to valid, writable memory for a `*mut c_char`.
|
|
628
|
+
/// The returned string must be freed with [`qtty_string_free`].
|
|
629
|
+
#[no_mangle]
|
|
630
|
+
pub unsafe extern "C" fn qtty_derived_to_json(
|
|
631
|
+
src: QttyDerivedQuantity,
|
|
632
|
+
out: *mut *mut c_char,
|
|
633
|
+
) -> i32 {
|
|
634
|
+
catch_panic!(QTTY_ERR_UNKNOWN_UNIT, {
|
|
635
|
+
if out.is_null() {
|
|
636
|
+
return QTTY_ERR_NULL_OUT;
|
|
637
|
+
}
|
|
638
|
+
let s = match serde_json::to_string(&src) {
|
|
639
|
+
Ok(s) => s,
|
|
640
|
+
Err(_) => return QTTY_ERR_INVALID_VALUE,
|
|
641
|
+
};
|
|
642
|
+
let c = CString::new(s).unwrap_or_default();
|
|
643
|
+
unsafe {
|
|
644
|
+
*out = c.into_raw();
|
|
645
|
+
}
|
|
646
|
+
QTTY_OK
|
|
647
|
+
})
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/// Deserializes a derived quantity from a JSON object.
|
|
651
|
+
///
|
|
652
|
+
/// # Safety
|
|
653
|
+
///
|
|
654
|
+
/// The caller must ensure that `json` points to a valid NUL-terminated C string,
|
|
655
|
+
/// and `out` points to valid, writable memory for a `QttyDerivedQuantity`.
|
|
656
|
+
#[no_mangle]
|
|
657
|
+
pub unsafe extern "C" fn qtty_derived_from_json(
|
|
658
|
+
json: *const c_char,
|
|
659
|
+
out: *mut QttyDerivedQuantity,
|
|
660
|
+
) -> i32 {
|
|
661
|
+
catch_panic!(QTTY_ERR_UNKNOWN_UNIT, {
|
|
662
|
+
if json.is_null() || out.is_null() {
|
|
663
|
+
return QTTY_ERR_NULL_OUT;
|
|
664
|
+
}
|
|
665
|
+
let cstr = unsafe { CStr::from_ptr(json) };
|
|
666
|
+
let s = match cstr.to_str() {
|
|
667
|
+
Ok(v) => v,
|
|
668
|
+
Err(_) => return QTTY_ERR_INVALID_VALUE,
|
|
669
|
+
};
|
|
670
|
+
let qty: QttyDerivedQuantity = match serde_json::from_str(s) {
|
|
671
|
+
Ok(v) => v,
|
|
672
|
+
Err(_) => return QTTY_ERR_INVALID_VALUE,
|
|
673
|
+
};
|
|
674
|
+
if registry::meta(qty.numerator).is_none() || registry::meta(qty.denominator).is_none() {
|
|
675
|
+
return QTTY_ERR_UNKNOWN_UNIT;
|
|
676
|
+
}
|
|
677
|
+
unsafe {
|
|
678
|
+
*out = qty;
|
|
679
|
+
}
|
|
680
|
+
QTTY_OK
|
|
681
|
+
})
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// =============================================================================
|
|
685
|
+
// Version Info
|
|
686
|
+
// =============================================================================
|
|
687
|
+
|
|
688
|
+
/// Returns the FFI ABI version.
|
|
689
|
+
///
|
|
690
|
+
/// This can be used by consumers to verify compatibility. The version is
|
|
691
|
+
/// incremented when breaking changes are made to the ABI.
|
|
692
|
+
///
|
|
693
|
+
/// Current version: 1
|
|
694
|
+
#[no_mangle]
|
|
695
|
+
pub extern "C" fn qtty_ffi_version() -> u32 {
|
|
696
|
+
1
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
#[cfg(test)]
|
|
700
|
+
mod tests {
|
|
701
|
+
use super::*;
|
|
702
|
+
use crate::types::QTTY_FMT_DEFAULT;
|
|
703
|
+
use crate::QTTY_ERR_INCOMPATIBLE_DIM;
|
|
704
|
+
use approx::assert_relative_eq;
|
|
705
|
+
use core::f64::consts::PI;
|
|
706
|
+
|
|
707
|
+
#[test]
|
|
708
|
+
fn test_unit_is_valid() {
|
|
709
|
+
assert!(qtty_unit_is_valid(UnitId::Meter));
|
|
710
|
+
assert!(qtty_unit_is_valid(UnitId::Second));
|
|
711
|
+
assert!(qtty_unit_is_valid(UnitId::Radian));
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
#[test]
|
|
715
|
+
fn test_unit_dimension() {
|
|
716
|
+
let mut dim = DimensionId::Length;
|
|
717
|
+
|
|
718
|
+
let status = unsafe { qtty_unit_dimension(UnitId::Meter, &mut dim) };
|
|
719
|
+
assert_eq!(status, QTTY_OK);
|
|
720
|
+
assert_eq!(dim, DimensionId::Length);
|
|
721
|
+
|
|
722
|
+
let status = unsafe { qtty_unit_dimension(UnitId::Second, &mut dim) };
|
|
723
|
+
assert_eq!(status, QTTY_OK);
|
|
724
|
+
assert_eq!(dim, DimensionId::Time);
|
|
725
|
+
|
|
726
|
+
let status = unsafe { qtty_unit_dimension(UnitId::Radian, &mut dim) };
|
|
727
|
+
assert_eq!(status, QTTY_OK);
|
|
728
|
+
assert_eq!(dim, DimensionId::Angle);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
#[test]
|
|
732
|
+
fn test_unit_dimension_null_out() {
|
|
733
|
+
let status = unsafe { qtty_unit_dimension(UnitId::Meter, core::ptr::null_mut()) };
|
|
734
|
+
assert_eq!(status, QTTY_ERR_NULL_OUT);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
#[test]
|
|
738
|
+
fn test_units_compatible() {
|
|
739
|
+
let mut result = false;
|
|
740
|
+
|
|
741
|
+
let status =
|
|
742
|
+
unsafe { qtty_units_compatible(UnitId::Meter, UnitId::Kilometer, &mut result) };
|
|
743
|
+
assert_eq!(status, QTTY_OK);
|
|
744
|
+
assert!(result);
|
|
745
|
+
|
|
746
|
+
let status = unsafe { qtty_units_compatible(UnitId::Meter, UnitId::Second, &mut result) };
|
|
747
|
+
assert_eq!(status, QTTY_OK);
|
|
748
|
+
assert!(!result);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
#[test]
|
|
752
|
+
fn test_units_compatible_null_out() {
|
|
753
|
+
let status = unsafe {
|
|
754
|
+
qtty_units_compatible(UnitId::Meter, UnitId::Kilometer, core::ptr::null_mut())
|
|
755
|
+
};
|
|
756
|
+
assert_eq!(status, QTTY_ERR_NULL_OUT);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
#[test]
|
|
760
|
+
fn test_quantity_make() {
|
|
761
|
+
let mut q = QttyQuantity::default();
|
|
762
|
+
|
|
763
|
+
let status = unsafe { qtty_quantity_make(1000.0, UnitId::Meter, &mut q) };
|
|
764
|
+
assert_eq!(status, QTTY_OK);
|
|
765
|
+
assert_relative_eq!(q.value, 1000.0);
|
|
766
|
+
assert_eq!(q.unit, UnitId::Meter);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
#[test]
|
|
770
|
+
fn test_quantity_make_null_out() {
|
|
771
|
+
let status = unsafe { qtty_quantity_make(1000.0, UnitId::Meter, core::ptr::null_mut()) };
|
|
772
|
+
assert_eq!(status, QTTY_ERR_NULL_OUT);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
#[test]
|
|
776
|
+
fn test_quantity_convert_meters_to_kilometers() {
|
|
777
|
+
let src = QttyQuantity::new(1000.0, UnitId::Meter);
|
|
778
|
+
let mut dst = QttyQuantity::default();
|
|
779
|
+
|
|
780
|
+
let status = unsafe { qtty_quantity_convert(src, UnitId::Kilometer, &mut dst) };
|
|
781
|
+
assert_eq!(status, QTTY_OK);
|
|
782
|
+
assert_relative_eq!(dst.value, 1.0, epsilon = 1e-12);
|
|
783
|
+
assert_eq!(dst.unit, UnitId::Kilometer);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
#[test]
|
|
787
|
+
fn test_quantity_convert_seconds_to_hours() {
|
|
788
|
+
let src = QttyQuantity::new(3600.0, UnitId::Second);
|
|
789
|
+
let mut dst = QttyQuantity::default();
|
|
790
|
+
|
|
791
|
+
let status = unsafe { qtty_quantity_convert(src, UnitId::Hour, &mut dst) };
|
|
792
|
+
assert_eq!(status, QTTY_OK);
|
|
793
|
+
assert_relative_eq!(dst.value, 1.0, epsilon = 1e-12);
|
|
794
|
+
assert_eq!(dst.unit, UnitId::Hour);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
#[test]
|
|
798
|
+
fn test_quantity_convert_degrees_to_radians() {
|
|
799
|
+
let src = QttyQuantity::new(180.0, UnitId::Degree);
|
|
800
|
+
let mut dst = QttyQuantity::default();
|
|
801
|
+
|
|
802
|
+
let status = unsafe { qtty_quantity_convert(src, UnitId::Radian, &mut dst) };
|
|
803
|
+
assert_eq!(status, QTTY_OK);
|
|
804
|
+
assert_relative_eq!(dst.value, PI, epsilon = 1e-12);
|
|
805
|
+
assert_eq!(dst.unit, UnitId::Radian);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
#[test]
|
|
809
|
+
fn test_quantity_convert_incompatible() {
|
|
810
|
+
let src = QttyQuantity::new(100.0, UnitId::Meter);
|
|
811
|
+
let mut dst = QttyQuantity::default();
|
|
812
|
+
|
|
813
|
+
let status = unsafe { qtty_quantity_convert(src, UnitId::Second, &mut dst) };
|
|
814
|
+
assert_eq!(status, QTTY_ERR_INCOMPATIBLE_DIM);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
#[test]
|
|
818
|
+
fn test_quantity_convert_null_out() {
|
|
819
|
+
let src = QttyQuantity::new(1000.0, UnitId::Meter);
|
|
820
|
+
|
|
821
|
+
let status =
|
|
822
|
+
unsafe { qtty_quantity_convert(src, UnitId::Kilometer, core::ptr::null_mut()) };
|
|
823
|
+
assert_eq!(status, QTTY_ERR_NULL_OUT);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
#[test]
|
|
827
|
+
fn test_quantity_convert_value() {
|
|
828
|
+
let mut out = 0.0;
|
|
829
|
+
|
|
830
|
+
let status = unsafe {
|
|
831
|
+
qtty_quantity_convert_value(1000.0, UnitId::Meter, UnitId::Kilometer, &mut out)
|
|
832
|
+
};
|
|
833
|
+
assert_eq!(status, QTTY_OK);
|
|
834
|
+
assert_relative_eq!(out, 1.0, epsilon = 1e-12);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
#[test]
|
|
838
|
+
fn test_quantity_convert_value_null_out() {
|
|
839
|
+
let status = unsafe {
|
|
840
|
+
qtty_quantity_convert_value(
|
|
841
|
+
1000.0,
|
|
842
|
+
UnitId::Meter,
|
|
843
|
+
UnitId::Kilometer,
|
|
844
|
+
core::ptr::null_mut(),
|
|
845
|
+
)
|
|
846
|
+
};
|
|
847
|
+
assert_eq!(status, QTTY_ERR_NULL_OUT);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
#[test]
|
|
851
|
+
fn test_unit_name() {
|
|
852
|
+
let name_ptr = qtty_unit_name(UnitId::Meter);
|
|
853
|
+
assert!(!name_ptr.is_null());
|
|
854
|
+
|
|
855
|
+
// SAFETY: We verified the pointer is not null and points to static memory
|
|
856
|
+
let name = unsafe { std::ffi::CStr::from_ptr(name_ptr) };
|
|
857
|
+
assert_eq!(name.to_str().unwrap(), "Meter");
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
#[test]
|
|
861
|
+
fn test_unit_name_all_dimensions() {
|
|
862
|
+
// Each of: length, time, angle, mass, power
|
|
863
|
+
for unit in [
|
|
864
|
+
UnitId::Kilometer,
|
|
865
|
+
UnitId::Hour,
|
|
866
|
+
UnitId::Degree,
|
|
867
|
+
UnitId::Kilogram,
|
|
868
|
+
UnitId::Watt,
|
|
869
|
+
] {
|
|
870
|
+
let ptr = qtty_unit_name(unit);
|
|
871
|
+
assert!(
|
|
872
|
+
!ptr.is_null(),
|
|
873
|
+
"unit_name should not be null for {:?}",
|
|
874
|
+
unit
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
#[test]
|
|
880
|
+
fn test_quantity_convert_value_incompatible() {
|
|
881
|
+
let mut out = 0.0;
|
|
882
|
+
let status =
|
|
883
|
+
unsafe { qtty_quantity_convert_value(1.0, UnitId::Meter, UnitId::Second, &mut out) };
|
|
884
|
+
assert_eq!(status, QTTY_ERR_INCOMPATIBLE_DIM);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// ─── qtty_string_free ────────────────────────────────────────────────────
|
|
888
|
+
|
|
889
|
+
#[test]
|
|
890
|
+
fn test_string_free_null_is_noop() {
|
|
891
|
+
// Must not crash
|
|
892
|
+
unsafe { qtty_string_free(std::ptr::null_mut()) };
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
#[test]
|
|
896
|
+
fn test_string_free_valid_ptr() {
|
|
897
|
+
// Allocate a string via to_json_value then free it
|
|
898
|
+
let src = QttyQuantity::new(1.0, UnitId::Meter);
|
|
899
|
+
let mut ptr: *mut std::ffi::c_char = std::ptr::null_mut();
|
|
900
|
+
let status = unsafe { qtty_quantity_to_json_value(src, &mut ptr) };
|
|
901
|
+
assert_eq!(status, QTTY_OK);
|
|
902
|
+
assert!(!ptr.is_null());
|
|
903
|
+
unsafe { qtty_string_free(ptr) }; // must not crash or leak
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// ─── qtty_quantity_to_json_value / qtty_quantity_from_json_value ─────────
|
|
907
|
+
|
|
908
|
+
#[test]
|
|
909
|
+
fn test_quantity_to_json_value_success() {
|
|
910
|
+
let src = QttyQuantity::new(42.5, UnitId::Meter);
|
|
911
|
+
let mut ptr: *mut std::ffi::c_char = std::ptr::null_mut();
|
|
912
|
+
let status = unsafe { qtty_quantity_to_json_value(src, &mut ptr) };
|
|
913
|
+
assert_eq!(status, QTTY_OK);
|
|
914
|
+
assert!(!ptr.is_null());
|
|
915
|
+
let s = unsafe { std::ffi::CStr::from_ptr(ptr).to_str().unwrap() };
|
|
916
|
+
assert_eq!(s, "42.5");
|
|
917
|
+
unsafe { qtty_string_free(ptr) };
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
#[test]
|
|
921
|
+
fn test_quantity_to_json_value_null_out() {
|
|
922
|
+
let src = QttyQuantity::new(1.0, UnitId::Meter);
|
|
923
|
+
let status = unsafe { qtty_quantity_to_json_value(src, std::ptr::null_mut()) };
|
|
924
|
+
assert_eq!(status, QTTY_ERR_NULL_OUT);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
#[test]
|
|
928
|
+
fn test_quantity_from_json_value_success() {
|
|
929
|
+
let json = std::ffi::CString::new("99.0").unwrap();
|
|
930
|
+
let mut out = QttyQuantity::default();
|
|
931
|
+
let status =
|
|
932
|
+
unsafe { qtty_quantity_from_json_value(UnitId::Second, json.as_ptr(), &mut out) };
|
|
933
|
+
assert_eq!(status, QTTY_OK);
|
|
934
|
+
assert_relative_eq!(out.value, 99.0);
|
|
935
|
+
assert_eq!(out.unit, UnitId::Second);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
#[test]
|
|
939
|
+
fn test_quantity_from_json_value_null_json() {
|
|
940
|
+
let mut out = QttyQuantity::default();
|
|
941
|
+
let status =
|
|
942
|
+
unsafe { qtty_quantity_from_json_value(UnitId::Meter, std::ptr::null(), &mut out) };
|
|
943
|
+
assert_eq!(status, QTTY_ERR_NULL_OUT);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
#[test]
|
|
947
|
+
fn test_quantity_from_json_value_null_out() {
|
|
948
|
+
let json = std::ffi::CString::new("1.0").unwrap();
|
|
949
|
+
let status = unsafe {
|
|
950
|
+
qtty_quantity_from_json_value(UnitId::Meter, json.as_ptr(), std::ptr::null_mut())
|
|
951
|
+
};
|
|
952
|
+
assert_eq!(status, QTTY_ERR_NULL_OUT);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
#[test]
|
|
956
|
+
fn test_quantity_from_json_value_invalid_json() {
|
|
957
|
+
let json = std::ffi::CString::new("not_a_number").unwrap();
|
|
958
|
+
let mut out = QttyQuantity::default();
|
|
959
|
+
let status =
|
|
960
|
+
unsafe { qtty_quantity_from_json_value(UnitId::Meter, json.as_ptr(), &mut out) };
|
|
961
|
+
assert_eq!(status, QTTY_ERR_INVALID_VALUE);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
#[test]
|
|
965
|
+
fn test_quantity_json_value_roundtrip() {
|
|
966
|
+
let src = QttyQuantity::new(1234.567, UnitId::Kilometer);
|
|
967
|
+
let mut ptr: *mut std::ffi::c_char = std::ptr::null_mut();
|
|
968
|
+
unsafe { qtty_quantity_to_json_value(src, &mut ptr) };
|
|
969
|
+
let mut out = QttyQuantity::default();
|
|
970
|
+
let status = unsafe { qtty_quantity_from_json_value(UnitId::Kilometer, ptr, &mut out) };
|
|
971
|
+
unsafe { qtty_string_free(ptr) };
|
|
972
|
+
assert_eq!(status, QTTY_OK);
|
|
973
|
+
assert_relative_eq!(out.value, 1234.567, epsilon = 1e-9);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// ─── qtty_quantity_to_json / qtty_quantity_from_json ─────────────────────
|
|
977
|
+
|
|
978
|
+
#[test]
|
|
979
|
+
fn test_quantity_to_json_success() {
|
|
980
|
+
let src = QttyQuantity::new(1.0, UnitId::Hour);
|
|
981
|
+
let mut ptr: *mut std::ffi::c_char = std::ptr::null_mut();
|
|
982
|
+
let status = unsafe { qtty_quantity_to_json(src, &mut ptr) };
|
|
983
|
+
assert_eq!(status, QTTY_OK);
|
|
984
|
+
assert!(!ptr.is_null());
|
|
985
|
+
let s = unsafe { std::ffi::CStr::from_ptr(ptr).to_str().unwrap() };
|
|
986
|
+
// Must include "value" and unit name
|
|
987
|
+
assert!(s.contains("value"));
|
|
988
|
+
assert!(s.contains("Hour"));
|
|
989
|
+
unsafe { qtty_string_free(ptr) };
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
#[test]
|
|
993
|
+
fn test_quantity_to_json_null_out() {
|
|
994
|
+
let src = QttyQuantity::new(1.0, UnitId::Meter);
|
|
995
|
+
let status = unsafe { qtty_quantity_to_json(src, std::ptr::null_mut()) };
|
|
996
|
+
assert_eq!(status, QTTY_ERR_NULL_OUT);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
#[test]
|
|
1000
|
+
fn test_quantity_from_json_success() {
|
|
1001
|
+
// Serialize first to get correct format
|
|
1002
|
+
let src = QttyQuantity::new(500.0, UnitId::Kilogram);
|
|
1003
|
+
let mut ptr: *mut std::ffi::c_char = std::ptr::null_mut();
|
|
1004
|
+
unsafe { qtty_quantity_to_json(src, &mut ptr) };
|
|
1005
|
+
|
|
1006
|
+
let mut out = QttyQuantity::default();
|
|
1007
|
+
let status = unsafe { qtty_quantity_from_json(ptr, &mut out) };
|
|
1008
|
+
unsafe { qtty_string_free(ptr) };
|
|
1009
|
+
|
|
1010
|
+
assert_eq!(status, QTTY_OK);
|
|
1011
|
+
assert_relative_eq!(out.value, 500.0);
|
|
1012
|
+
assert_eq!(out.unit, UnitId::Kilogram);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
#[test]
|
|
1016
|
+
fn test_quantity_from_json_null_json() {
|
|
1017
|
+
let mut out = QttyQuantity::default();
|
|
1018
|
+
let status = unsafe { qtty_quantity_from_json(std::ptr::null(), &mut out) };
|
|
1019
|
+
assert_eq!(status, QTTY_ERR_NULL_OUT);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
#[test]
|
|
1023
|
+
fn test_quantity_from_json_null_out() {
|
|
1024
|
+
let json = std::ffi::CString::new(r#"{"value":1.0,"unit":"Meter"}"#).unwrap();
|
|
1025
|
+
let status = unsafe { qtty_quantity_from_json(json.as_ptr(), std::ptr::null_mut()) };
|
|
1026
|
+
assert_eq!(status, QTTY_ERR_NULL_OUT);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
#[test]
|
|
1030
|
+
fn test_quantity_from_json_invalid_json() {
|
|
1031
|
+
let json = std::ffi::CString::new("not valid json at all").unwrap();
|
|
1032
|
+
let mut out = QttyQuantity::default();
|
|
1033
|
+
let status = unsafe { qtty_quantity_from_json(json.as_ptr(), &mut out) };
|
|
1034
|
+
assert_eq!(status, QTTY_ERR_INVALID_VALUE);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
#[test]
|
|
1038
|
+
fn test_quantity_json_object_roundtrip() {
|
|
1039
|
+
let src = QttyQuantity::new(PI, UnitId::Radian);
|
|
1040
|
+
let mut ptr: *mut std::ffi::c_char = std::ptr::null_mut();
|
|
1041
|
+
unsafe { qtty_quantity_to_json(src, &mut ptr) };
|
|
1042
|
+
let mut out = QttyQuantity::default();
|
|
1043
|
+
let status = unsafe { qtty_quantity_from_json(ptr, &mut out) };
|
|
1044
|
+
unsafe { qtty_string_free(ptr) };
|
|
1045
|
+
assert_eq!(status, QTTY_OK);
|
|
1046
|
+
assert_relative_eq!(out.value, PI, epsilon = 1e-12);
|
|
1047
|
+
assert_eq!(out.unit, UnitId::Radian);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// ─── qtty_derived_make ───────────────────────────────────────────────────
|
|
1051
|
+
|
|
1052
|
+
#[test]
|
|
1053
|
+
fn test_derived_make_success() {
|
|
1054
|
+
let mut out = QttyDerivedQuantity::default();
|
|
1055
|
+
let status = unsafe { qtty_derived_make(100.0, UnitId::Meter, UnitId::Second, &mut out) };
|
|
1056
|
+
assert_eq!(status, QTTY_OK);
|
|
1057
|
+
assert_relative_eq!(out.value, 100.0);
|
|
1058
|
+
assert_eq!(out.numerator, UnitId::Meter);
|
|
1059
|
+
assert_eq!(out.denominator, UnitId::Second);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
#[test]
|
|
1063
|
+
fn test_derived_make_null_out() {
|
|
1064
|
+
let status =
|
|
1065
|
+
unsafe { qtty_derived_make(1.0, UnitId::Meter, UnitId::Second, std::ptr::null_mut()) };
|
|
1066
|
+
assert_eq!(status, QTTY_ERR_NULL_OUT);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// ─── qtty_derived_convert ────────────────────────────────────────────────
|
|
1070
|
+
|
|
1071
|
+
#[test]
|
|
1072
|
+
fn test_derived_convert_success() {
|
|
1073
|
+
// 100 m/s → 360 km/h
|
|
1074
|
+
let src = QttyDerivedQuantity::new(100.0, UnitId::Meter, UnitId::Second);
|
|
1075
|
+
let mut out = QttyDerivedQuantity::default();
|
|
1076
|
+
let status =
|
|
1077
|
+
unsafe { qtty_derived_convert(src, UnitId::Kilometer, UnitId::Hour, &mut out) };
|
|
1078
|
+
assert_eq!(status, QTTY_OK);
|
|
1079
|
+
assert_relative_eq!(out.value, 360.0, epsilon = 1e-9);
|
|
1080
|
+
assert_eq!(out.numerator, UnitId::Kilometer);
|
|
1081
|
+
assert_eq!(out.denominator, UnitId::Hour);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
#[test]
|
|
1085
|
+
fn test_derived_convert_null_out() {
|
|
1086
|
+
let src = QttyDerivedQuantity::new(1.0, UnitId::Meter, UnitId::Second);
|
|
1087
|
+
let status = unsafe {
|
|
1088
|
+
qtty_derived_convert(src, UnitId::Kilometer, UnitId::Hour, std::ptr::null_mut())
|
|
1089
|
+
};
|
|
1090
|
+
assert_eq!(status, QTTY_ERR_NULL_OUT);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
#[test]
|
|
1094
|
+
fn test_derived_convert_incompatible_dim() {
|
|
1095
|
+
// m/s → kg/h: incompatible numerator dimension
|
|
1096
|
+
let src = QttyDerivedQuantity::new(1.0, UnitId::Meter, UnitId::Second);
|
|
1097
|
+
let mut out = QttyDerivedQuantity::default();
|
|
1098
|
+
let status = unsafe { qtty_derived_convert(src, UnitId::Kilogram, UnitId::Hour, &mut out) };
|
|
1099
|
+
assert_eq!(status, QTTY_ERR_INCOMPATIBLE_DIM);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// ─── qtty_derived_to_json / qtty_derived_from_json ───────────────────────
|
|
1103
|
+
|
|
1104
|
+
#[test]
|
|
1105
|
+
fn test_derived_to_json_success() {
|
|
1106
|
+
let src = QttyDerivedQuantity::new(100.0, UnitId::Meter, UnitId::Second);
|
|
1107
|
+
let mut ptr: *mut std::ffi::c_char = std::ptr::null_mut();
|
|
1108
|
+
let status = unsafe { qtty_derived_to_json(src, &mut ptr) };
|
|
1109
|
+
assert_eq!(status, QTTY_OK);
|
|
1110
|
+
assert!(!ptr.is_null());
|
|
1111
|
+
let s = unsafe { std::ffi::CStr::from_ptr(ptr).to_str().unwrap() };
|
|
1112
|
+
assert!(s.contains("value"));
|
|
1113
|
+
assert!(s.contains("Meter"));
|
|
1114
|
+
assert!(s.contains("Second"));
|
|
1115
|
+
unsafe { qtty_string_free(ptr) };
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
#[test]
|
|
1119
|
+
fn test_derived_to_json_null_out() {
|
|
1120
|
+
let src = QttyDerivedQuantity::new(1.0, UnitId::Meter, UnitId::Second);
|
|
1121
|
+
let status = unsafe { qtty_derived_to_json(src, std::ptr::null_mut()) };
|
|
1122
|
+
assert_eq!(status, QTTY_ERR_NULL_OUT);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
#[test]
|
|
1126
|
+
fn test_derived_from_json_success() {
|
|
1127
|
+
// Roundtrip via to_json then from_json
|
|
1128
|
+
let src = QttyDerivedQuantity::new(360.0, UnitId::Kilometer, UnitId::Hour);
|
|
1129
|
+
let mut ptr: *mut std::ffi::c_char = std::ptr::null_mut();
|
|
1130
|
+
unsafe { qtty_derived_to_json(src, &mut ptr) };
|
|
1131
|
+
|
|
1132
|
+
let mut out = QttyDerivedQuantity::default();
|
|
1133
|
+
let status = unsafe { qtty_derived_from_json(ptr, &mut out) };
|
|
1134
|
+
unsafe { qtty_string_free(ptr) };
|
|
1135
|
+
|
|
1136
|
+
assert_eq!(status, QTTY_OK);
|
|
1137
|
+
assert_relative_eq!(out.value, 360.0);
|
|
1138
|
+
assert_eq!(out.numerator, UnitId::Kilometer);
|
|
1139
|
+
assert_eq!(out.denominator, UnitId::Hour);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
#[test]
|
|
1143
|
+
fn test_derived_from_json_null_json() {
|
|
1144
|
+
let mut out = QttyDerivedQuantity::default();
|
|
1145
|
+
let status = unsafe { qtty_derived_from_json(std::ptr::null(), &mut out) };
|
|
1146
|
+
assert_eq!(status, QTTY_ERR_NULL_OUT);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
#[test]
|
|
1150
|
+
fn test_derived_from_json_null_out() {
|
|
1151
|
+
let json =
|
|
1152
|
+
std::ffi::CString::new(r#"{"value":1.0,"numerator":"Meter","denominator":"Second"}"#)
|
|
1153
|
+
.unwrap();
|
|
1154
|
+
let status = unsafe { qtty_derived_from_json(json.as_ptr(), std::ptr::null_mut()) };
|
|
1155
|
+
assert_eq!(status, QTTY_ERR_NULL_OUT);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
#[test]
|
|
1159
|
+
fn test_derived_from_json_invalid_json() {
|
|
1160
|
+
let json = std::ffi::CString::new("not json").unwrap();
|
|
1161
|
+
let mut out = QttyDerivedQuantity::default();
|
|
1162
|
+
let status = unsafe { qtty_derived_from_json(json.as_ptr(), &mut out) };
|
|
1163
|
+
assert_eq!(status, QTTY_ERR_INVALID_VALUE);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
#[test]
|
|
1167
|
+
fn test_ffi_version() {
|
|
1168
|
+
assert_eq!(qtty_ffi_version(), 1);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// -------------------------------------------------------------------------
|
|
1172
|
+
// qtty_quantity_format tests
|
|
1173
|
+
// -------------------------------------------------------------------------
|
|
1174
|
+
|
|
1175
|
+
fn format_qty(qty: QttyQuantity, precision: i32, flags: u32) -> String {
|
|
1176
|
+
let mut buf = [0i8; 256];
|
|
1177
|
+
let result =
|
|
1178
|
+
unsafe { qtty_quantity_format(qty, precision, flags, buf.as_mut_ptr(), buf.len()) };
|
|
1179
|
+
assert!(result >= 0, "qtty_quantity_format returned error {result}");
|
|
1180
|
+
let c_str = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) };
|
|
1181
|
+
c_str.to_str().unwrap().to_owned()
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
#[test]
|
|
1185
|
+
fn test_format_default_no_precision() {
|
|
1186
|
+
let qty = QttyQuantity::new(1234.56789, UnitId::Second);
|
|
1187
|
+
let s = format_qty(qty, -1, QTTY_FMT_DEFAULT);
|
|
1188
|
+
assert_eq!(s, "1234.56789 s");
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
#[test]
|
|
1192
|
+
fn test_format_default_two_decimal_places() {
|
|
1193
|
+
let qty = QttyQuantity::new(1234.56789, UnitId::Second);
|
|
1194
|
+
let s = format_qty(qty, 2, QTTY_FMT_DEFAULT);
|
|
1195
|
+
assert_eq!(s, "1234.57 s");
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
#[test]
|
|
1199
|
+
fn test_format_lower_exp_no_precision() {
|
|
1200
|
+
let qty = QttyQuantity::new(1234.56789, UnitId::Second);
|
|
1201
|
+
let s = format_qty(qty, -1, QTTY_FMT_LOWER_EXP);
|
|
1202
|
+
assert_eq!(s, "1.23456789e3 s");
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
#[test]
|
|
1206
|
+
fn test_format_lower_exp_four_decimal_places() {
|
|
1207
|
+
let qty = QttyQuantity::new(1234.56789, UnitId::Second);
|
|
1208
|
+
let s = format_qty(qty, 4, QTTY_FMT_LOWER_EXP);
|
|
1209
|
+
assert_eq!(s, "1.2346e3 s");
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
#[test]
|
|
1213
|
+
fn test_format_upper_exp_four_decimal_places() {
|
|
1214
|
+
let qty = QttyQuantity::new(1234.56789, UnitId::Second);
|
|
1215
|
+
let s = format_qty(qty, 4, QTTY_FMT_UPPER_EXP);
|
|
1216
|
+
assert_eq!(s, "1.2346E3 s");
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
#[test]
|
|
1220
|
+
fn test_format_meters_default() {
|
|
1221
|
+
let qty = QttyQuantity::new(42.0, UnitId::Meter);
|
|
1222
|
+
let s = format_qty(qty, -1, QTTY_FMT_DEFAULT);
|
|
1223
|
+
assert_eq!(s, "42 m");
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
#[test]
|
|
1227
|
+
fn test_format_null_buf() {
|
|
1228
|
+
let qty = QttyQuantity::new(1.0, UnitId::Meter);
|
|
1229
|
+
let result =
|
|
1230
|
+
unsafe { qtty_quantity_format(qty, -1, QTTY_FMT_DEFAULT, core::ptr::null_mut(), 64) };
|
|
1231
|
+
assert_eq!(result, QTTY_ERR_NULL_OUT);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
#[test]
|
|
1235
|
+
fn test_format_zero_buf_len() {
|
|
1236
|
+
let qty = QttyQuantity::new(1.0, UnitId::Meter);
|
|
1237
|
+
let mut buf = [0i8; 4];
|
|
1238
|
+
let result =
|
|
1239
|
+
unsafe { qtty_quantity_format(qty, -1, QTTY_FMT_DEFAULT, buf.as_mut_ptr(), 0) };
|
|
1240
|
+
assert_eq!(result, QTTY_ERR_NULL_OUT);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
#[test]
|
|
1244
|
+
fn test_format_buffer_too_small() {
|
|
1245
|
+
let qty = QttyQuantity::new(1234.56789, UnitId::Second);
|
|
1246
|
+
let mut buf = [0i8; 4]; // way too small
|
|
1247
|
+
let result =
|
|
1248
|
+
unsafe { qtty_quantity_format(qty, 2, QTTY_FMT_DEFAULT, buf.as_mut_ptr(), buf.len()) };
|
|
1249
|
+
assert_eq!(result, QTTY_ERR_BUFFER_TOO_SMALL);
|
|
1250
|
+
}
|
|
1251
|
+
}
|