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.
Files changed (272) hide show
  1. package/.github/workflows/ci.yml +166 -0
  2. package/.gitmodules +9 -0
  3. package/CHANGELOG.md +26 -0
  4. package/LICENSE +661 -0
  5. package/README.md +138 -0
  6. package/package.json +12 -0
  7. package/qtty-js/.github/workflows/ci.yml +151 -0
  8. package/qtty-js/.gitmodules +3 -0
  9. package/qtty-js/CHANGELOG.md +31 -0
  10. package/qtty-js/LICENSE +661 -0
  11. package/qtty-js/README.md +132 -0
  12. package/qtty-js/package.json +20 -0
  13. package/qtty-js/qtty/.github/workflows/ci.yml +155 -0
  14. package/qtty-js/qtty/CHANGELOG.md +120 -0
  15. package/qtty-js/qtty/Cargo.lock +1462 -0
  16. package/qtty-js/qtty/Cargo.toml +12 -0
  17. package/qtty-js/qtty/LICENSE +661 -0
  18. package/qtty-js/qtty/README.md +9 -0
  19. package/qtty-js/qtty/qtty/Cargo.toml +41 -0
  20. package/qtty-js/qtty/qtty/README.md +8 -0
  21. package/qtty-js/qtty/qtty/examples/angles.rs +14 -0
  22. package/qtty-js/qtty/qtty/examples/astronomy.rs +17 -0
  23. package/qtty-js/qtty/qtty/examples/dimensional_arithmetic.rs +83 -0
  24. package/qtty-js/qtty/qtty/examples/python_integration.rs +61 -0
  25. package/qtty-js/qtty/qtty/examples/quickstart.rs +15 -0
  26. package/qtty-js/qtty/qtty/examples/ratios.rs +12 -0
  27. package/qtty-js/qtty/qtty/examples/serde_with_unit.rs +234 -0
  28. package/qtty-js/qtty/qtty/examples/serialization.rs +141 -0
  29. package/qtty-js/qtty/qtty/examples/serialization_advanced.rs +155 -0
  30. package/qtty-js/qtty/qtty/src/f32.rs +108 -0
  31. package/qtty-js/qtty/qtty/src/f64.rs +30 -0
  32. package/qtty-js/qtty/qtty/src/i128.rs +111 -0
  33. package/qtty-js/qtty/qtty/src/i16.rs +111 -0
  34. package/qtty-js/qtty/qtty/src/i32.rs +111 -0
  35. package/qtty-js/qtty/qtty/src/i64.rs +111 -0
  36. package/qtty-js/qtty/qtty/src/i8.rs +111 -0
  37. package/qtty-js/qtty/qtty/src/lib.rs +238 -0
  38. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std/Cargo.lock +83 -0
  39. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std/Cargo.toml +10 -0
  40. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std/src/lib.rs +7 -0
  41. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std-alloc/Cargo.lock +83 -0
  42. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std-alloc/Cargo.toml +10 -0
  43. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std-alloc/src/lib.rs +7 -0
  44. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-std/Cargo.lock +83 -0
  45. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-std/Cargo.toml +10 -0
  46. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-std/src/lib.rs +5 -0
  47. package/qtty-js/qtty/qtty/tests/integration_tests.rs +529 -0
  48. package/qtty-js/qtty/qtty/tests/qtty_vec_feature_matrix.rs +58 -0
  49. package/qtty-js/qtty/qtty-core/Cargo.toml +41 -0
  50. package/qtty-js/qtty/qtty-core/README.md +8 -0
  51. package/qtty-js/qtty/qtty-core/examples/diesel_integration.rs +145 -0
  52. package/qtty-js/qtty/qtty-core/examples/quantity_db_serde.rs +215 -0
  53. package/qtty-js/qtty/qtty-core/src/dimension.rs +249 -0
  54. package/qtty-js/qtty/qtty-core/src/feature_diesel.rs +318 -0
  55. package/qtty-js/qtty/qtty-core/src/feature_pyo3.rs +27 -0
  56. package/qtty-js/qtty/qtty-core/src/feature_serde.rs +203 -0
  57. package/qtty-js/qtty/qtty-core/src/feature_tiberius.rs +28 -0
  58. package/qtty-js/qtty/qtty-core/src/lib.rs +744 -0
  59. package/qtty-js/qtty/qtty-core/src/macros.rs +93 -0
  60. package/qtty-js/qtty/qtty-core/src/quantity.rs +810 -0
  61. package/qtty-js/qtty/qtty-core/src/scalar.rs +1742 -0
  62. package/qtty-js/qtty/qtty-core/src/unit.rs +332 -0
  63. package/qtty-js/qtty/qtty-core/src/units/angular.rs +1228 -0
  64. package/qtty-js/qtty/qtty-core/src/units/area.rs +243 -0
  65. package/qtty-js/qtty/qtty-core/src/units/frequency.rs +179 -0
  66. package/qtty-js/qtty/qtty-core/src/units/length.rs +1270 -0
  67. package/qtty-js/qtty/qtty-core/src/units/mass.rs +488 -0
  68. package/qtty-js/qtty/qtty-core/src/units/mod.rs +26 -0
  69. package/qtty-js/qtty/qtty-core/src/units/power.rs +324 -0
  70. package/qtty-js/qtty/qtty-core/src/units/time.rs +667 -0
  71. package/qtty-js/qtty/qtty-core/src/units/unitless.rs +212 -0
  72. package/qtty-js/qtty/qtty-core/src/units/velocity.rs +210 -0
  73. package/qtty-js/qtty/qtty-core/src/units/volume.rs +269 -0
  74. package/qtty-js/qtty/qtty-core/tests/core.rs +628 -0
  75. package/qtty-js/qtty/qtty-core/tests/diesel.rs +461 -0
  76. package/qtty-js/qtty/qtty-core/tests/integers.rs +632 -0
  77. package/qtty-js/qtty/qtty-core/tests/no_cross_unit_ops.rs +35 -0
  78. package/qtty-js/qtty/qtty-core/tests/pyo3.rs +334 -0
  79. package/qtty-js/qtty/qtty-core/tests/quantity_f32.rs +276 -0
  80. package/qtty-js/qtty/qtty-core/tests/scalar_decimal.rs +258 -0
  81. package/qtty-js/qtty/qtty-core/tests/scalar_f32.rs +286 -0
  82. package/qtty-js/qtty/qtty-core/tests/scalar_f64_real.rs +287 -0
  83. package/qtty-js/qtty/qtty-core/tests/scalar_rational.rs +260 -0
  84. package/qtty-js/qtty/qtty-core/tests/serde.rs +256 -0
  85. package/qtty-js/qtty/qtty-core/tests/tiberius.rs +208 -0
  86. package/qtty-js/qtty/qtty-derive/Cargo.toml +23 -0
  87. package/qtty-js/qtty/qtty-derive/README.md +8 -0
  88. package/qtty-js/qtty/qtty-derive/src/lib.rs +340 -0
  89. package/qtty-js/qtty/qtty-ffi/ARCHITECTURE.md +3 -0
  90. package/qtty-js/qtty/qtty-ffi/Cargo.toml +31 -0
  91. package/qtty-js/qtty/qtty-ffi/README.md +9 -0
  92. package/qtty-js/qtty/qtty-ffi/build.rs +326 -0
  93. package/qtty-js/qtty/qtty-ffi/cbindgen.toml +105 -0
  94. package/qtty-js/qtty/qtty-ffi/include/qtty_ffi.h +1126 -0
  95. package/qtty-js/qtty/qtty-ffi/src/ffi.rs +1251 -0
  96. package/qtty-js/qtty/qtty-ffi/src/ffi_serde.rs +294 -0
  97. package/qtty-js/qtty/qtty-ffi/src/helpers.rs +310 -0
  98. package/qtty-js/qtty/qtty-ffi/src/lib.rs +229 -0
  99. package/qtty-js/qtty/qtty-ffi/src/macros.rs +121 -0
  100. package/qtty-js/qtty/qtty-ffi/src/registry.rs +274 -0
  101. package/qtty-js/qtty/qtty-ffi/src/types.rs +620 -0
  102. package/qtty-js/qtty/qtty-ffi/tests/integration_tests.rs +842 -0
  103. package/qtty-js/qtty/qtty-ffi/units.csv +156 -0
  104. package/qtty-js/qtty/qtty-ffi/units.csv.md +3 -0
  105. package/qtty-js/qtty-node/.prettierignore +6 -0
  106. package/qtty-js/qtty-node/.prettierrc.json +6 -0
  107. package/qtty-js/qtty-node/README.md +250 -0
  108. package/qtty-js/qtty-node/c8.config.json +11 -0
  109. package/qtty-js/qtty-node/eslint.config.js +31 -0
  110. package/qtty-js/qtty-node/examples/arithmetic.mjs +64 -0
  111. package/qtty-js/qtty-node/examples/astronomy.mjs +90 -0
  112. package/qtty-js/qtty-node/examples/quickstart.mjs +36 -0
  113. package/qtty-js/qtty-node/examples/serialization.mjs +125 -0
  114. package/qtty-js/qtty-node/examples/unit_factories.mjs +74 -0
  115. package/qtty-js/qtty-node/index.d.ts +219 -0
  116. package/qtty-js/qtty-node/index.js +323 -0
  117. package/qtty-js/qtty-node/lib/DerivedQuantity.js +122 -0
  118. package/qtty-js/qtty-node/lib/Quantity.js +151 -0
  119. package/qtty-js/qtty-node/lib/backend.js +25 -0
  120. package/qtty-js/qtty-node/native.cjs +306 -0
  121. package/qtty-js/qtty-node/package-lock.json +3223 -0
  122. package/qtty-js/qtty-node/package.json +70 -0
  123. package/qtty-js/qtty-node/units.d.ts +299 -0
  124. package/qtty-js/qtty-node/units.js +210 -0
  125. package/qtty-js/qtty-web/Cargo.lock +767 -0
  126. package/qtty-js/qtty-web/Cargo.toml +21 -0
  127. package/qtty-js/qtty-web/index.d.ts +140 -0
  128. package/qtty-js/qtty-web/index.js +20 -0
  129. package/qtty-js/qtty-web/lib/DerivedQuantity.js +58 -0
  130. package/qtty-js/qtty-web/lib/Quantity.js +75 -0
  131. package/qtty-js/qtty-web/lib/backend.js +80 -0
  132. package/qtty-js/qtty-web/package.json +45 -0
  133. package/qtty-js/qtty-web/src/lib.rs +111 -0
  134. package/qtty-js/scripts/ci.sh +73 -0
  135. package/scripts/ci.sh +123 -0
  136. package/siderust-core/Cargo.lock +787 -0
  137. package/siderust-core/Cargo.toml +18 -0
  138. package/siderust-core/DEDUPLICATION.md +124 -0
  139. package/siderust-core/src/body.rs +120 -0
  140. package/siderust-core/src/events.rs +184 -0
  141. package/siderust-core/src/lib.rs +20 -0
  142. package/siderust-core/src/observer.rs +55 -0
  143. package/siderust-core/src/position.rs +213 -0
  144. package/siderust-node/.prettierignore +7 -0
  145. package/siderust-node/.prettierrc.json +6 -0
  146. package/siderust-node/Cargo.lock +906 -0
  147. package/siderust-node/Cargo.toml +29 -0
  148. package/siderust-node/README.md +109 -0
  149. package/siderust-node/__test__/index.test.mjs +248 -0
  150. package/siderust-node/build.rs +5 -0
  151. package/siderust-node/c8.config.json +3 -0
  152. package/siderust-node/eslint.config.js +31 -0
  153. package/siderust-node/examples/01_basic_coordinates.mjs +24 -0
  154. package/siderust-node/examples/02_coordinate_transformations.mjs +25 -0
  155. package/siderust-node/examples/03_all_frames_conversions.mjs +26 -0
  156. package/siderust-node/examples/04_all_center_conversions.mjs +24 -0
  157. package/siderust-node/examples/05_target_tracking.mjs +22 -0
  158. package/siderust-node/examples/06_night_events.mjs +18 -0
  159. package/siderust-node/examples/07_moon_properties.mjs +21 -0
  160. package/siderust-node/examples/08_solar_system.mjs +19 -0
  161. package/siderust-node/examples/09_star_observability.mjs +22 -0
  162. package/siderust-node/examples/10_time_periods.mjs +9 -0
  163. package/siderust-node/examples/11_serialization.mjs +31 -0
  164. package/siderust-node/examples/12_runtime_ephemeris.mjs +27 -0
  165. package/siderust-node/examples/13_coordinate_operations.mjs +20 -0
  166. package/siderust-node/index.d.ts +623 -0
  167. package/siderust-node/index.js +79 -0
  168. package/siderust-node/lib/Observer.js +112 -0
  169. package/siderust-node/lib/Star.js +118 -0
  170. package/siderust-node/lib/backend.js +63 -0
  171. package/siderust-node/lib/wrappers.js +566 -0
  172. package/siderust-node/main.js +20 -0
  173. package/siderust-node/native.cjs +360 -0
  174. package/siderust-node/package-lock.json +3261 -0
  175. package/siderust-node/package.json +71 -0
  176. package/siderust-node/src/body.rs +74 -0
  177. package/siderust-node/src/coordinates.rs +372 -0
  178. package/siderust-node/src/ephemeris.rs +462 -0
  179. package/siderust-node/src/events.rs +577 -0
  180. package/siderust-node/src/lib.rs +43 -0
  181. package/siderust-node/src/observer.rs +132 -0
  182. package/siderust-node/src/phase.rs +218 -0
  183. package/siderust-node/src/position.rs +292 -0
  184. package/siderust-node/src/star.rs +200 -0
  185. package/siderust-web/Cargo.lock +855 -0
  186. package/siderust-web/Cargo.toml +34 -0
  187. package/siderust-web/README.md +100 -0
  188. package/siderust-web/__test__/index.test.mjs +118 -0
  189. package/siderust-web/examples/github-pages/README.md +31 -0
  190. package/siderust-web/examples/github-pages/index.html +135 -0
  191. package/siderust-web/index.d.ts +311 -0
  192. package/siderust-web/index.js +66 -0
  193. package/siderust-web/lib/Observer.js +103 -0
  194. package/siderust-web/lib/Star.js +116 -0
  195. package/siderust-web/lib/backend.js +400 -0
  196. package/siderust-web/lib/wrappers.js +512 -0
  197. package/siderust-web/package.json +55 -0
  198. package/siderust-web/src/body.rs +69 -0
  199. package/siderust-web/src/coordinates.rs +302 -0
  200. package/siderust-web/src/ephemeris.rs +456 -0
  201. package/siderust-web/src/events.rs +520 -0
  202. package/siderust-web/src/lib.rs +51 -0
  203. package/siderust-web/src/observer.rs +117 -0
  204. package/siderust-web/src/phase.rs +190 -0
  205. package/siderust-web/src/position.rs +291 -0
  206. package/siderust-web/src/star.rs +178 -0
  207. package/tempoch-js/.github/workflows/ci.yml +142 -0
  208. package/tempoch-js/.gitmodules +3 -0
  209. package/tempoch-js/CHANGELOG.md +25 -0
  210. package/tempoch-js/LICENSE +661 -0
  211. package/tempoch-js/README.md +126 -0
  212. package/tempoch-js/package.json +20 -0
  213. package/tempoch-js/scripts/ci.sh +73 -0
  214. package/tempoch-js/tempoch/.github/workflows/ci.yml +113 -0
  215. package/tempoch-js/tempoch/CHANGELOG.md +82 -0
  216. package/tempoch-js/tempoch/Cargo.lock +947 -0
  217. package/tempoch-js/tempoch/Cargo.toml +3 -0
  218. package/tempoch-js/tempoch/LICENSE +661 -0
  219. package/tempoch-js/tempoch/README.md +76 -0
  220. package/tempoch-js/tempoch/tempoch/Cargo.toml +27 -0
  221. package/tempoch-js/tempoch/tempoch/examples/periods.rs +45 -0
  222. package/tempoch-js/tempoch/tempoch/examples/quickstart.rs +13 -0
  223. package/tempoch-js/tempoch/tempoch/src/lib.rs +49 -0
  224. package/tempoch-js/tempoch/tempoch/tests/integration.rs +57 -0
  225. package/tempoch-js/tempoch/tempoch-core/Cargo.toml +24 -0
  226. package/tempoch-js/tempoch/tempoch-core/src/delta_t.rs +345 -0
  227. package/tempoch-js/tempoch/tempoch-core/src/instant.rs +811 -0
  228. package/tempoch-js/tempoch/tempoch-core/src/julian_date_ext.rs +142 -0
  229. package/tempoch-js/tempoch/tempoch-core/src/lib.rs +81 -0
  230. package/tempoch-js/tempoch/tempoch-core/src/period.rs +1168 -0
  231. package/tempoch-js/tempoch/tempoch-core/src/scales.rs +779 -0
  232. package/tempoch-js/tempoch/tempoch-ffi/Cargo.lock +889 -0
  233. package/tempoch-js/tempoch/tempoch-ffi/Cargo.toml +26 -0
  234. package/tempoch-js/tempoch/tempoch-ffi/build.rs +24 -0
  235. package/tempoch-js/tempoch/tempoch-ffi/cbindgen.toml +30 -0
  236. package/tempoch-js/tempoch/tempoch-ffi/src/error.rs +19 -0
  237. package/tempoch-js/tempoch/tempoch-ffi/src/lib.rs +82 -0
  238. package/tempoch-js/tempoch/tempoch-ffi/src/period.rs +101 -0
  239. package/tempoch-js/tempoch/tempoch-ffi/src/time.rs +711 -0
  240. package/tempoch-js/tempoch/tempoch-ffi/tests/ffi.rs +265 -0
  241. package/tempoch-js/tempoch-node/.prettierignore +6 -0
  242. package/tempoch-js/tempoch-node/.prettierrc.json +6 -0
  243. package/tempoch-js/tempoch-node/Cargo.lock +496 -0
  244. package/tempoch-js/tempoch-node/Cargo.toml +29 -0
  245. package/tempoch-js/tempoch-node/README.md +265 -0
  246. package/tempoch-js/tempoch-node/__test__/index.test.mjs +598 -0
  247. package/tempoch-js/tempoch-node/build.rs +5 -0
  248. package/tempoch-js/tempoch-node/c8.config.json +3 -0
  249. package/tempoch-js/tempoch-node/eslint.config.js +31 -0
  250. package/tempoch-js/tempoch-node/examples/periods.mjs +79 -0
  251. package/tempoch-js/tempoch-node/examples/quickstart.mjs +71 -0
  252. package/tempoch-js/tempoch-node/examples/timescales.mjs +92 -0
  253. package/tempoch-js/tempoch-node/index.d.ts +280 -0
  254. package/tempoch-js/tempoch-node/index.js +32 -0
  255. package/tempoch-js/tempoch-node/lib/JulianDate.js +176 -0
  256. package/tempoch-js/tempoch-node/lib/ModifiedJulianDate.js +156 -0
  257. package/tempoch-js/tempoch-node/lib/Period.js +133 -0
  258. package/tempoch-js/tempoch-node/lib/backend.js +38 -0
  259. package/tempoch-js/tempoch-node/lib/qttyCompat.js +92 -0
  260. package/tempoch-js/tempoch-node/native.cjs +317 -0
  261. package/tempoch-js/tempoch-node/package-lock.json +3223 -0
  262. package/tempoch-js/tempoch-node/package.json +56 -0
  263. package/tempoch-js/tempoch-node/src/lib.rs +573 -0
  264. package/tempoch-js/tempoch-web/Cargo.toml +23 -0
  265. package/tempoch-js/tempoch-web/index.d.ts +95 -0
  266. package/tempoch-js/tempoch-web/index.js +27 -0
  267. package/tempoch-js/tempoch-web/lib/JulianDate.js +170 -0
  268. package/tempoch-js/tempoch-web/lib/ModifiedJulianDate.js +145 -0
  269. package/tempoch-js/tempoch-web/lib/Period.js +121 -0
  270. package/tempoch-js/tempoch-web/lib/backend.js +118 -0
  271. package/tempoch-js/tempoch-web/package.json +46 -0
  272. 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
+ }