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,229 @@
1
+ //! C-compatible FFI bindings for `qtty` physical quantities and unit conversions.
2
+ //!
3
+ //! `qtty-ffi` provides a stable C ABI for `qtty`, enabling interoperability with C/C++ code
4
+ //! and other languages with C FFI support. It also provides helper types and macros for
5
+ //! downstream Rust crates that need to expose their own FFI APIs using `qtty` quantities.
6
+ //!
7
+ //! # Features
8
+ //!
9
+ //! - **ABI-stable types**: `#[repr(C)]` and `#[repr(u32)]` types safe for FFI
10
+ //! - **Unit registry**: Mapping between FFI unit IDs and conversion factors
11
+ //! - **C API**: `extern "C"` functions for quantity construction and conversion
12
+ //! - **Rust helpers**: Macros and trait implementations for downstream integration
13
+ //!
14
+ //! # Quick Start (C/C++)
15
+ //!
16
+ //! Include the generated header and link against the library:
17
+ //!
18
+ //! ```c
19
+ //! #include "qtty_ffi.h"
20
+ //!
21
+ //! // Create a quantity
22
+ //! QttyQuantity meters;
23
+ //! qtty_quantity_make(1000.0, UnitId_Meter, &meters);
24
+ //!
25
+ //! // Convert to kilometers
26
+ //! QttyQuantity kilometers;
27
+ //! int32_t status = qtty_quantity_convert(meters, UnitId_Kilometer, &kilometers);
28
+ //! if (status == QTTY_OK) {
29
+ //! // kilometers.value == 1.0
30
+ //! }
31
+ //! ```
32
+ //!
33
+ //! # Quick Start (Rust)
34
+ //!
35
+ //! Use the helper traits and macros for seamless conversion:
36
+ //!
37
+ //! ```rust
38
+ //! use qtty::length::{Meters, Kilometers};
39
+ //! use qtty_ffi::{QttyQuantity, UnitId};
40
+ //!
41
+ //! // Convert Rust type to FFI
42
+ //! let meters = Meters::new(1000.0);
43
+ //! let ffi_qty: QttyQuantity = meters.into();
44
+ //!
45
+ //! // Convert FFI back to Rust type (with automatic unit conversion)
46
+ //! let km: Kilometers = ffi_qty.try_into().unwrap();
47
+ //! assert!((km.value() - 1.0).abs() < 1e-12);
48
+ //! ```
49
+ //!
50
+ //! # ABI Stability
51
+ //!
52
+ //! The following are part of the ABI contract and will never change:
53
+ //!
54
+ //! - [`UnitId`] discriminant values (existing variants)
55
+ //! - [`DimensionId`] discriminant values (existing variants)
56
+ //! - [`QttyQuantity`] memory layout
57
+ //! - Status code values ([`QTTY_OK`], [`QTTY_ERR_UNKNOWN_UNIT`], etc.)
58
+ //! - Function signatures of exported `extern "C"` functions
59
+ //!
60
+ //! New variants may be added to enums (with new discriminant values), and new functions
61
+ //! may be added, but existing items will remain stable.
62
+ //!
63
+ //! # Supported Units (v1)
64
+ //!
65
+ //! ## Length
66
+ //! - [`UnitId::Meter`] - SI base unit
67
+ //! - [`UnitId::Kilometer`] - 1000 meters
68
+ //!
69
+ //! ## Time
70
+ //! - [`UnitId::Second`] - SI base unit
71
+ //! - [`UnitId::Minute`] - 60 seconds
72
+ //! - [`UnitId::Hour`] - 3600 seconds
73
+ //! - [`UnitId::Day`] - 86400 seconds
74
+ //!
75
+ //! ## Angle
76
+ //! - [`UnitId::Radian`] - SI unit
77
+ //! - [`UnitId::Degree`] - π/180 radians
78
+ //!
79
+ //! # Error Handling
80
+ //!
81
+ //! All FFI functions return status codes:
82
+ //!
83
+ //! - [`QTTY_OK`] (0): Success
84
+ //! - [`QTTY_ERR_UNKNOWN_UNIT`] (-1): Invalid unit ID
85
+ //! - [`QTTY_ERR_INCOMPATIBLE_DIM`] (-2): Dimension mismatch
86
+ //! - [`QTTY_ERR_NULL_OUT`] (-3): Null output pointer
87
+ //! - [`QTTY_ERR_INVALID_VALUE`] (-4): Invalid value (reserved)
88
+ //! - [`QTTY_ERR_BUFFER_TOO_SMALL`] (-5): Output buffer too small
89
+ //!
90
+ //! Format flags for [`qtty_quantity_format`]:
91
+ //!
92
+ //! - [`QTTY_FMT_DEFAULT`] (0): decimal notation
93
+ //! - [`QTTY_FMT_LOWER_EXP`] (1): scientific notation (lowercase `e`)
94
+ //! - [`QTTY_FMT_UPPER_EXP`] (2): scientific notation (uppercase `E`)
95
+ //!
96
+ //! # Thread Safety
97
+ //!
98
+ //! All functions are thread-safe. The library contains no global mutable state.
99
+
100
+ #![deny(missing_docs)]
101
+ // PyO3 generated code contains unsafe operations, so we can't enforce this when pyo3 feature is enabled
102
+ #![cfg_attr(not(feature = "pyo3"), deny(unsafe_op_in_unsafe_fn))]
103
+
104
+ // Core modules
105
+ mod ffi;
106
+ pub mod helpers;
107
+ #[macro_use]
108
+ pub mod macros;
109
+ pub mod registry;
110
+ mod types;
111
+
112
+ // Re-export FFI functions
113
+ pub use ffi::{
114
+ qtty_derived_convert, qtty_derived_from_json, qtty_derived_make, qtty_derived_to_json,
115
+ qtty_ffi_version, qtty_quantity_convert, qtty_quantity_convert_value, qtty_quantity_format,
116
+ qtty_quantity_from_json, qtty_quantity_from_json_value, qtty_quantity_make,
117
+ qtty_quantity_to_json, qtty_quantity_to_json_value, qtty_string_free, qtty_unit_dimension,
118
+ qtty_unit_is_valid, qtty_unit_name, qtty_units_compatible,
119
+ };
120
+
121
+ // Re-export types
122
+ pub use types::{
123
+ DimensionId, QttyDerivedQuantity, QttyQuantity, UnitId, QTTY_ERR_BUFFER_TOO_SMALL,
124
+ QTTY_ERR_INCOMPATIBLE_DIM, QTTY_ERR_INVALID_VALUE, QTTY_ERR_NULL_OUT, QTTY_ERR_UNKNOWN_UNIT,
125
+ QTTY_FMT_DEFAULT, QTTY_FMT_LOWER_EXP, QTTY_FMT_UPPER_EXP, QTTY_OK,
126
+ };
127
+
128
+ // The impl_unit_ffi! macro is automatically exported at crate root by #[macro_export]
129
+
130
+ // Re-export helper functions
131
+ pub use helpers::{
132
+ days_into_ffi, degrees_into_ffi, hours_into_ffi, kilometers_into_ffi, meters_into_ffi,
133
+ minutes_into_ffi, radians_into_ffi, seconds_into_ffi, try_into_days, try_into_degrees,
134
+ try_into_hours, try_into_kilometers, try_into_meters, try_into_minutes, try_into_radians,
135
+ try_into_seconds,
136
+ };
137
+
138
+ #[cfg(test)]
139
+ mod tests {
140
+ use super::*;
141
+ use core::mem::{align_of, size_of};
142
+
143
+ /// Test that QttyQuantity has the expected size and alignment for FFI.
144
+ #[test]
145
+ fn test_qtty_quantity_layout() {
146
+ // QttyQuantity should be:
147
+ // - f64 (8 bytes) + UnitId (4 bytes) + padding (4 bytes) = 16 bytes
148
+ // - Aligned to 8 bytes (alignment of f64)
149
+ assert_eq!(size_of::<QttyQuantity>(), 16);
150
+ assert_eq!(align_of::<QttyQuantity>(), 8);
151
+ }
152
+
153
+ /// Test that UnitId has the expected size.
154
+ #[test]
155
+ fn test_unit_id_layout() {
156
+ assert_eq!(size_of::<UnitId>(), 4);
157
+ assert_eq!(align_of::<UnitId>(), 4);
158
+ }
159
+
160
+ /// Test that DimensionId has the expected size.
161
+ #[test]
162
+ fn test_dimension_id_layout() {
163
+ assert_eq!(size_of::<DimensionId>(), 4);
164
+ assert_eq!(align_of::<DimensionId>(), 4);
165
+ }
166
+
167
+ /// Test known conversion: 1000 meters → 1 kilometer
168
+ #[test]
169
+ fn test_known_conversion_meters_to_kilometers() {
170
+ let mut out = QttyQuantity::default();
171
+ let src = QttyQuantity::new(1000.0, UnitId::Meter);
172
+
173
+ let status = unsafe { qtty_quantity_convert(src, UnitId::Kilometer, &mut out) };
174
+
175
+ assert_eq!(status, QTTY_OK);
176
+ assert!((out.value - 1.0).abs() < 1e-12);
177
+ assert_eq!(out.unit, UnitId::Kilometer);
178
+ }
179
+
180
+ /// Test known conversion: 3600 seconds → 1 hour
181
+ #[test]
182
+ fn test_known_conversion_seconds_to_hours() {
183
+ let mut out = QttyQuantity::default();
184
+ let src = QttyQuantity::new(3600.0, UnitId::Second);
185
+
186
+ let status = unsafe { qtty_quantity_convert(src, UnitId::Hour, &mut out) };
187
+
188
+ assert_eq!(status, QTTY_OK);
189
+ assert!((out.value - 1.0).abs() < 1e-12);
190
+ assert_eq!(out.unit, UnitId::Hour);
191
+ }
192
+
193
+ /// Test known conversion: 180 degrees → π radians
194
+ #[test]
195
+ fn test_known_conversion_degrees_to_radians() {
196
+ use core::f64::consts::PI;
197
+
198
+ let mut out = QttyQuantity::default();
199
+ let src = QttyQuantity::new(180.0, UnitId::Degree);
200
+
201
+ let status = unsafe { qtty_quantity_convert(src, UnitId::Radian, &mut out) };
202
+
203
+ assert_eq!(status, QTTY_OK);
204
+ assert!((out.value - PI).abs() < 1e-12);
205
+ assert_eq!(out.unit, UnitId::Radian);
206
+ }
207
+
208
+ /// Test incompatible conversion: meters → seconds fails
209
+ #[test]
210
+ fn test_incompatible_conversion_fails() {
211
+ let mut out = QttyQuantity::default();
212
+ let src = QttyQuantity::new(100.0, UnitId::Meter);
213
+
214
+ let status = unsafe { qtty_quantity_convert(src, UnitId::Second, &mut out) };
215
+
216
+ assert_eq!(status, QTTY_ERR_INCOMPATIBLE_DIM);
217
+ }
218
+
219
+ /// Test null output pointer handling
220
+ #[test]
221
+ fn test_null_out_pointer() {
222
+ let src = QttyQuantity::new(100.0, UnitId::Meter);
223
+
224
+ let status =
225
+ unsafe { qtty_quantity_convert(src, UnitId::Kilometer, core::ptr::null_mut()) };
226
+
227
+ assert_eq!(status, QTTY_ERR_NULL_OUT);
228
+ }
229
+ }
@@ -0,0 +1,121 @@
1
+ //! Macros for implementing FFI conversions for qtty unit types.
2
+ //!
3
+ //! This module provides macros that make it easy to implement `From` and `TryFrom`
4
+ //! conversions between `qtty` unit types and [`QttyQuantity`], as well as code generation
5
+ //! macros for defining all FFI units in one place.
6
+ //!
7
+ //! # Example
8
+ //!
9
+ //! The macro is intended to be used in crates that own the types being converted.
10
+ //! Since `qtty-ffi` already implements these for the standard `qtty` types, you
11
+ //! typically only need this for custom wrapper types.
12
+ //!
13
+ //! ```rust,ignore
14
+ //! use qtty_ffi::{impl_unit_ffi, QttyQuantity, UnitId};
15
+ //! use qtty::length::Meters;
16
+ //!
17
+ //! // This would work in your own crate that defines MyMeters:
18
+ //! impl_unit_ffi!(MyMeters, UnitId::Meter);
19
+ //!
20
+ //! // Then you can convert between MyMeters and QttyQuantity
21
+ //! let meters = MyMeters::new(100.0);
22
+ //! let quantity: QttyQuantity = meters.into();
23
+ //! ```
24
+
25
+ /// Internal helper macro for generating unit match arms in `UnitId` methods.
26
+ #[doc(hidden)]
27
+ #[macro_export]
28
+ macro_rules! unit_match_arms {
29
+ ($(($variant:ident, $name:expr)),* $(,)?) => {
30
+ $(
31
+ UnitId::$variant => $name
32
+ ),*
33
+ };
34
+ }
35
+
36
+ /// Internal helper macro for generating unit `from_u32` match arms.
37
+ #[doc(hidden)]
38
+ #[macro_export]
39
+ macro_rules! unit_from_u32_arms {
40
+ ($(($discriminant:expr, $variant:ident)),* $(,)?) => {
41
+ $(
42
+ $discriminant => Some(UnitId::$variant)
43
+ ),*
44
+ };
45
+ }
46
+
47
+ /// Internal helper macro for generating registry metadata match arms.
48
+ #[doc(hidden)]
49
+ #[macro_export]
50
+ macro_rules! registry_match_arms {
51
+ ($(($variant:ident, $dim:ident, $scale:expr, $name:expr)),* $(,)?) => {
52
+ $(
53
+ UnitId::$variant => Some(UnitMeta {
54
+ dim: DimensionId::$dim,
55
+ scale_to_canonical: $scale,
56
+ name: $name,
57
+ })
58
+ ),*
59
+ };
60
+ }
61
+
62
+ /// Implements `From<$qty_type>` for `QttyQuantity` and `TryFrom<QttyQuantity>` for `$qty_type`.
63
+ ///
64
+ /// This macro generates bidirectional conversion implementations between a specific
65
+ /// `qtty` quantity type (like `Meters`, `Seconds`, etc.) and the FFI-safe [`QttyQuantity`] type.
66
+ ///
67
+ /// # Arguments
68
+ ///
69
+ /// * `$qty_type` - The `qtty` quantity type (e.g., `qtty::length::Meters`)
70
+ /// * `$unit_id` - The corresponding [`UnitId`] variant (e.g., `UnitId::Meter`)
71
+ ///
72
+ /// # Generated Implementations
73
+ ///
74
+ /// * `impl From<$qty_type> for QttyQuantity` - Converts the typed quantity to FFI format
75
+ /// * `impl TryFrom<QttyQuantity> for $qty_type` - Converts FFI format back to typed quantity,
76
+ /// performing unit conversion if the FFI quantity is in a compatible unit
77
+ ///
78
+ /// # Example
79
+ ///
80
+ /// The macro is used internally by `qtty-ffi` to implement conversions for standard types.
81
+ /// For your own types, usage looks like:
82
+ ///
83
+ /// ```rust,ignore
84
+ /// use qtty_ffi::{impl_unit_ffi, QttyQuantity, UnitId};
85
+ ///
86
+ /// // Your custom type (defined in your crate)
87
+ /// struct MySeconds(f64);
88
+ /// impl MySeconds {
89
+ /// fn new(v: f64) -> Self { Self(v) }
90
+ /// fn value(&self) -> f64 { self.0 }
91
+ /// }
92
+ ///
93
+ /// impl_unit_ffi!(MySeconds, UnitId::Second);
94
+ /// ```
95
+ #[macro_export]
96
+ macro_rules! impl_unit_ffi {
97
+ ($qty_type:ty, $unit_id:expr) => {
98
+ impl From<$qty_type> for $crate::QttyQuantity {
99
+ #[inline]
100
+ fn from(qty: $qty_type) -> Self {
101
+ $crate::QttyQuantity::new(qty.value(), $unit_id)
102
+ }
103
+ }
104
+
105
+ impl core::convert::TryFrom<$crate::QttyQuantity> for $qty_type {
106
+ type Error = i32;
107
+
108
+ #[inline]
109
+ fn try_from(qty: $crate::QttyQuantity) -> Result<Self, Self::Error> {
110
+ // If already the right unit, just wrap
111
+ if qty.unit == $unit_id {
112
+ return Ok(<$qty_type>::new(qty.value));
113
+ }
114
+
115
+ // Otherwise, try to convert
116
+ let converted = $crate::registry::convert_value(qty.value, qty.unit, $unit_id)?;
117
+ Ok(<$qty_type>::new(converted))
118
+ }
119
+ }
120
+ };
121
+ }
@@ -0,0 +1,274 @@
1
+ //! Unit registry and conversion logic for FFI.
2
+ //!
3
+ //! This module provides a Rust-only unit registry that maps [`UnitId`] values to their
4
+ //! metadata (dimension, scaling factor, name) and implements conversion between compatible units.
5
+ //!
6
+ //! # Conversion Formula
7
+ //!
8
+ //! Conversions use a canonical unit per dimension:
9
+ //! - Length: Meter
10
+ //! - Time: Second
11
+ //! - Angle: Radian
12
+ //!
13
+ //! The conversion formula is:
14
+ //! ```text
15
+ //! v_canonical = v_src * src.scale_to_canonical
16
+ //! v_dst = v_canonical / dst.scale_to_canonical
17
+ //! ```
18
+ //!
19
+ //! Which simplifies to:
20
+ //! ```text
21
+ //! v_dst = v_src * (src.scale_to_canonical / dst.scale_to_canonical)
22
+ //! ```
23
+
24
+ use crate::types::{
25
+ DimensionId, UnitId, QTTY_ERR_INCOMPATIBLE_DIM, QTTY_ERR_UNKNOWN_UNIT, QTTY_OK,
26
+ };
27
+
28
+ // =============================================================================
29
+ // Unit Metadata
30
+ // =============================================================================
31
+
32
+ /// Metadata about a unit for internal registry use.
33
+ ///
34
+ /// This struct is Rust-only and not exposed via FFI.
35
+ #[derive(Debug, Clone, Copy)]
36
+ pub struct UnitMeta {
37
+ /// The dimension this unit belongs to.
38
+ pub dim: DimensionId,
39
+ /// Scaling factor to convert to the canonical unit for this dimension.
40
+ ///
41
+ /// For example, for Kilometer: `scale_to_canonical = 1000.0` (1 km = 1000 m)
42
+ pub scale_to_canonical: f64,
43
+ /// Human-readable name of the unit.
44
+ pub name: &'static str,
45
+ }
46
+
47
+ // =============================================================================
48
+ // Registry Functions
49
+ // =============================================================================
50
+
51
+ /// Returns metadata for the given unit ID.
52
+ ///
53
+ /// Returns `None` if the unit ID is not recognized.
54
+ #[inline]
55
+ pub fn meta(id: UnitId) -> Option<UnitMeta> {
56
+ include!(concat!(env!("OUT_DIR"), "/unit_registry.rs"))
57
+ }
58
+
59
+ /// Returns the dimension for the given unit ID.
60
+ ///
61
+ /// Returns `None` if the unit ID is not recognized.
62
+ #[inline]
63
+ pub fn dimension(id: UnitId) -> Option<DimensionId> {
64
+ meta(id).map(|m| m.dim)
65
+ }
66
+
67
+ /// Checks if two units are compatible (same dimension).
68
+ ///
69
+ /// Returns `true` if both units have the same dimension, `false` otherwise.
70
+ /// Also returns `false` if either unit is not recognized.
71
+ #[inline]
72
+ pub fn compatible(a: UnitId, b: UnitId) -> bool {
73
+ match (dimension(a), dimension(b)) {
74
+ (Some(da), Some(db)) => da == db,
75
+ _ => false,
76
+ }
77
+ }
78
+
79
+ /// Converts a value from one unit to another.
80
+ ///
81
+ /// # Arguments
82
+ ///
83
+ /// * `v` - The value to convert
84
+ /// * `src` - The source unit
85
+ /// * `dst` - The destination unit
86
+ ///
87
+ /// # Returns
88
+ ///
89
+ /// * `Ok(converted_value)` on success
90
+ /// * `Err(QTTY_ERR_UNKNOWN_UNIT)` if either unit is not recognized
91
+ /// * `Err(QTTY_ERR_INCOMPATIBLE_DIM)` if units have different dimensions
92
+ ///
93
+ /// # Example
94
+ ///
95
+ /// ```rust
96
+ /// use qtty_ffi::{registry, UnitId};
97
+ ///
98
+ /// let meters = registry::convert_value(1000.0, UnitId::Meter, UnitId::Kilometer);
99
+ /// assert!((meters.unwrap() - 1.0).abs() < 1e-12);
100
+ /// ```
101
+ #[inline]
102
+ pub fn convert_value(v: f64, src: UnitId, dst: UnitId) -> Result<f64, i32> {
103
+ let src_meta = meta(src).ok_or(QTTY_ERR_UNKNOWN_UNIT)?;
104
+ let dst_meta = meta(dst).ok_or(QTTY_ERR_UNKNOWN_UNIT)?;
105
+
106
+ if src_meta.dim != dst_meta.dim {
107
+ return Err(QTTY_ERR_INCOMPATIBLE_DIM);
108
+ }
109
+
110
+ // If same unit, no conversion needed
111
+ if src == dst {
112
+ return Ok(v);
113
+ }
114
+
115
+ // Convert: v_canonical = v * src_scale, then v_dst = v_canonical / dst_scale
116
+ let v_canonical = v * src_meta.scale_to_canonical;
117
+ let v_dst = v_canonical / dst_meta.scale_to_canonical;
118
+
119
+ Ok(v_dst)
120
+ }
121
+
122
+ /// Converts a value from one unit to another, returning a status code.
123
+ ///
124
+ /// This is a convenience function that returns `QTTY_OK` on success and
125
+ /// the appropriate error code on failure. The converted value is stored
126
+ /// in `result` (which must be initialized).
127
+ ///
128
+ /// # Arguments
129
+ ///
130
+ /// * `v` - The value to convert
131
+ /// * `src` - The source unit
132
+ /// * `dst` - The destination unit
133
+ /// * `result` - Mutable reference to store the converted value
134
+ ///
135
+ /// # Returns
136
+ ///
137
+ /// * `QTTY_OK` on success
138
+ /// * `QTTY_ERR_UNKNOWN_UNIT` if either unit is not recognized
139
+ /// * `QTTY_ERR_INCOMPATIBLE_DIM` if units have different dimensions
140
+ #[inline]
141
+ pub fn convert_value_status(v: f64, src: UnitId, dst: UnitId, result: &mut f64) -> i32 {
142
+ match convert_value(v, src, dst) {
143
+ Ok(converted) => {
144
+ *result = converted;
145
+ QTTY_OK
146
+ }
147
+ Err(code) => code,
148
+ }
149
+ }
150
+
151
+ #[cfg(test)]
152
+ mod tests {
153
+ use super::*;
154
+ use approx::assert_relative_eq;
155
+ use core::f64::consts::PI;
156
+
157
+ #[test]
158
+ fn test_meta_returns_correct_dimensions() {
159
+ assert_eq!(meta(UnitId::Meter).unwrap().dim, DimensionId::Length);
160
+ assert_eq!(meta(UnitId::Kilometer).unwrap().dim, DimensionId::Length);
161
+ assert_eq!(meta(UnitId::Second).unwrap().dim, DimensionId::Time);
162
+ assert_eq!(meta(UnitId::Minute).unwrap().dim, DimensionId::Time);
163
+ assert_eq!(meta(UnitId::Hour).unwrap().dim, DimensionId::Time);
164
+ assert_eq!(meta(UnitId::Day).unwrap().dim, DimensionId::Time);
165
+ assert_eq!(meta(UnitId::Radian).unwrap().dim, DimensionId::Angle);
166
+ assert_eq!(meta(UnitId::Degree).unwrap().dim, DimensionId::Angle);
167
+ }
168
+
169
+ #[test]
170
+ fn test_compatible_same_dimension() {
171
+ assert!(compatible(UnitId::Meter, UnitId::Kilometer));
172
+ assert!(compatible(UnitId::Second, UnitId::Hour));
173
+ assert!(compatible(UnitId::Radian, UnitId::Degree));
174
+ }
175
+
176
+ #[test]
177
+ fn test_compatible_different_dimension() {
178
+ assert!(!compatible(UnitId::Meter, UnitId::Second));
179
+ assert!(!compatible(UnitId::Hour, UnitId::Radian));
180
+ assert!(!compatible(UnitId::Degree, UnitId::Kilometer));
181
+ }
182
+
183
+ #[test]
184
+ fn test_convert_meters_to_kilometers() {
185
+ let result = convert_value(1000.0, UnitId::Meter, UnitId::Kilometer).unwrap();
186
+ assert_relative_eq!(result, 1.0, epsilon = 1e-12);
187
+ }
188
+
189
+ #[test]
190
+ fn test_convert_kilometers_to_meters() {
191
+ let result = convert_value(1.0, UnitId::Kilometer, UnitId::Meter).unwrap();
192
+ assert_relative_eq!(result, 1000.0, epsilon = 1e-12);
193
+ }
194
+
195
+ #[test]
196
+ fn test_convert_seconds_to_minutes() {
197
+ let result = convert_value(60.0, UnitId::Second, UnitId::Minute).unwrap();
198
+ assert_relative_eq!(result, 1.0, epsilon = 1e-12);
199
+ }
200
+
201
+ #[test]
202
+ fn test_convert_seconds_to_hours() {
203
+ let result = convert_value(3600.0, UnitId::Second, UnitId::Hour).unwrap();
204
+ assert_relative_eq!(result, 1.0, epsilon = 1e-12);
205
+ }
206
+
207
+ #[test]
208
+ fn test_convert_hours_to_seconds() {
209
+ let result = convert_value(1.0, UnitId::Hour, UnitId::Second).unwrap();
210
+ assert_relative_eq!(result, 3600.0, epsilon = 1e-12);
211
+ }
212
+
213
+ #[test]
214
+ fn test_convert_days_to_hours() {
215
+ let result = convert_value(1.0, UnitId::Day, UnitId::Hour).unwrap();
216
+ assert_relative_eq!(result, 24.0, epsilon = 1e-12);
217
+ }
218
+
219
+ #[test]
220
+ fn test_convert_degrees_to_radians() {
221
+ let result = convert_value(180.0, UnitId::Degree, UnitId::Radian).unwrap();
222
+ assert_relative_eq!(result, PI, epsilon = 1e-12);
223
+ }
224
+
225
+ #[test]
226
+ fn test_convert_radians_to_degrees() {
227
+ let result = convert_value(PI, UnitId::Radian, UnitId::Degree).unwrap();
228
+ assert_relative_eq!(result, 180.0, epsilon = 1e-12);
229
+ }
230
+
231
+ #[test]
232
+ fn test_convert_same_unit() {
233
+ let result = convert_value(42.0, UnitId::Meter, UnitId::Meter).unwrap();
234
+ assert_relative_eq!(result, 42.0, epsilon = 1e-12);
235
+ }
236
+
237
+ #[test]
238
+ fn test_convert_incompatible_dimensions() {
239
+ let result = convert_value(1.0, UnitId::Meter, UnitId::Second);
240
+ assert_eq!(result, Err(QTTY_ERR_INCOMPATIBLE_DIM));
241
+ }
242
+
243
+ #[test]
244
+ fn test_convert_preserves_special_values() {
245
+ // NaN
246
+ let nan_result = convert_value(f64::NAN, UnitId::Meter, UnitId::Kilometer).unwrap();
247
+ assert!(nan_result.is_nan());
248
+
249
+ // Infinity
250
+ let inf_result = convert_value(f64::INFINITY, UnitId::Second, UnitId::Minute).unwrap();
251
+ assert!(inf_result.is_infinite() && inf_result.is_sign_positive());
252
+
253
+ // Negative infinity
254
+ let neg_inf_result =
255
+ convert_value(f64::NEG_INFINITY, UnitId::Second, UnitId::Minute).unwrap();
256
+ assert!(neg_inf_result.is_infinite() && neg_inf_result.is_sign_negative());
257
+ }
258
+
259
+ #[test]
260
+ fn test_convert_value_status_success() {
261
+ let mut out = 0.0;
262
+ let status = convert_value_status(2.0, UnitId::Hour, UnitId::Minute, &mut out);
263
+ assert_eq!(status, QTTY_OK);
264
+ assert_relative_eq!(out, 120.0, epsilon = 1e-12);
265
+ }
266
+
267
+ #[test]
268
+ fn test_convert_value_status_incompatible_dimension() {
269
+ let mut out = -1.0;
270
+ let status = convert_value_status(1.0, UnitId::Meter, UnitId::Second, &mut out);
271
+ assert_eq!(status, QTTY_ERR_INCOMPATIBLE_DIM);
272
+ assert_relative_eq!(out, -1.0, epsilon = 1e-12);
273
+ }
274
+ }