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,1228 @@
1
+ //! Angular quantities and utilities.
2
+ //!
3
+ //! This module defines the **`Angular` dimension**, a blanket [`AngularUnit`] trait that extends
4
+ //! [`Unit`] for all angular units, common angular units (degrees, radians, arcseconds, etc.), and a set of
5
+ //! convenience methods on [`Quantity<U>`] where `U: AngularUnit`.
6
+ //!
7
+ //! # Design overview
8
+ //!
9
+ //! * **Canonical unit:** Degrees are taken as the canonical *scaling* unit for this dimension. That is,
10
+ //! `Degree::RATIO == 1.0`, and all other angular units express how many *degrees* correspond to one of that unit.
11
+ //! For example, `Radian::RATIO == 180.0 / PI` because 1 radian = 180/π degrees.
12
+ //! * **Associated constants:** The `AngularUnit` trait exposes precomputed constants (`FULL_TURN`, `HALF_TURN`,
13
+ //! `QUARTED_TURN`) expressed *in the receiving unit* for ergonomic range‑wrapping. These are derived from `τ`
14
+ //! radians and then converted to the target unit to avoid cumulative error from chained conversions.
15
+ //! * **Trigonometry:** `sin`, `cos`, `tan`, and `sin_cos` methods are provided on angular quantities; they convert to
16
+ //! radians internally and then call the corresponding `f64` intrinsic.
17
+ //! * **Wrapping helpers:** Utility methods to wrap any angle into common ranges — `[0, 360)` (or unit equivalent),
18
+ //! `(-180, 180]`, and the latitude‑style quarter fold `[-90, 90]`.
19
+ //!
20
+ //! ## Edge cases
21
+ //!
22
+ //! Wrapping and trig operations follow IEEE‑754 semantics from `f64`: if the underlying numeric is `NaN` or
23
+ //! `±∞`, results will generally be `NaN`.
24
+ //!
25
+ //! ## Unit symbols
26
+ //!
27
+ //! Unit `SYMBOL`s are used for display (e.g., `format!("{}", angle)`) and follow conventional unit symbols.
28
+ //! Unicode symbols are used where standard notation requires them (e.g., `°`, `′`, `″`, `μas`).
29
+ //!
30
+ //! ## Examples
31
+ //!
32
+ //! Convert between degrees and radians and evaluate a trig function:
33
+ //!
34
+ //! ```rust
35
+ //! use qtty_core::angular::{Degrees, Radians};
36
+ //!
37
+ //! let angle: Degrees = Degrees::new(90.0);
38
+ //! let r: Radians = angle.to();
39
+ //! assert!((r.value() - core::f64::consts::FRAC_PI_2).abs() < 1e-12);
40
+ //! assert!((angle.sin() - 1.0).abs() < 1e-12);
41
+ //! ```
42
+ //!
43
+ //! Wrap into the conventional signed range:
44
+ //!
45
+ //! ```rust
46
+ //! use qtty_core::angular::Degrees;
47
+ //! let a = Degrees::new(370.0).wrap_signed();
48
+ //! assert_eq!(a.value(), 10.0);
49
+ //! ```
50
+
51
+ use crate::scalar::Transcendental;
52
+ use crate::{Quantity, Unit};
53
+ use core::f64::consts::TAU;
54
+ use qtty_derive::Unit;
55
+
56
+ #[inline]
57
+ fn rem_euclid(x: f64, modulus: f64) -> f64 {
58
+ #[cfg(feature = "std")]
59
+ {
60
+ x.rem_euclid(modulus)
61
+ }
62
+ #[cfg(not(feature = "std"))]
63
+ {
64
+ let r = crate::libm::fmod(x, modulus);
65
+ if r < 0.0 {
66
+ r + modulus
67
+ } else {
68
+ r
69
+ }
70
+ }
71
+ }
72
+
73
+ /// Re-export from the dimension module.
74
+ pub use crate::dimension::Angular;
75
+
76
+ /// Blanket extension trait for any [`Unit`] whose dimension is [`Angular`].
77
+ ///
78
+ /// These associated constants provide the size of key turn fractions *expressed in the implementing unit*.
79
+ /// They are computed via a compile-time conversion from `TAU` radians (i.e., a full revolution) and then scaled.
80
+ /// This keeps all fractions derived from the same base value.
81
+ ///
82
+ /// > **Naming note:** The historical spelling `QUARTED_TURN` is retained for backward compatibility. It represents a
83
+ /// > quarter turn (90°).
84
+ pub trait AngularUnit: Unit<Dim = Angular> {
85
+ /// One full revolution (τ radians / 360°) expressed in this unit.
86
+ const FULL_TURN: f64;
87
+ /// Half a revolution (π radians / 180°) expressed in this unit.
88
+ const HALF_TURN: f64;
89
+ /// A quarter revolution (π/2 radians / 90°) expressed in this unit.
90
+ const QUARTED_TURN: f64;
91
+ }
92
+ impl<T: Unit<Dim = Angular>> AngularUnit for T {
93
+ /// One full revolution (360°) expressed in T unit.
94
+ const FULL_TURN: f64 = Radians::new(TAU).to_const::<T>().value();
95
+ /// Half a revolution (180°) expressed in T unit.
96
+ const HALF_TURN: f64 = Radians::new(TAU).to_const::<T>().value() * 0.5;
97
+ /// Quarter revolution (90°) expressed in T unit.
98
+ const QUARTED_TURN: f64 = Radians::new(TAU).to_const::<T>().value() * 0.25;
99
+ }
100
+
101
+ impl<U: AngularUnit + Copy> Quantity<U> {
102
+ /// Constant representing τ radians (2π rad == 360°).
103
+ ///
104
+ /// For angular quantities, `TAU` and [`Self::FULL_TURN`] are identical by construction.
105
+ pub const TAU: Quantity<U> = Quantity::<U>::new(U::FULL_TURN);
106
+ /// One full revolution (360°) expressed as `Quantity<U>`.
107
+ pub const FULL_TURN: Quantity<U> = Quantity::<U>::new(U::FULL_TURN);
108
+ /// Half a revolution (180°) expressed as `Quantity<U>`.
109
+ pub const HALF_TURN: Quantity<U> = Quantity::<U>::new(U::HALF_TURN);
110
+ /// Quarter revolution (90°) expressed as `Quantity<U>`.
111
+ pub const QUARTED_TURN: Quantity<U> = Quantity::<U>::new(U::QUARTED_TURN);
112
+
113
+ /// Sign of the *raw numeric* in this unit (same semantics as `f64::signum()`).
114
+ #[inline]
115
+ pub const fn signum_const(self) -> f64 {
116
+ self.value().signum()
117
+ }
118
+
119
+ /// Normalize into the canonical positive range `[0, FULL_TURN)`.
120
+ ///
121
+ /// Shorthand for [`Self::wrap_pos`].
122
+ #[inline]
123
+ pub fn normalize(self) -> Self {
124
+ self.wrap_pos()
125
+ }
126
+
127
+ /// Wrap into the positive range `[0, FULL_TURN)` using Euclidean remainder.
128
+ ///
129
+ /// IEEE‑754 note: `NaN`/`±∞` inputs generally produce `NaN`.
130
+ #[inline]
131
+ pub fn wrap_pos(self) -> Self {
132
+ Self::new(rem_euclid(self.value(), U::FULL_TURN))
133
+ }
134
+
135
+ /// Wrap into the signed range `(-HALF_TURN, HALF_TURN]`.
136
+ ///
137
+ /// *Upper bound is inclusive*; lower bound is exclusive. Useful for computing minimal signed angular differences.
138
+ ///
139
+ /// IEEE‑754 note: `NaN`/`±∞` inputs generally produce `NaN`.
140
+ #[inline]
141
+ pub fn wrap_signed(self) -> Self {
142
+ let full = U::FULL_TURN;
143
+ let half = 0.5 * full;
144
+ let x = self.value();
145
+ let y = rem_euclid(x + half, full) - half;
146
+ let norm = if y <= -half { y + full } else { y };
147
+ Self::new(norm)
148
+ }
149
+
150
+ /// Wrap into the alternate signed range `[-HALF_TURN, HALF_TURN)`.
151
+ ///
152
+ /// Lower bound inclusive; upper bound exclusive. Equivalent to `self.wrap_signed()` with the boundary flipped.
153
+ ///
154
+ /// IEEE‑754 note: `NaN`/`±∞` inputs generally produce `NaN`.
155
+ #[inline]
156
+ pub fn wrap_signed_lo(self) -> Self {
157
+ let mut y = self.wrap_signed().value(); // now in (-half, half]
158
+ let half = 0.5 * U::FULL_TURN;
159
+ if y >= half {
160
+ // move +half to -half
161
+ y -= U::FULL_TURN;
162
+ }
163
+ Self::new(y)
164
+ }
165
+
166
+ /// "Latitude fold": map into `[-QUARTER_TURN, +QUARTER_TURN]`.
167
+ ///
168
+ /// Useful for folding polar coordinates (e.g., converting declination‑like angles to a limited range).
169
+ ///
170
+ /// IEEE‑754 note: `NaN`/`±∞` inputs generally produce `NaN`.
171
+ #[inline]
172
+ pub fn wrap_quarter_fold(self) -> Self {
173
+ let full = U::FULL_TURN;
174
+ let half = 0.5 * full;
175
+ let quarter = 0.25 * full;
176
+ let y = rem_euclid(self.value() + quarter, full);
177
+ // quarter - |y - half| yields [-quarter, quarter]
178
+ Self::new(quarter - (y - half).abs())
179
+ }
180
+
181
+ /// Signed smallest angular separation in `(-HALF_TURN, HALF_TURN]`.
182
+ #[inline]
183
+ pub fn signed_separation(self, other: Self) -> Self {
184
+ (self - other).wrap_signed()
185
+ }
186
+
187
+ /// Absolute smallest angular separation (magnitude only).
188
+ #[inline]
189
+ pub fn abs_separation(self, other: Self) -> Self {
190
+ let sep = self.signed_separation(other);
191
+ Self::new(sep.value().abs())
192
+ }
193
+ }
194
+
195
+ // ─────────────────────────────────────────────────────────────────────────────
196
+ // Generic trigonometric implementations for any Transcendental scalar type
197
+ // ─────────────────────────────────────────────────────────────────────────────
198
+
199
+ impl<U: AngularUnit + Copy, S: Transcendental> Quantity<U, S> {
200
+ /// Sine of the angle.
201
+ ///
202
+ /// Converts the angle to radians and computes the sine.
203
+ /// Works with any scalar type that implements [`Transcendental`] (e.g., `f32`, `f64`).
204
+ #[inline]
205
+ pub fn sin(self) -> S {
206
+ let x_rad = self.to::<Radian>().value();
207
+ x_rad.sin()
208
+ }
209
+
210
+ /// Cosine of the angle.
211
+ ///
212
+ /// Converts the angle to radians and computes the cosine.
213
+ /// Works with any scalar type that implements [`Transcendental`] (e.g., `f32`, `f64`).
214
+ #[inline]
215
+ pub fn cos(self) -> S {
216
+ let x_rad = self.to::<Radian>().value();
217
+ x_rad.cos()
218
+ }
219
+
220
+ /// Tangent of the angle.
221
+ ///
222
+ /// Converts the angle to radians and computes the tangent.
223
+ /// Works with any scalar type that implements [`Transcendental`] (e.g., `f32`, `f64`).
224
+ #[inline]
225
+ pub fn tan(self) -> S {
226
+ let x_rad = self.to::<Radian>().value();
227
+ x_rad.tan()
228
+ }
229
+
230
+ /// Simultaneously compute sine and cosine.
231
+ ///
232
+ /// Converts the angle to radians and computes both sine and cosine.
233
+ /// Works with any scalar type that implements [`Transcendental`] (e.g., `f32`, `f64`).
234
+ #[inline]
235
+ pub fn sin_cos(self) -> (S, S) {
236
+ let x_rad = self.to::<Radian>().value();
237
+ x_rad.sin_cos()
238
+ }
239
+ }
240
+
241
+ /// Degree.
242
+ #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
243
+ #[unit(symbol = "°", dimension = Angular, ratio = 1.0)]
244
+ pub struct Degree;
245
+ /// Type alias shorthand for [`Degree`].
246
+ pub type Deg = Degree;
247
+ /// Convenience alias for a degree quantity.
248
+ pub type Degrees = Quantity<Deg>;
249
+ /// One degree.
250
+ pub const DEG: Degrees = Degrees::new(1.0);
251
+
252
+ /// Radian.
253
+ #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
254
+ #[unit(symbol = "rad", dimension = Angular, ratio = 180.0 / core::f64::consts::PI)]
255
+ pub struct Radian;
256
+ /// Type alias shorthand for [`Radian`].
257
+ pub type Rad = Radian;
258
+ /// Convenience alias for a radian quantity.
259
+ pub type Radians = Quantity<Rad>;
260
+ /// One radian.
261
+ pub const RAD: Radians = Radians::new(1.0);
262
+
263
+ /// Milliradian (`1/1000` radian).
264
+ #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
265
+ #[unit(symbol = "mrad", dimension = Angular, ratio = (180.0 / core::f64::consts::PI) / 1_000.0)]
266
+ pub struct Milliradian;
267
+ /// Type alias shorthand for [`Milliradian`].
268
+ pub type Mrad = Milliradian;
269
+ /// Convenience alias for a milliradian quantity.
270
+ pub type Milliradians = Quantity<Mrad>;
271
+ /// One milliradian.
272
+ pub const MRAD: Milliradians = Milliradians::new(1.0);
273
+
274
+ /// Arcminute (`1/60` degree).
275
+ #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
276
+ #[unit(symbol = "′", dimension = Angular, ratio = 1.0 / 60.0)]
277
+ pub struct Arcminute;
278
+ /// Alias for [`Arcminute`] (minute of angle, MOA).
279
+ pub type MOA = Arcminute;
280
+ /// Type alias shorthand for [`Arcminute`].
281
+ pub type Arcm = Arcminute;
282
+ /// Convenience alias for an arcminute quantity.
283
+ pub type Arcminutes = Quantity<Arcm>;
284
+ /// One arcminute.
285
+ pub const ARCM: Arcminutes = Arcminutes::new(1.0);
286
+
287
+ /// Arcsecond (`1/3600` degree).
288
+ #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
289
+ #[unit(symbol = "″", dimension = Angular, ratio = 1.0 / 3600.0)]
290
+ pub struct Arcsecond;
291
+ /// Type alias shorthand for [`Arcsecond`].
292
+ pub type Arcs = Arcsecond;
293
+ /// Convenience alias for an arcsecond quantity.
294
+ pub type Arcseconds = Quantity<Arcs>;
295
+ /// One arcsecond.
296
+ pub const ARCS: Arcseconds = Arcseconds::new(1.0);
297
+
298
+ /// Milliarcsecond (`1/3_600_000` degree).
299
+ #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
300
+ #[unit(symbol = "mas", dimension = Angular, ratio = 1.0 / 3_600_000.0)]
301
+ pub struct MilliArcsecond;
302
+ /// Type alias shorthand for [`MilliArcsecond`].
303
+ pub type Mas = MilliArcsecond;
304
+ /// Convenience alias for a milliarcsecond quantity.
305
+ pub type MilliArcseconds = Quantity<Mas>;
306
+ /// One milliarcsecond.
307
+ pub const MAS: MilliArcseconds = MilliArcseconds::new(1.0);
308
+
309
+ /// Microarcsecond (`1/3_600_000_000` degree).
310
+ #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
311
+ #[unit(symbol = "μas", dimension = Angular, ratio = 1.0 / 3_600_000_000.0)]
312
+ pub struct MicroArcsecond;
313
+ /// Type alias shorthand for [`MicroArcsecond`].
314
+ pub type Uas = MicroArcsecond;
315
+ /// Convenience alias for a microarcsecond quantity.
316
+ pub type MicroArcseconds = Quantity<Uas>;
317
+ /// One microarcsecond.
318
+ pub const UAS: MicroArcseconds = MicroArcseconds::new(1.0);
319
+
320
+ /// Gradian (also called gon; `1/400` of a full turn = `0.9` degree).
321
+ #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
322
+ #[unit(symbol = "gon", dimension = Angular, ratio = 0.9)]
323
+ pub struct Gradian;
324
+ /// Type alias shorthand for [`Gradian`].
325
+ pub type Gon = Gradian;
326
+ /// Convenience alias for a gradian quantity.
327
+ pub type Gradians = Quantity<Gon>;
328
+ /// One gradian.
329
+ pub const GON: Gradians = Gradians::new(1.0);
330
+
331
+ /// Turn (full revolution; `360` degrees).
332
+ #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
333
+ #[unit(symbol = "turn", dimension = Angular, ratio = 360.0)]
334
+ pub struct Turn;
335
+ /// Convenience alias for a turn quantity.
336
+ pub type Turns = Quantity<Turn>;
337
+ /// One turn.
338
+ pub const TURN: Turns = Turns::new(1.0);
339
+
340
+ /// Hour angle hour (`15` degrees).
341
+ #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
342
+ #[unit(symbol = "h", dimension = Angular, ratio = 15.0)]
343
+ pub struct HourAngle;
344
+ /// Type alias shorthand for [`HourAngle`].
345
+ pub type Hms = HourAngle;
346
+ /// Convenience alias for an hour-angle quantity.
347
+ pub type HourAngles = Quantity<Hms>;
348
+ /// One hour angle hour (==15°).
349
+ pub const HOUR_ANGLE: HourAngles = HourAngles::new(1.0);
350
+
351
+ impl HourAngles {
352
+ /// Construct from **HMS** components (`hours`, `minutes`, `seconds`).
353
+ ///
354
+ /// Sign is taken from `hours`; the `minutes` and `seconds` parameters are treated as magnitudes.
355
+ ///
356
+ /// ```rust
357
+ /// use qtty_core::angular::HourAngles;
358
+ /// let ra = HourAngles::from_hms(5, 30, 0.0); // 5h30m == 5.5h
359
+ /// assert_eq!(ra.value(), 5.5);
360
+ /// ```
361
+ pub const fn from_hms(hours: i32, minutes: u32, seconds: f64) -> Self {
362
+ let sign = if hours < 0 { -1.0 } else { 1.0 };
363
+ let h_abs = if hours < 0 { -hours } else { hours } as f64;
364
+ let m = minutes as f64 / 60.0;
365
+ let s = seconds / 3600.0;
366
+ let total_hours = sign * (h_abs + m + s);
367
+ Self::new(total_hours)
368
+ }
369
+ }
370
+
371
+ impl Degrees {
372
+ /// Construct from **DMS** components (`deg`, `min`, `sec`).
373
+ ///
374
+ /// Sign is taken from `deg`; the magnitude of `min` and `sec` is always added.
375
+ /// No range checking is performed. Use one of the wrapping helpers if you need a canonical range.
376
+ ///
377
+ /// ```rust
378
+ /// use qtty_core::angular::Degrees;
379
+ /// let lat = Degrees::from_dms(-33, 52, 0.0); // −33°52′00″
380
+ /// assert!(lat.value() < 0.0);
381
+ /// ```
382
+ pub const fn from_dms(deg: i32, min: u32, sec: f64) -> Self {
383
+ let sign = if deg < 0 { -1.0 } else { 1.0 };
384
+ let d_abs = if deg < 0 { -deg } else { deg } as f64;
385
+ let m = min as f64 / 60.0;
386
+ let s = sec / 3600.0;
387
+ let total = sign * (d_abs + m + s);
388
+ Self::new(total)
389
+ }
390
+
391
+ /// Construct from explicit sign and magnitude components.
392
+ ///
393
+ /// `sign` should be −1, 0, or +1 (0 treated as +1 unless all components are zero).
394
+ pub const fn from_dms_sign(sign: i8, deg: u32, min: u32, sec: f64) -> Self {
395
+ let s = if sign < 0 { -1.0 } else { 1.0 };
396
+ let total = (deg as f64) + (min as f64) / 60.0 + (sec / 3600.0);
397
+ Self::new(s * total)
398
+ }
399
+ }
400
+
401
+ // Generate all bidirectional From implementations between angular units.
402
+ crate::impl_unit_from_conversions!(
403
+ Degree,
404
+ Radian,
405
+ Milliradian,
406
+ Arcminute,
407
+ Arcsecond,
408
+ MilliArcsecond,
409
+ MicroArcsecond,
410
+ Gradian,
411
+ Turn,
412
+ HourAngle
413
+ );
414
+
415
+ // Optional cross-unit operator support (`==`, `<`, etc.).
416
+ #[cfg(feature = "cross-unit-ops")]
417
+ crate::impl_unit_cross_unit_ops!(
418
+ Degree,
419
+ Radian,
420
+ Milliradian,
421
+ Arcminute,
422
+ Arcsecond,
423
+ MilliArcsecond,
424
+ MicroArcsecond,
425
+ Gradian,
426
+ Turn,
427
+ HourAngle
428
+ );
429
+
430
+ #[cfg(test)]
431
+ mod tests {
432
+ use super::*;
433
+ use approx::{assert_abs_diff_eq, assert_relative_eq};
434
+ use proptest::prelude::*;
435
+ use std::f64::consts::{PI, TAU};
436
+
437
+ // ─────────────────────────────────────────────────────────────────────────────
438
+ // Angular unit constants
439
+ // ─────────────────────────────────────────────────────────────────────────────
440
+
441
+ #[test]
442
+ fn test_full_turn() {
443
+ assert_abs_diff_eq!(Radian::FULL_TURN, TAU, epsilon = 1e-12);
444
+ assert_eq!(Degree::FULL_TURN, 360.0);
445
+ assert_eq!(Arcsecond::FULL_TURN, 1_296_000.0);
446
+ }
447
+
448
+ #[test]
449
+ fn test_half_turn() {
450
+ assert_abs_diff_eq!(Radian::HALF_TURN, PI, epsilon = 1e-12);
451
+ assert_eq!(Degree::HALF_TURN, 180.0);
452
+ assert_eq!(Arcsecond::HALF_TURN, 648_000.0);
453
+ }
454
+
455
+ #[test]
456
+ fn test_quarter_turn() {
457
+ assert_abs_diff_eq!(Radian::QUARTED_TURN, PI / 2.0, epsilon = 1e-12);
458
+ assert_eq!(Degree::QUARTED_TURN, 90.0);
459
+ assert_eq!(Arcsecond::QUARTED_TURN, 324_000.0);
460
+ }
461
+
462
+ #[test]
463
+ fn test_quantity_constants() {
464
+ assert_eq!(Degrees::FULL_TURN.value(), 360.0);
465
+ assert_eq!(Degrees::HALF_TURN.value(), 180.0);
466
+ assert_eq!(Degrees::QUARTED_TURN.value(), 90.0);
467
+ assert_eq!(Degrees::TAU.value(), 360.0);
468
+ }
469
+
470
+ // ─────────────────────────────────────────────────────────────────────────────
471
+ // Conversions
472
+ // ─────────────────────────────────────────────────────────────────────────────
473
+
474
+ #[test]
475
+ fn conversion_degrees_to_radians() {
476
+ let deg = Degrees::new(180.0);
477
+ let rad = deg.to::<Radian>();
478
+ assert_abs_diff_eq!(rad.value(), PI, epsilon = 1e-12);
479
+ }
480
+
481
+ #[test]
482
+ fn conversion_radians_to_degrees() {
483
+ let rad = Radians::new(PI);
484
+ let deg = rad.to::<Degree>();
485
+ assert_abs_diff_eq!(deg.value(), 180.0, epsilon = 1e-12);
486
+ }
487
+
488
+ #[test]
489
+ fn conversion_degrees_to_arcseconds() {
490
+ let deg = Degrees::new(1.0);
491
+ let arcs = deg.to::<Arcsecond>();
492
+ assert_abs_diff_eq!(arcs.value(), 3600.0, epsilon = 1e-9);
493
+ }
494
+
495
+ #[test]
496
+ fn conversion_arcseconds_to_degrees() {
497
+ let arcs = Arcseconds::new(3600.0);
498
+ let deg = arcs.to::<Degree>();
499
+ assert_abs_diff_eq!(deg.value(), 1.0, epsilon = 1e-12);
500
+ }
501
+
502
+ #[test]
503
+ fn conversion_degrees_to_milliarcseconds() {
504
+ let deg = Degrees::new(1.0);
505
+ let mas = deg.to::<MilliArcsecond>();
506
+ assert_abs_diff_eq!(mas.value(), 3_600_000.0, epsilon = 1e-6);
507
+ }
508
+
509
+ #[test]
510
+ fn conversion_hour_angles_to_degrees() {
511
+ let ha = HourAngles::new(1.0);
512
+ let deg = ha.to::<Degree>();
513
+ assert_abs_diff_eq!(deg.value(), 15.0, epsilon = 1e-12);
514
+ }
515
+
516
+ #[test]
517
+ fn conversion_roundtrip() {
518
+ let original = Degrees::new(123.456);
519
+ let rad = original.to::<Radian>();
520
+ let back = rad.to::<Degree>();
521
+ assert_abs_diff_eq!(back.value(), original.value(), epsilon = 1e-12);
522
+ }
523
+
524
+ #[test]
525
+ fn from_impl_degrees_radians() {
526
+ let deg = Degrees::new(90.0);
527
+ let rad: Radians = deg.into();
528
+ assert_abs_diff_eq!(rad.value(), PI / 2.0, epsilon = 1e-12);
529
+
530
+ let rad2 = Radians::new(PI);
531
+ let deg2: Degrees = rad2.into();
532
+ assert_abs_diff_eq!(deg2.value(), 180.0, epsilon = 1e-12);
533
+ }
534
+
535
+ // ─────────────────────────────────────────────────────────────────────────────
536
+ // Trig functions
537
+ // ─────────────────────────────────────────────────────────────────────────────
538
+
539
+ #[test]
540
+ fn test_trig() {
541
+ let a = Degrees::new(90.0);
542
+ assert!((a.sin() - 1.0).abs() < 1e-12);
543
+ assert!(a.cos().abs() < 1e-12);
544
+ }
545
+
546
+ #[test]
547
+ fn trig_sin_known_values() {
548
+ assert_abs_diff_eq!(Degrees::new(0.0).sin(), 0.0, epsilon = 1e-12);
549
+ assert_abs_diff_eq!(Degrees::new(30.0).sin(), 0.5, epsilon = 1e-12);
550
+ assert_abs_diff_eq!(Degrees::new(90.0).sin(), 1.0, epsilon = 1e-12);
551
+ assert_abs_diff_eq!(Degrees::new(180.0).sin(), 0.0, epsilon = 1e-12);
552
+ assert_abs_diff_eq!(Degrees::new(270.0).sin(), -1.0, epsilon = 1e-12);
553
+ }
554
+
555
+ #[test]
556
+ fn trig_cos_known_values() {
557
+ assert_abs_diff_eq!(Degrees::new(0.0).cos(), 1.0, epsilon = 1e-12);
558
+ assert_abs_diff_eq!(Degrees::new(60.0).cos(), 0.5, epsilon = 1e-12);
559
+ assert_abs_diff_eq!(Degrees::new(90.0).cos(), 0.0, epsilon = 1e-12);
560
+ assert_abs_diff_eq!(Degrees::new(180.0).cos(), -1.0, epsilon = 1e-12);
561
+ }
562
+
563
+ #[test]
564
+ fn trig_tan_known_values() {
565
+ assert_abs_diff_eq!(Degrees::new(0.0).tan(), 0.0, epsilon = 1e-12);
566
+ assert_abs_diff_eq!(Degrees::new(45.0).tan(), 1.0, epsilon = 1e-12);
567
+ assert_abs_diff_eq!(Degrees::new(180.0).tan(), 0.0, epsilon = 1e-12);
568
+ }
569
+
570
+ #[test]
571
+ fn trig_sin_cos_consistency() {
572
+ let angle = Degrees::new(37.5);
573
+ let (sin, cos) = angle.sin_cos();
574
+ assert_abs_diff_eq!(sin, angle.sin(), epsilon = 1e-15);
575
+ assert_abs_diff_eq!(cos, angle.cos(), epsilon = 1e-15);
576
+ }
577
+
578
+ #[test]
579
+ fn trig_pythagorean_identity() {
580
+ let angle = Degrees::new(123.456);
581
+ let sin = angle.sin();
582
+ let cos = angle.cos();
583
+ assert_abs_diff_eq!(sin * sin + cos * cos, 1.0, epsilon = 1e-12);
584
+ }
585
+
586
+ #[test]
587
+ fn trig_radians() {
588
+ assert_abs_diff_eq!(Radians::new(0.0).sin(), 0.0, epsilon = 1e-12);
589
+ assert_abs_diff_eq!(Radians::new(PI / 2.0).sin(), 1.0, epsilon = 1e-12);
590
+ assert_abs_diff_eq!(Radians::new(PI).cos(), -1.0, epsilon = 1e-12);
591
+ }
592
+
593
+ // ─────────────────────────────────────────────────────────────────────────────
594
+ // signum
595
+ // ─────────────────────────────────────────────────────────────────────────────
596
+
597
+ #[test]
598
+ fn signum_positive() {
599
+ assert_eq!(Degrees::new(45.0).signum(), 1.0);
600
+ }
601
+
602
+ #[test]
603
+ fn signum_negative() {
604
+ assert_eq!(Degrees::new(-45.0).signum(), -1.0);
605
+ }
606
+
607
+ #[test]
608
+ fn signum_zero() {
609
+ assert_eq!(Degrees::new(0.0).signum(), 1.0);
610
+ }
611
+
612
+ // ─────────────────────────────────────────────────────────────────────────────
613
+ // wrap_pos (normalize)
614
+ // ─────────────────────────────────────────────────────────────────────────────
615
+
616
+ #[test]
617
+ fn wrap_pos_basic() {
618
+ assert_abs_diff_eq!(
619
+ Degrees::new(370.0).wrap_pos().value(),
620
+ 10.0,
621
+ epsilon = 1e-12
622
+ );
623
+ assert_abs_diff_eq!(Degrees::new(720.0).wrap_pos().value(), 0.0, epsilon = 1e-12);
624
+ assert_abs_diff_eq!(Degrees::new(0.0).wrap_pos().value(), 0.0, epsilon = 1e-12);
625
+ }
626
+
627
+ #[test]
628
+ fn wrap_pos_negative() {
629
+ assert_abs_diff_eq!(
630
+ Degrees::new(-10.0).wrap_pos().value(),
631
+ 350.0,
632
+ epsilon = 1e-12
633
+ );
634
+ assert_abs_diff_eq!(
635
+ Degrees::new(-370.0).wrap_pos().value(),
636
+ 350.0,
637
+ epsilon = 1e-12
638
+ );
639
+ assert_abs_diff_eq!(
640
+ Degrees::new(-720.0).wrap_pos().value(),
641
+ 0.0,
642
+ epsilon = 1e-12
643
+ );
644
+ }
645
+
646
+ #[test]
647
+ fn wrap_pos_boundary() {
648
+ assert_abs_diff_eq!(Degrees::new(360.0).wrap_pos().value(), 0.0, epsilon = 1e-12);
649
+ assert_abs_diff_eq!(
650
+ Degrees::new(-360.0).wrap_pos().value(),
651
+ 0.0,
652
+ epsilon = 1e-12
653
+ );
654
+ }
655
+
656
+ #[test]
657
+ fn normalize_is_wrap_pos() {
658
+ let angle = Degrees::new(450.0);
659
+ assert_eq!(angle.normalize().value(), angle.wrap_pos().value());
660
+ }
661
+
662
+ // ─────────────────────────────────────────────────────────────────────────────
663
+ // wrap_signed: (-180, 180]
664
+ // ─────────────────────────────────────────────────────────────────────────────
665
+
666
+ #[test]
667
+ fn test_wrap_signed() {
668
+ let a = Degrees::new(370.0).wrap_signed();
669
+ assert_eq!(a.value(), 10.0);
670
+ let b = Degrees::new(-190.0).wrap_signed();
671
+ assert_eq!(b.value(), 170.0);
672
+ }
673
+
674
+ #[test]
675
+ fn wrap_signed_basic() {
676
+ assert_abs_diff_eq!(
677
+ Degrees::new(10.0).wrap_signed().value(),
678
+ 10.0,
679
+ epsilon = 1e-12
680
+ );
681
+ assert_abs_diff_eq!(
682
+ Degrees::new(-10.0).wrap_signed().value(),
683
+ -10.0,
684
+ epsilon = 1e-12
685
+ );
686
+ }
687
+
688
+ #[test]
689
+ fn wrap_signed_over_180() {
690
+ assert_abs_diff_eq!(
691
+ Degrees::new(190.0).wrap_signed().value(),
692
+ -170.0,
693
+ epsilon = 1e-12
694
+ );
695
+ assert_abs_diff_eq!(
696
+ Degrees::new(270.0).wrap_signed().value(),
697
+ -90.0,
698
+ epsilon = 1e-12
699
+ );
700
+ }
701
+
702
+ #[test]
703
+ fn wrap_signed_boundary_180() {
704
+ assert_abs_diff_eq!(
705
+ Degrees::new(180.0).wrap_signed().value(),
706
+ 180.0,
707
+ epsilon = 1e-12
708
+ );
709
+ assert_abs_diff_eq!(
710
+ Degrees::new(-180.0).wrap_signed().value(),
711
+ 180.0,
712
+ epsilon = 1e-12
713
+ );
714
+ }
715
+
716
+ #[test]
717
+ fn wrap_signed_large_values() {
718
+ assert_abs_diff_eq!(
719
+ Degrees::new(540.0).wrap_signed().value(),
720
+ 180.0,
721
+ epsilon = 1e-12
722
+ );
723
+ assert_abs_diff_eq!(
724
+ Degrees::new(-540.0).wrap_signed().value(),
725
+ 180.0,
726
+ epsilon = 1e-12
727
+ );
728
+ }
729
+
730
+ // ─────────────────────────────────────────────────────────────────────────────
731
+ // wrap_quarter_fold: [-90, 90]
732
+ // ─────────────────────────────────────────────────────────────────────────────
733
+
734
+ #[test]
735
+ fn wrap_quarter_fold_basic() {
736
+ assert_abs_diff_eq!(
737
+ Degrees::new(0.0).wrap_quarter_fold().value(),
738
+ 0.0,
739
+ epsilon = 1e-12
740
+ );
741
+ assert_abs_diff_eq!(
742
+ Degrees::new(45.0).wrap_quarter_fold().value(),
743
+ 45.0,
744
+ epsilon = 1e-12
745
+ );
746
+ assert_abs_diff_eq!(
747
+ Degrees::new(-45.0).wrap_quarter_fold().value(),
748
+ -45.0,
749
+ epsilon = 1e-12
750
+ );
751
+ }
752
+
753
+ #[test]
754
+ fn wrap_quarter_fold_boundary() {
755
+ assert_abs_diff_eq!(
756
+ Degrees::new(90.0).wrap_quarter_fold().value(),
757
+ 90.0,
758
+ epsilon = 1e-12
759
+ );
760
+ assert_abs_diff_eq!(
761
+ Degrees::new(-90.0).wrap_quarter_fold().value(),
762
+ -90.0,
763
+ epsilon = 1e-12
764
+ );
765
+ }
766
+
767
+ #[test]
768
+ fn wrap_quarter_fold_over_90() {
769
+ assert_abs_diff_eq!(
770
+ Degrees::new(100.0).wrap_quarter_fold().value(),
771
+ 80.0,
772
+ epsilon = 1e-12
773
+ );
774
+ assert_abs_diff_eq!(
775
+ Degrees::new(135.0).wrap_quarter_fold().value(),
776
+ 45.0,
777
+ epsilon = 1e-12
778
+ );
779
+ assert_abs_diff_eq!(
780
+ Degrees::new(180.0).wrap_quarter_fold().value(),
781
+ 0.0,
782
+ epsilon = 1e-12
783
+ );
784
+ }
785
+
786
+ // ─────────────────────────────────────────────────────────────────────────────
787
+ // Separation helpers
788
+ // ─────────────────────────────────────────────────────────────────────────────
789
+
790
+ #[test]
791
+ fn signed_separation_basic() {
792
+ let a = Degrees::new(30.0);
793
+ let b = Degrees::new(50.0);
794
+ assert_abs_diff_eq!(a.signed_separation(b).value(), -20.0, epsilon = 1e-12);
795
+ assert_abs_diff_eq!(b.signed_separation(a).value(), 20.0, epsilon = 1e-12);
796
+ }
797
+
798
+ #[test]
799
+ fn signed_separation_wrap() {
800
+ let a = Degrees::new(10.0);
801
+ let b = Degrees::new(350.0);
802
+ assert_abs_diff_eq!(a.signed_separation(b).value(), 20.0, epsilon = 1e-12);
803
+ assert_abs_diff_eq!(b.signed_separation(a).value(), -20.0, epsilon = 1e-12);
804
+ }
805
+
806
+ #[test]
807
+ fn abs_separation() {
808
+ let a = Degrees::new(30.0);
809
+ let b = Degrees::new(50.0);
810
+ assert_abs_diff_eq!(a.abs_separation(b).value(), 20.0, epsilon = 1e-12);
811
+ assert_abs_diff_eq!(b.abs_separation(a).value(), 20.0, epsilon = 1e-12);
812
+ }
813
+
814
+ // ─────────────────────────────────────────────────────────────────────────────
815
+ // DMS / HMS construction
816
+ // ─────────────────────────────────────────────────────────────────────────────
817
+
818
+ #[test]
819
+ fn degrees_from_dms_positive() {
820
+ let d = Degrees::from_dms(12, 30, 0.0);
821
+ assert_abs_diff_eq!(d.value(), 12.5, epsilon = 1e-12);
822
+ }
823
+
824
+ #[test]
825
+ fn degrees_from_dms_negative() {
826
+ let d = Degrees::from_dms(-33, 52, 0.0);
827
+ assert!(d.value() < 0.0);
828
+ assert_abs_diff_eq!(d.value(), -(33.0 + 52.0 / 60.0), epsilon = 1e-12);
829
+ }
830
+
831
+ #[test]
832
+ fn degrees_from_dms_with_seconds() {
833
+ let d = Degrees::from_dms(10, 20, 30.0);
834
+ assert_abs_diff_eq!(
835
+ d.value(),
836
+ 10.0 + 20.0 / 60.0 + 30.0 / 3600.0,
837
+ epsilon = 1e-12
838
+ );
839
+ }
840
+
841
+ #[test]
842
+ fn degrees_from_dms_sign() {
843
+ let pos = Degrees::from_dms_sign(1, 45, 30, 0.0);
844
+ let neg = Degrees::from_dms_sign(-1, 45, 30, 0.0);
845
+ assert_abs_diff_eq!(pos.value(), 45.5, epsilon = 1e-12);
846
+ assert_abs_diff_eq!(neg.value(), -45.5, epsilon = 1e-12);
847
+ }
848
+
849
+ #[test]
850
+ fn hour_angles_from_hms() {
851
+ let ha = HourAngles::from_hms(5, 30, 0.0);
852
+ assert_abs_diff_eq!(ha.value(), 5.5, epsilon = 1e-12);
853
+ }
854
+
855
+ #[test]
856
+ fn hour_angles_from_hms_negative() {
857
+ let ha = HourAngles::from_hms(-3, 15, 0.0);
858
+ assert_abs_diff_eq!(ha.value(), -3.25, epsilon = 1e-12);
859
+ }
860
+
861
+ #[test]
862
+ fn hour_angles_to_degrees() {
863
+ let ha = HourAngles::new(6.0);
864
+ let deg = ha.to::<Degree>();
865
+ assert_abs_diff_eq!(deg.value(), 90.0, epsilon = 1e-12);
866
+ }
867
+
868
+ // ─────────────────────────────────────────────────────────────────────────────
869
+ // Display formatting
870
+ // ─────────────────────────────────────────────────────────────────────────────
871
+
872
+ #[test]
873
+ fn display_degrees() {
874
+ let d = Degrees::new(45.5);
875
+ assert_eq!(format!("{}", d), "45.5 °");
876
+ }
877
+
878
+ #[test]
879
+ fn display_radians() {
880
+ let r = Radians::new(1.0);
881
+ assert_eq!(format!("{}", r), "1 rad");
882
+ }
883
+
884
+ // ─────────────────────────────────────────────────────────────────────────────
885
+ // Unit constants
886
+ // ─────────────────────────────────────────────────────────────────────────────
887
+
888
+ #[test]
889
+ fn unit_constants() {
890
+ assert_eq!(DEG.value(), 1.0);
891
+ assert_eq!(RAD.value(), 1.0);
892
+ assert_eq!(MRAD.value(), 1.0);
893
+ assert_eq!(ARCM.value(), 1.0);
894
+ assert_eq!(ARCS.value(), 1.0);
895
+ assert_eq!(MAS.value(), 1.0);
896
+ assert_eq!(UAS.value(), 1.0);
897
+ assert_eq!(GON.value(), 1.0);
898
+ assert_eq!(TURN.value(), 1.0);
899
+ assert_eq!(HOUR_ANGLE.value(), 1.0);
900
+ }
901
+
902
+ // ─────────────────────────────────────────────────────────────────────────────
903
+ // wrap_signed_lo: [-180, 180)
904
+ // ─────────────────────────────────────────────────────────────────────────────
905
+
906
+ #[test]
907
+ fn wrap_signed_lo_boundary_half_turn() {
908
+ // +half turn should map to -half turn to make the upper bound exclusive.
909
+ assert_abs_diff_eq!(
910
+ Degrees::new(180.0).wrap_signed_lo().value(),
911
+ -180.0,
912
+ epsilon = 1e-12
913
+ );
914
+ assert_abs_diff_eq!(
915
+ Degrees::new(-180.0).wrap_signed_lo().value(),
916
+ -180.0,
917
+ epsilon = 1e-12
918
+ );
919
+ }
920
+
921
+ // ─────────────────────────────────────────────────────────────────────────────
922
+ // New unit conversions and tests
923
+ // ─────────────────────────────────────────────────────────────────────────────
924
+
925
+ #[test]
926
+ fn conversion_degrees_to_arcminutes() {
927
+ let deg = Degrees::new(1.0);
928
+ let arcm = deg.to::<Arcminute>();
929
+ assert_abs_diff_eq!(arcm.value(), 60.0, epsilon = 1e-12);
930
+ }
931
+
932
+ #[test]
933
+ fn conversion_arcminutes_to_degrees() {
934
+ let arcm = Arcminutes::new(60.0);
935
+ let deg = arcm.to::<Degree>();
936
+ assert_abs_diff_eq!(deg.value(), 1.0, epsilon = 1e-12);
937
+ }
938
+
939
+ #[test]
940
+ fn conversion_arcminutes_to_arcseconds() {
941
+ let arcm = Arcminutes::new(1.0);
942
+ let arcs = arcm.to::<Arcsecond>();
943
+ assert_abs_diff_eq!(arcs.value(), 60.0, epsilon = 1e-12);
944
+ }
945
+
946
+ #[test]
947
+ fn conversion_arcseconds_to_microarcseconds() {
948
+ let arcs = Arcseconds::new(1.0);
949
+ let uas = arcs.to::<MicroArcsecond>();
950
+ assert_abs_diff_eq!(uas.value(), 1_000_000.0, epsilon = 1e-6);
951
+ }
952
+
953
+ #[test]
954
+ fn conversion_microarcseconds_to_degrees() {
955
+ let uas = MicroArcseconds::new(3_600_000_000.0);
956
+ let deg = uas.to::<Degree>();
957
+ assert_abs_diff_eq!(deg.value(), 1.0, epsilon = 1e-9);
958
+ }
959
+
960
+ #[test]
961
+ fn conversion_degrees_to_gradians() {
962
+ let deg = Degrees::new(90.0);
963
+ let gon = deg.to::<Gradian>();
964
+ assert_abs_diff_eq!(gon.value(), 100.0, epsilon = 1e-12);
965
+ }
966
+
967
+ #[test]
968
+ fn conversion_gradians_to_degrees() {
969
+ let gon = Gradians::new(400.0);
970
+ let deg = gon.to::<Degree>();
971
+ assert_abs_diff_eq!(deg.value(), 360.0, epsilon = 1e-12);
972
+ }
973
+
974
+ #[test]
975
+ fn conversion_gradians_to_radians() {
976
+ let gon = Gradians::new(200.0);
977
+ let rad = gon.to::<Radian>();
978
+ assert_abs_diff_eq!(rad.value(), PI, epsilon = 1e-12);
979
+ }
980
+
981
+ #[test]
982
+ fn conversion_degrees_to_turns() {
983
+ let deg = Degrees::new(360.0);
984
+ let turn = deg.to::<Turn>();
985
+ assert_abs_diff_eq!(turn.value(), 1.0, epsilon = 1e-12);
986
+ }
987
+
988
+ #[test]
989
+ fn conversion_milliradians_to_radians() {
990
+ let mrad = Milliradians::new(1_000.0);
991
+ let rad = mrad.to::<Radian>();
992
+ assert_abs_diff_eq!(rad.value(), 1.0, epsilon = 1e-12);
993
+ }
994
+
995
+ #[test]
996
+ fn conversion_turns_to_degrees() {
997
+ let turn = Turns::new(2.5);
998
+ let deg = turn.to::<Degree>();
999
+ assert_abs_diff_eq!(deg.value(), 900.0, epsilon = 1e-12);
1000
+ }
1001
+
1002
+ #[test]
1003
+ fn conversion_turns_to_radians() {
1004
+ let turn = Turns::new(1.0);
1005
+ let rad = turn.to::<Radian>();
1006
+ assert_abs_diff_eq!(rad.value(), TAU, epsilon = 1e-12);
1007
+ }
1008
+
1009
+ #[test]
1010
+ fn from_impl_new_units() {
1011
+ // Test From trait implementations for new units
1012
+ let deg = Degrees::new(1.0);
1013
+ let arcm: Arcminutes = deg.into();
1014
+ assert_abs_diff_eq!(arcm.value(), 60.0, epsilon = 1e-12);
1015
+
1016
+ let gon = Gradians::new(100.0);
1017
+ let deg2: Degrees = gon.into();
1018
+ assert_abs_diff_eq!(deg2.value(), 90.0, epsilon = 1e-12);
1019
+
1020
+ let turn = Turns::new(0.25);
1021
+ let deg3: Degrees = turn.into();
1022
+ assert_abs_diff_eq!(deg3.value(), 90.0, epsilon = 1e-12);
1023
+ }
1024
+
1025
+ #[test]
1026
+ fn roundtrip_arcminute_arcsecond() {
1027
+ let original = Arcminutes::new(5.0);
1028
+ let arcs = original.to::<Arcsecond>();
1029
+ let back = arcs.to::<Arcminute>();
1030
+ assert_abs_diff_eq!(back.value(), original.value(), epsilon = 1e-12);
1031
+ }
1032
+
1033
+ #[test]
1034
+ fn roundtrip_gradian_degree() {
1035
+ let original = Gradians::new(123.456);
1036
+ let deg = original.to::<Degree>();
1037
+ let back = deg.to::<Gradian>();
1038
+ assert_abs_diff_eq!(back.value(), original.value(), epsilon = 1e-12);
1039
+ }
1040
+
1041
+ #[test]
1042
+ fn roundtrip_turn_radian() {
1043
+ let original = Turns::new(2.717);
1044
+ let rad = original.to::<Radian>();
1045
+ let back = rad.to::<Turn>();
1046
+ assert_abs_diff_eq!(back.value(), original.value(), epsilon = 1e-12);
1047
+ }
1048
+
1049
+ #[test]
1050
+ fn gradian_full_turn() {
1051
+ assert_abs_diff_eq!(Gradian::FULL_TURN, 400.0, epsilon = 1e-12);
1052
+ }
1053
+
1054
+ #[test]
1055
+ fn turn_full_turn() {
1056
+ assert_abs_diff_eq!(Turn::FULL_TURN, 1.0, epsilon = 1e-12);
1057
+ }
1058
+
1059
+ #[test]
1060
+ fn arcminute_full_turn() {
1061
+ assert_abs_diff_eq!(Arcminute::FULL_TURN, 21_600.0, epsilon = 1e-9);
1062
+ }
1063
+
1064
+ #[test]
1065
+ fn microarcsecond_conversion_chain() {
1066
+ // Test a long conversion chain
1067
+ let uas = MicroArcseconds::new(1e9);
1068
+ let mas = uas.to::<MilliArcsecond>();
1069
+ let arcs = mas.to::<Arcsecond>();
1070
+ let arcm = arcs.to::<Arcminute>();
1071
+ let deg = arcm.to::<Degree>();
1072
+
1073
+ assert_abs_diff_eq!(mas.value(), 1_000_000.0, epsilon = 1e-6);
1074
+ assert_abs_diff_eq!(arcs.value(), 1_000.0, epsilon = 1e-9);
1075
+ assert_abs_diff_eq!(arcm.value(), 1_000.0 / 60.0, epsilon = 1e-9);
1076
+ assert_relative_eq!(deg.value(), 1_000.0 / 3600.0, max_relative = 1e-9);
1077
+ }
1078
+
1079
+ #[test]
1080
+ fn wrap_pos_with_turns() {
1081
+ let turn = Turns::new(2.7);
1082
+ let wrapped = turn.wrap_pos();
1083
+ assert_abs_diff_eq!(wrapped.value(), 0.7, epsilon = 1e-12);
1084
+ }
1085
+
1086
+ #[test]
1087
+ fn wrap_signed_with_gradians() {
1088
+ let gon = Gradians::new(350.0);
1089
+ let wrapped = gon.wrap_signed();
1090
+ assert_abs_diff_eq!(wrapped.value(), -50.0, epsilon = 1e-12);
1091
+ }
1092
+
1093
+ #[test]
1094
+ fn trig_with_gradians() {
1095
+ let gon = Gradians::new(100.0); // 90 degrees
1096
+ assert_abs_diff_eq!(gon.sin(), 1.0, epsilon = 1e-12);
1097
+ assert_abs_diff_eq!(gon.cos(), 0.0, epsilon = 1e-12);
1098
+ }
1099
+
1100
+ #[test]
1101
+ fn trig_with_turns() {
1102
+ let turn = Turns::new(0.25); // 90 degrees
1103
+ assert_abs_diff_eq!(turn.sin(), 1.0, epsilon = 1e-12);
1104
+ assert_abs_diff_eq!(turn.cos(), 0.0, epsilon = 1e-12);
1105
+ }
1106
+
1107
+ #[test]
1108
+ fn all_units_to_degrees() {
1109
+ // Verify all units convert correctly to degrees
1110
+ assert_abs_diff_eq!(
1111
+ Radians::new(PI).to::<Degree>().value(),
1112
+ 180.0,
1113
+ epsilon = 1e-12
1114
+ );
1115
+ assert_abs_diff_eq!(
1116
+ Arcminutes::new(60.0).to::<Degree>().value(),
1117
+ 1.0,
1118
+ epsilon = 1e-12
1119
+ );
1120
+ assert_abs_diff_eq!(
1121
+ Arcseconds::new(3600.0).to::<Degree>().value(),
1122
+ 1.0,
1123
+ epsilon = 1e-12
1124
+ );
1125
+ assert_abs_diff_eq!(
1126
+ MilliArcseconds::new(3_600_000.0).to::<Degree>().value(),
1127
+ 1.0,
1128
+ epsilon = 1e-9
1129
+ );
1130
+ assert_abs_diff_eq!(
1131
+ MicroArcseconds::new(3_600_000_000.0).to::<Degree>().value(),
1132
+ 1.0,
1133
+ epsilon = 1e-6
1134
+ );
1135
+ assert_abs_diff_eq!(
1136
+ Gradians::new(100.0).to::<Degree>().value(),
1137
+ 90.0,
1138
+ epsilon = 1e-12
1139
+ );
1140
+ assert_abs_diff_eq!(
1141
+ Turns::new(1.0).to::<Degree>().value(),
1142
+ 360.0,
1143
+ epsilon = 1e-12
1144
+ );
1145
+ assert_abs_diff_eq!(
1146
+ HourAngles::new(1.0).to::<Degree>().value(),
1147
+ 15.0,
1148
+ epsilon = 1e-12
1149
+ );
1150
+ }
1151
+
1152
+ // ─────────────────────────────────────────────────────────────────────────────
1153
+ // Property-based tests
1154
+ // ─────────────────────────────────────────────────────────────────────────────
1155
+
1156
+ proptest! {
1157
+ #[test]
1158
+ fn prop_wrap_pos_range(angle in -1e6..1e6f64) {
1159
+ let wrapped = Degrees::new(angle).wrap_pos();
1160
+ prop_assert!(wrapped.value() >= 0.0);
1161
+ prop_assert!(wrapped.value() < 360.0);
1162
+ }
1163
+
1164
+ #[test]
1165
+ fn prop_wrap_signed_range(angle in -1e6..1e6f64) {
1166
+ let wrapped = Degrees::new(angle).wrap_signed();
1167
+ prop_assert!(wrapped.value() > -180.0);
1168
+ prop_assert!(wrapped.value() <= 180.0);
1169
+ }
1170
+
1171
+ #[test]
1172
+ fn prop_wrap_quarter_fold_range(angle in -1e6..1e6f64) {
1173
+ let wrapped = Degrees::new(angle).wrap_quarter_fold();
1174
+ prop_assert!(wrapped.value() >= -90.0);
1175
+ prop_assert!(wrapped.value() <= 90.0);
1176
+ }
1177
+
1178
+ #[test]
1179
+ fn prop_pythagorean_identity(angle in -360.0..360.0f64) {
1180
+ let a = Degrees::new(angle);
1181
+ let sin = a.sin();
1182
+ let cos = a.cos();
1183
+ assert_abs_diff_eq!(sin * sin + cos * cos, 1.0, epsilon = 1e-12);
1184
+ }
1185
+
1186
+ #[test]
1187
+ fn prop_conversion_roundtrip(angle in -1e6..1e6f64) {
1188
+ let deg = Degrees::new(angle);
1189
+ let rad = deg.to::<Radian>();
1190
+ let back = rad.to::<Degree>();
1191
+ assert_relative_eq!(back.value(), deg.value(), max_relative = 1e-12);
1192
+ }
1193
+
1194
+ #[test]
1195
+ fn prop_abs_separation_symmetric(a in -360.0..360.0f64, b in -360.0..360.0f64) {
1196
+ let da = Degrees::new(a);
1197
+ let db = Degrees::new(b);
1198
+ assert_abs_diff_eq!(
1199
+ da.abs_separation(db).value(),
1200
+ db.abs_separation(da).value(),
1201
+ epsilon = 1e-12
1202
+ );
1203
+ }
1204
+ }
1205
+
1206
+ /// Invoke derive-generated PartialEq/Clone/Debug on each unit struct to
1207
+ /// cover the #[derive] attribute lines tracked by llvm-cov.
1208
+ #[test]
1209
+ fn derive_coverage_unit_structs() {
1210
+ // Direct struct value comparisons invoke <T as PartialEq>::eq() which
1211
+ // is the derive-generated implementation, covering the #[derive] line.
1212
+ assert!(Degree == Degree);
1213
+ assert!(Radian == Radian);
1214
+ assert!(Milliradian == Milliradian);
1215
+ assert!(Arcminute == Arcminute);
1216
+ assert!(Arcsecond == Arcsecond);
1217
+ assert!(MilliArcsecond == MilliArcsecond);
1218
+ assert!(MicroArcsecond == MicroArcsecond);
1219
+ assert!(Gradian == Gradian);
1220
+ assert!(Turn == Turn);
1221
+ assert!(HourAngle == HourAngle);
1222
+ // signum_const: cover both positive and negative branches
1223
+ let pos = Degrees::new(90.0);
1224
+ let neg = Degrees::new(-45.0);
1225
+ assert_eq!(pos.signum_const(), 1.0);
1226
+ assert_eq!(neg.signum_const(), -1.0);
1227
+ }
1228
+ }