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,811 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (C) 2026 Vallés Puig, Ramon
3
+
4
+ //! Generic time–scale parameterised instant.
5
+ //!
6
+ //! [`Time<S>`] is the core type of the time module. It stores a scalar
7
+ //! quantity in [`Days`] whose *meaning* is determined by the compile-time
8
+ //! marker `S: TimeScale`. All arithmetic (addition/subtraction of
9
+ //! durations, difference between instants), UTC conversion, serialisation,
10
+ //! and display are implemented generically — no code duplication.
11
+ //!
12
+ //! Domain-specific methods that only make sense for a particular scale
13
+ //! (e.g. [`Time::<JD>::julian_centuries()`]) are placed in inherent `impl`
14
+ //! blocks gated on the concrete marker type.
15
+
16
+ use chrono::{DateTime, Utc};
17
+ use qtty::*;
18
+ use std::marker::PhantomData;
19
+ use std::ops::{Add, AddAssign, Sub, SubAssign};
20
+
21
+ #[cfg(feature = "serde")]
22
+ use serde::{Deserialize, Deserializer, Serialize, Serializer};
23
+
24
+ // ═══════════════════════════════════════════════════════════════════════════
25
+ // TimeScale trait
26
+ // ═══════════════════════════════════════════════════════════════════════════
27
+
28
+ /// Marker trait for time scales.
29
+ ///
30
+ /// A **time scale** defines:
31
+ ///
32
+ /// 1. A human-readable **label** (e.g. `"JD"`, `"MJD"`, `"TAI"`).
33
+ /// 2. A pair of conversion functions between the scale's native quantity
34
+ /// (in [`Days`]) and **Julian Date in TT** (JD(TT)) — the canonical
35
+ /// internal representation used throughout the crate.
36
+ ///
37
+ /// For pure *epoch counters* (JD, MJD, Unix Time, GPS) the conversions are
38
+ /// trivial constant offsets that the compiler will inline and fold away.
39
+ ///
40
+ /// For *physical scales* (TT, TDB, TAI) the conversions may include
41
+ /// function-based corrections (e.g. the ≈1.7 ms TDB↔TT periodic term).
42
+ pub trait TimeScale: Copy + Clone + std::fmt::Debug + PartialEq + PartialOrd + 'static {
43
+ /// Display label used by [`Time`] formatting.
44
+ const LABEL: &'static str;
45
+
46
+ /// Convert a quantity in this scale's native unit to an absolute JD(TT).
47
+ fn to_jd_tt(value: Days) -> Days;
48
+
49
+ /// Convert an absolute JD(TT) back to this scale's native quantity.
50
+ fn from_jd_tt(jd_tt: Days) -> Days;
51
+ }
52
+
53
+ // ═══════════════════════════════════════════════════════════════════════════
54
+ // Error types
55
+ // ═══════════════════════════════════════════════════════════════════════════
56
+
57
+ /// Error returned when a `Time` value is non-finite (`NaN` or `±∞`).
58
+ ///
59
+ /// Non-finite values break ordering, intersection, and arithmetic invariants,
60
+ /// so validated constructors ([`Time::try_new`], [`Time::try_from_days`])
61
+ /// reject them.
62
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
63
+ pub struct NonFiniteTimeError;
64
+
65
+ impl std::fmt::Display for NonFiniteTimeError {
66
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67
+ write!(f, "time value must be finite (not NaN or infinity)")
68
+ }
69
+ }
70
+
71
+ impl std::error::Error for NonFiniteTimeError {}
72
+
73
+ // ═══════════════════════════════════════════════════════════════════════════
74
+ // Time<S> — the generic instant
75
+ // ═══════════════════════════════════════════════════════════════════════════
76
+
77
+ /// A point on time scale `S`.
78
+ ///
79
+ /// Internally stores a single `Days` quantity whose interpretation depends on
80
+ /// `S: TimeScale`. The struct is `Copy` and zero-cost: `PhantomData` is
81
+ /// zero-sized, so `Time<S>` is layout-identical to `Days` (a single `f64`).
82
+ #[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
83
+ pub struct Time<S: TimeScale> {
84
+ quantity: Days,
85
+ _scale: PhantomData<S>,
86
+ }
87
+
88
+ impl<S: TimeScale> Time<S> {
89
+ // ── constructors ──────────────────────────────────────────────────
90
+
91
+ /// Create from a raw scalar (days since the scale's epoch).
92
+ ///
93
+ /// **Note:** this constructor accepts any `f64`, including `NaN` and `±∞`.
94
+ /// Prefer [`try_new`](Self::try_new) when the value comes from untrusted
95
+ /// or computed input.
96
+ #[inline]
97
+ pub const fn new(value: f64) -> Self {
98
+ Self {
99
+ quantity: Days::new(value),
100
+ _scale: PhantomData,
101
+ }
102
+ }
103
+
104
+ /// Create from a raw scalar, rejecting non-finite values.
105
+ ///
106
+ /// Returns [`NonFiniteTimeError`] if `value` is `NaN`, `+∞`, or `−∞`.
107
+ ///
108
+ /// # Examples
109
+ ///
110
+ /// ```
111
+ /// # use tempoch_core as tempoch;
112
+ /// use tempoch::{Time, JD};
113
+ ///
114
+ /// assert!(Time::<JD>::try_new(2451545.0).is_ok());
115
+ /// assert!(Time::<JD>::try_new(f64::NAN).is_err());
116
+ /// assert!(Time::<JD>::try_new(f64::INFINITY).is_err());
117
+ /// ```
118
+ #[inline]
119
+ pub fn try_new(value: f64) -> Result<Self, NonFiniteTimeError> {
120
+ if value.is_finite() {
121
+ Ok(Self::new(value))
122
+ } else {
123
+ Err(NonFiniteTimeError)
124
+ }
125
+ }
126
+
127
+ /// Create from a [`Days`] quantity.
128
+ ///
129
+ /// **Note:** this constructor accepts any `f64`, including `NaN` and `±∞`.
130
+ /// Prefer [`try_from_days`](Self::try_from_days) when the value comes from
131
+ /// untrusted or computed input.
132
+ #[inline]
133
+ pub const fn from_days(days: Days) -> Self {
134
+ Self {
135
+ quantity: days,
136
+ _scale: PhantomData,
137
+ }
138
+ }
139
+
140
+ /// Create from a [`Days`] quantity, rejecting non-finite values.
141
+ ///
142
+ /// Returns [`NonFiniteTimeError`] if the underlying value is `NaN`,
143
+ /// `+∞`, or `−∞`.
144
+ #[inline]
145
+ pub fn try_from_days(days: Days) -> Result<Self, NonFiniteTimeError> {
146
+ Self::try_new(days.value())
147
+ }
148
+
149
+ // ── accessors ─────────────────────────────────────────────────────
150
+
151
+ /// The underlying quantity in days.
152
+ #[inline]
153
+ pub const fn quantity(&self) -> Days {
154
+ self.quantity
155
+ }
156
+
157
+ /// The underlying scalar value in days.
158
+ #[inline]
159
+ pub const fn value(&self) -> f64 {
160
+ self.quantity.value()
161
+ }
162
+
163
+ /// Absolute Julian Day (TT) corresponding to this instant.
164
+ #[inline]
165
+ pub fn julian_day(&self) -> Days {
166
+ S::to_jd_tt(self.quantity)
167
+ }
168
+
169
+ /// Absolute Julian Day (TT) as scalar.
170
+ #[inline]
171
+ pub fn julian_day_value(&self) -> f64 {
172
+ self.julian_day().value()
173
+ }
174
+
175
+ /// Build an instant from an absolute Julian Day (TT).
176
+ #[inline]
177
+ pub fn from_julian_day(jd: Days) -> Self {
178
+ Self::from_days(S::from_jd_tt(jd))
179
+ }
180
+
181
+ // ── cross-scale conversion (mirroring qtty's .to::<T>()) ─────────
182
+
183
+ /// Convert this instant to another time scale.
184
+ ///
185
+ /// The conversion routes through the canonical JD(TT) intermediate:
186
+ ///
187
+ /// ```text
188
+ /// self → JD(TT) → target
189
+ /// ```
190
+ ///
191
+ /// For pure epoch-offset scales this compiles down to a single
192
+ /// addition/subtraction.
193
+ #[inline]
194
+ pub fn to<T: TimeScale>(&self) -> Time<T> {
195
+ Time::<T>::from_julian_day(S::to_jd_tt(self.quantity))
196
+ }
197
+
198
+ // ── UTC helpers ───────────────────────────────────────────────────
199
+
200
+ /// Convert to a `chrono::DateTime<Utc>`.
201
+ ///
202
+ /// Inverts the ΔT correction to recover the UTC / UT timestamp.
203
+ /// Returns `None` if the value falls outside chrono's representable range.
204
+ pub fn to_utc(&self) -> Option<DateTime<Utc>> {
205
+ use super::scales::UT;
206
+ const UNIX_EPOCH_JD: f64 = 2_440_587.5;
207
+ let jd_ut = self.to::<UT>().quantity();
208
+ let seconds_since_epoch = (jd_ut - Days::new(UNIX_EPOCH_JD)).to::<Second>().value();
209
+ let secs = seconds_since_epoch.floor() as i64;
210
+ let nanos = ((seconds_since_epoch - secs as f64) * 1e9) as u32;
211
+ DateTime::<Utc>::from_timestamp(secs, nanos)
212
+ }
213
+
214
+ /// Build an instant from a `chrono::DateTime<Utc>`.
215
+ ///
216
+ /// The UTC timestamp is interpreted as Universal Time (≈ UT1) and the
217
+ /// epoch-dependent **ΔT** correction is applied automatically, so the
218
+ /// resulting `Time<S>` is on the target scale's axis.
219
+ pub fn from_utc(datetime: DateTime<Utc>) -> Self {
220
+ use super::scales::UT;
221
+ const UNIX_EPOCH_JD: f64 = 2_440_587.5;
222
+ let seconds_since_epoch = Seconds::new(datetime.timestamp() as f64);
223
+ let nanos = Seconds::new(datetime.timestamp_subsec_nanos() as f64 / 1e9);
224
+ let jd_ut = Days::new(UNIX_EPOCH_JD) + (seconds_since_epoch + nanos).to::<Day>();
225
+ Time::<UT>::from_days(jd_ut).to::<S>()
226
+ }
227
+
228
+ // ── min / max ─────────────────────────────────────────────────────
229
+
230
+ /// Element-wise minimum.
231
+ #[inline]
232
+ pub const fn min(self, other: Self) -> Self {
233
+ Self::from_days(self.quantity.min_const(other.quantity))
234
+ }
235
+
236
+ /// Element-wise maximum.
237
+ #[inline]
238
+ pub const fn max(self, other: Self) -> Self {
239
+ Self::from_days(self.quantity.max_const(other.quantity))
240
+ }
241
+
242
+ /// Mean (midpoint) between two instants on the same time scale.
243
+ #[inline]
244
+ pub const fn mean(self, other: Self) -> Self {
245
+ Self::from_days(self.quantity.const_add(other.quantity).const_div(2.0))
246
+ }
247
+ }
248
+
249
+ // ═══════════════════════════════════════════════════════════════════════════
250
+ // Generic trait implementations
251
+ // ═══════════════════════════════════════════════════════════════════════════
252
+
253
+ // ── Display ───────────────────────────────────────────────────────────────
254
+
255
+ impl<S: TimeScale> std::fmt::Display for Time<S> {
256
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257
+ write!(f, "{} {}", S::LABEL, self.quantity)
258
+ }
259
+ }
260
+
261
+ // ── Serde ─────────────────────────────────────────────────────────────────
262
+
263
+ #[cfg(feature = "serde")]
264
+ impl<S: TimeScale> Serialize for Time<S> {
265
+ fn serialize<Ser>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error>
266
+ where
267
+ Ser: Serializer,
268
+ {
269
+ serializer.serialize_f64(self.value())
270
+ }
271
+ }
272
+
273
+ #[cfg(feature = "serde")]
274
+ impl<'de, S: TimeScale> Deserialize<'de> for Time<S> {
275
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
276
+ where
277
+ D: Deserializer<'de>,
278
+ {
279
+ let v = f64::deserialize(deserializer)?;
280
+ if !v.is_finite() {
281
+ return Err(serde::de::Error::custom(
282
+ "time value must be finite (not NaN or infinity)",
283
+ ));
284
+ }
285
+ Ok(Self::new(v))
286
+ }
287
+ }
288
+
289
+ // ── Arithmetic ────────────────────────────────────────────────────────────
290
+
291
+ impl<S: TimeScale> Add<Days> for Time<S> {
292
+ type Output = Self;
293
+ #[inline]
294
+ fn add(self, rhs: Days) -> Self::Output {
295
+ Self::from_days(self.quantity + rhs)
296
+ }
297
+ }
298
+
299
+ impl<S: TimeScale> AddAssign<Days> for Time<S> {
300
+ #[inline]
301
+ fn add_assign(&mut self, rhs: Days) {
302
+ self.quantity += rhs;
303
+ }
304
+ }
305
+
306
+ impl<S: TimeScale> Sub<Days> for Time<S> {
307
+ type Output = Self;
308
+ #[inline]
309
+ fn sub(self, rhs: Days) -> Self::Output {
310
+ Self::from_days(self.quantity - rhs)
311
+ }
312
+ }
313
+
314
+ impl<S: TimeScale> SubAssign<Days> for Time<S> {
315
+ #[inline]
316
+ fn sub_assign(&mut self, rhs: Days) {
317
+ self.quantity -= rhs;
318
+ }
319
+ }
320
+
321
+ impl<S: TimeScale> Sub for Time<S> {
322
+ type Output = Days;
323
+ #[inline]
324
+ fn sub(self, rhs: Self) -> Self::Output {
325
+ self.quantity - rhs.quantity
326
+ }
327
+ }
328
+
329
+ impl<S: TimeScale> std::ops::Div<Days> for Time<S> {
330
+ type Output = f64;
331
+ #[inline]
332
+ fn div(self, rhs: Days) -> Self::Output {
333
+ (self.quantity / rhs).simplify().value()
334
+ }
335
+ }
336
+
337
+ impl<S: TimeScale> std::ops::Div<f64> for Time<S> {
338
+ type Output = f64;
339
+ #[inline]
340
+ fn div(self, rhs: f64) -> Self::Output {
341
+ (self.quantity / rhs).value()
342
+ }
343
+ }
344
+
345
+ // ── From/Into Days ────────────────────────────────────────────────────────
346
+
347
+ impl<S: TimeScale> From<Days> for Time<S> {
348
+ #[inline]
349
+ fn from(days: Days) -> Self {
350
+ Self::from_days(days)
351
+ }
352
+ }
353
+
354
+ impl<S: TimeScale> From<Time<S>> for Days {
355
+ #[inline]
356
+ fn from(time: Time<S>) -> Self {
357
+ time.quantity
358
+ }
359
+ }
360
+
361
+ // ═══════════════════════════════════════════════════════════════════════════
362
+ // TimeInstant trait
363
+ // ═══════════════════════════════════════════════════════════════════════════
364
+
365
+ /// Trait for types that represent a point in time.
366
+ ///
367
+ /// Types implementing this trait can be used as time instants in `Interval<T>`
368
+ /// and provide conversions to/from UTC plus basic arithmetic operations.
369
+ pub trait TimeInstant: Copy + Clone + PartialEq + PartialOrd + Sized {
370
+ /// The duration type used for arithmetic operations.
371
+ type Duration;
372
+
373
+ /// Convert this time instant to UTC DateTime.
374
+ fn to_utc(&self) -> Option<DateTime<Utc>>;
375
+
376
+ /// Create a time instant from UTC DateTime.
377
+ fn from_utc(datetime: DateTime<Utc>) -> Self;
378
+
379
+ /// Compute the difference between two time instants.
380
+ fn difference(&self, other: &Self) -> Self::Duration;
381
+
382
+ /// Add a duration to this time instant.
383
+ fn add_duration(&self, duration: Self::Duration) -> Self;
384
+
385
+ /// Subtract a duration from this time instant.
386
+ fn sub_duration(&self, duration: Self::Duration) -> Self;
387
+ }
388
+
389
+ impl<S: TimeScale> TimeInstant for Time<S> {
390
+ type Duration = Days;
391
+
392
+ #[inline]
393
+ fn to_utc(&self) -> Option<DateTime<Utc>> {
394
+ Time::to_utc(self)
395
+ }
396
+
397
+ #[inline]
398
+ fn from_utc(datetime: DateTime<Utc>) -> Self {
399
+ Time::from_utc(datetime)
400
+ }
401
+
402
+ #[inline]
403
+ fn difference(&self, other: &Self) -> Self::Duration {
404
+ *self - *other
405
+ }
406
+
407
+ #[inline]
408
+ fn add_duration(&self, duration: Self::Duration) -> Self {
409
+ *self + duration
410
+ }
411
+
412
+ #[inline]
413
+ fn sub_duration(&self, duration: Self::Duration) -> Self {
414
+ *self - duration
415
+ }
416
+ }
417
+
418
+ impl TimeInstant for DateTime<Utc> {
419
+ type Duration = chrono::Duration;
420
+
421
+ fn to_utc(&self) -> Option<DateTime<Utc>> {
422
+ Some(*self)
423
+ }
424
+
425
+ fn from_utc(datetime: DateTime<Utc>) -> Self {
426
+ datetime
427
+ }
428
+
429
+ fn difference(&self, other: &Self) -> Self::Duration {
430
+ *self - *other
431
+ }
432
+
433
+ fn add_duration(&self, duration: Self::Duration) -> Self {
434
+ *self + duration
435
+ }
436
+
437
+ fn sub_duration(&self, duration: Self::Duration) -> Self {
438
+ *self - duration
439
+ }
440
+ }
441
+
442
+ // ═══════════════════════════════════════════════════════════════════════════
443
+ // Tests
444
+ // ═══════════════════════════════════════════════════════════════════════════
445
+
446
+ #[cfg(test)]
447
+ mod tests {
448
+ use super::super::scales::{JD, MJD};
449
+ use super::*;
450
+ use chrono::TimeZone;
451
+
452
+ #[test]
453
+ fn test_julian_day_creation() {
454
+ let jd = Time::<JD>::new(2_451_545.0);
455
+ assert_eq!(jd.quantity(), Days::new(2_451_545.0));
456
+ }
457
+
458
+ #[test]
459
+ fn test_jd_utc_roundtrip() {
460
+ // from_utc applies ΔT (UT→TT); to_utc inverts it (TT→UT).
461
+ let datetime = DateTime::from_timestamp(946_728_000, 0).unwrap();
462
+ let jd = Time::<JD>::from_utc(datetime);
463
+ let back = jd.to_utc().expect("to_utc");
464
+ let delta_ns =
465
+ back.timestamp_nanos_opt().unwrap() - datetime.timestamp_nanos_opt().unwrap();
466
+ assert!(delta_ns.abs() < 1_000, "roundtrip error: {} ns", delta_ns);
467
+ }
468
+
469
+ #[test]
470
+ fn test_from_utc_applies_delta_t() {
471
+ // 2000-01-01 12:00:00 UTC → JD(UT)=2451545.0; ΔT≈63.83 s
472
+ let datetime = DateTime::from_timestamp(946_728_000, 0).unwrap();
473
+ let jd = Time::<JD>::from_utc(datetime);
474
+ let delta_t_secs = (jd.quantity() - Days::new(2_451_545.0)).to::<Second>();
475
+ assert!(
476
+ (delta_t_secs - Seconds::new(63.83)).abs() < Seconds::new(1.0),
477
+ "ΔT correction = {} s, expected ~63.83 s",
478
+ delta_t_secs
479
+ );
480
+ }
481
+
482
+ #[test]
483
+ fn test_julian_conversions() {
484
+ let jd = Time::<JD>::J2000 + Days::new(365_250.0);
485
+ assert!((jd.julian_millennias() - Millennia::new(1.0)).abs() < 1e-12);
486
+ assert!((jd.julian_centuries() - Centuries::new(10.0)).abs() < Centuries::new(1e-12));
487
+ assert!((jd.julian_years() - JulianYears::new(1000.0)).abs() < JulianYears::new(1e-9));
488
+ }
489
+
490
+ #[test]
491
+ fn test_tt_to_tdb_and_min_max() {
492
+ let jd_tdb = Time::<JD>::tt_to_tdb(Time::<JD>::J2000);
493
+ assert!((jd_tdb - Time::<JD>::J2000).abs() < 1e-6);
494
+
495
+ let earlier = Time::<JD>::J2000;
496
+ let later = earlier + Days::new(1.0);
497
+ assert_eq!(earlier.min(later), earlier);
498
+ assert_eq!(earlier.max(later), later);
499
+ }
500
+
501
+ #[test]
502
+ fn test_const_min_max() {
503
+ const A: Time<JD> = Time::<JD>::new(10.0);
504
+ const B: Time<JD> = Time::<JD>::new(14.0);
505
+ const MIN: Time<JD> = A.min(B);
506
+ const MAX: Time<JD> = A.max(B);
507
+ assert_eq!(MIN.quantity(), Days::new(10.0));
508
+ assert_eq!(MAX.quantity(), Days::new(14.0));
509
+ }
510
+
511
+ #[test]
512
+ fn test_mean_and_const_mean() {
513
+ let a = Time::<JD>::new(10.0);
514
+ let b = Time::<JD>::new(14.0);
515
+ assert_eq!(a.mean(b).quantity(), Days::new(12.0));
516
+ assert_eq!(b.mean(a).quantity(), Days::new(12.0));
517
+
518
+ const MID: Time<JD> = Time::<JD>::new(10.0).mean(Time::<JD>::new(14.0));
519
+ assert_eq!(MID.quantity(), Days::new(12.0));
520
+ }
521
+
522
+ #[test]
523
+ fn test_into_days() {
524
+ let jd = Time::<JD>::new(2_451_547.5);
525
+ let days: Days = jd.into();
526
+ assert_eq!(days, 2_451_547.5);
527
+
528
+ let roundtrip = Time::<JD>::from(days);
529
+ assert_eq!(roundtrip, jd);
530
+ }
531
+
532
+ #[test]
533
+ fn test_into_julian_years() {
534
+ let jd = Time::<JD>::J2000 + Days::new(365.25 * 2.0);
535
+ let years: JulianYears = jd.into();
536
+ assert!((years - JulianYears::new(2.0)).abs() < JulianYears::new(1e-12));
537
+
538
+ let roundtrip = Time::<JD>::from(years);
539
+ assert!((roundtrip.quantity() - jd.quantity()).abs() < Days::new(1e-12));
540
+ }
541
+
542
+ #[test]
543
+ fn test_into_centuries() {
544
+ let jd = Time::<JD>::J2000 + Days::new(36_525.0 * 3.0);
545
+ let centuries: Centuries = jd.into();
546
+ assert!((centuries - Centuries::new(3.0)).abs() < Centuries::new(1e-12));
547
+
548
+ let roundtrip = Time::<JD>::from(centuries);
549
+ assert!((roundtrip.quantity() - jd.quantity()).abs() < Days::new(1e-12));
550
+ }
551
+
552
+ #[test]
553
+ fn test_into_millennia() {
554
+ let jd = Time::<JD>::J2000 + Days::new(365_250.0 * 1.5);
555
+ let millennia: Millennia = jd.into();
556
+ assert!((millennia - Millennia::new(1.5)).abs() < Millennia::new(1e-12));
557
+
558
+ let roundtrip = Time::<JD>::from(millennia);
559
+ assert!((roundtrip.quantity() - jd.quantity()).abs() < Days::new(1e-9));
560
+ }
561
+
562
+ #[test]
563
+ fn test_mjd_creation() {
564
+ let mjd = Time::<MJD>::new(51_544.5);
565
+ assert_eq!(mjd.quantity(), Days::new(51_544.5));
566
+ }
567
+
568
+ #[test]
569
+ fn test_mjd_into_jd() {
570
+ let mjd = Time::<MJD>::new(51_544.5);
571
+ let jd: Time<JD> = mjd.into();
572
+ assert_eq!(jd.quantity(), Days::new(2_451_545.0));
573
+ }
574
+
575
+ #[test]
576
+ fn test_mjd_utc_roundtrip() {
577
+ let datetime = DateTime::from_timestamp(946_728_000, 0).unwrap();
578
+ let mjd = Time::<MJD>::from_utc(datetime);
579
+ let back = mjd.to_utc().expect("to_utc");
580
+ let delta_ns =
581
+ back.timestamp_nanos_opt().unwrap() - datetime.timestamp_nanos_opt().unwrap();
582
+ assert!(delta_ns.abs() < 1_000, "roundtrip error: {} ns", delta_ns);
583
+ }
584
+
585
+ #[test]
586
+ fn test_mjd_from_utc_applies_delta_t() {
587
+ // MJD epoch is JD − 2400000.5; ΔT should shift value by ~63.83/86400 days
588
+ let datetime = DateTime::from_timestamp(946_728_000, 0).unwrap();
589
+ let mjd = Time::<MJD>::from_utc(datetime);
590
+ let delta_t_secs = (mjd.quantity() - Days::new(51_544.5)).to::<Second>();
591
+ assert!(
592
+ (delta_t_secs - Seconds::new(63.83)).abs() < Seconds::new(1.0),
593
+ "ΔT correction = {} s, expected ~63.83 s",
594
+ delta_t_secs
595
+ );
596
+ }
597
+
598
+ #[test]
599
+ fn test_mjd_add_days() {
600
+ let mjd = Time::<MJD>::new(59_000.0);
601
+ let result = mjd + Days::new(1.5);
602
+ assert_eq!(result.quantity(), Days::new(59_001.5));
603
+ }
604
+
605
+ #[test]
606
+ fn test_mjd_sub_days() {
607
+ let mjd = Time::<MJD>::new(59_000.0);
608
+ let result = mjd - Days::new(1.5);
609
+ assert_eq!(result.quantity(), Days::new(58_998.5));
610
+ }
611
+
612
+ #[test]
613
+ fn test_mjd_sub_mjd() {
614
+ let mjd1 = Time::<MJD>::new(59_001.0);
615
+ let mjd2 = Time::<MJD>::new(59_000.0);
616
+ let diff = mjd1 - mjd2;
617
+ assert_eq!(diff, 1.0);
618
+ }
619
+
620
+ #[test]
621
+ fn test_mjd_comparison() {
622
+ let mjd1 = Time::<MJD>::new(59_000.0);
623
+ let mjd2 = Time::<MJD>::new(59_001.0);
624
+ assert!(mjd1 < mjd2);
625
+ assert!(mjd2 > mjd1);
626
+ }
627
+
628
+ #[test]
629
+ fn test_display_jd() {
630
+ let jd = Time::<JD>::new(2_451_545.0);
631
+ let s = format!("{jd}");
632
+ assert!(s.contains("Julian Day"));
633
+ }
634
+
635
+ #[test]
636
+ fn test_try_new_finite() {
637
+ let jd = Time::<JD>::try_new(2_451_545.0);
638
+ assert!(jd.is_ok());
639
+ assert_eq!(jd.unwrap().value(), 2_451_545.0);
640
+ }
641
+
642
+ #[test]
643
+ fn test_try_new_nan() {
644
+ assert!(Time::<JD>::try_new(f64::NAN).is_err());
645
+ }
646
+
647
+ #[test]
648
+ fn test_try_new_infinity() {
649
+ assert!(Time::<JD>::try_new(f64::INFINITY).is_err());
650
+ assert!(Time::<JD>::try_new(f64::NEG_INFINITY).is_err());
651
+ }
652
+
653
+ #[test]
654
+ fn test_try_from_days() {
655
+ assert!(Time::<JD>::try_from_days(Days::new(100.0)).is_ok());
656
+ assert!(Time::<JD>::try_from_days(Days::new(f64::NAN)).is_err());
657
+ }
658
+
659
+ #[test]
660
+ fn test_display_mjd() {
661
+ let mjd = Time::<MJD>::new(51_544.5);
662
+ let s = format!("{mjd}");
663
+ assert!(s.contains("MJD"));
664
+ }
665
+
666
+ #[test]
667
+ fn test_add_assign_sub_assign() {
668
+ let mut jd = Time::<JD>::new(2_451_545.0);
669
+ jd += Days::new(1.0);
670
+ assert_eq!(jd.quantity(), Days::new(2_451_546.0));
671
+ jd -= Days::new(0.5);
672
+ assert_eq!(jd.quantity(), Days::new(2_451_545.5));
673
+ }
674
+
675
+ #[test]
676
+ fn test_add_years() {
677
+ let jd = Time::<JD>::new(2_450_000.0);
678
+ let with_years = jd + Years::new(1.0);
679
+ let span: Days = with_years - jd;
680
+ assert!((span - Time::<JD>::JULIAN_YEAR).abs() < Days::new(1e-9));
681
+ }
682
+
683
+ #[test]
684
+ fn test_div_days_and_f64() {
685
+ let jd = Time::<JD>::new(100.0);
686
+ assert!((jd / Days::new(2.0) - 50.0).abs() < 1e-12);
687
+ assert!((jd / 4.0 - 25.0).abs() < 1e-12);
688
+ }
689
+
690
+ #[test]
691
+ fn test_to_method_jd_mjd() {
692
+ let jd = Time::<JD>::new(2_451_545.0);
693
+ let mjd = jd.to::<MJD>();
694
+ assert!((mjd.quantity() - Days::new(51_544.5)).abs() < Days::new(1e-10));
695
+ }
696
+
697
+ #[test]
698
+ fn timeinstant_for_julian_date_handles_arithmetic() {
699
+ let jd = Time::<JD>::new(2_451_545.0);
700
+ let other = jd + Days::new(2.0);
701
+
702
+ assert_eq!(jd.difference(&other), Days::new(-2.0));
703
+ assert_eq!(
704
+ jd.add_duration(Days::new(1.5)).quantity(),
705
+ Days::new(2_451_546.5)
706
+ );
707
+ assert_eq!(
708
+ other.sub_duration(Days::new(0.5)).quantity(),
709
+ Days::new(2_451_546.5)
710
+ );
711
+ }
712
+
713
+ #[test]
714
+ fn timeinstant_for_modified_julian_date_roundtrips_utc() {
715
+ let dt = DateTime::from_timestamp(946_684_800, 123_000_000).unwrap(); // 2000-01-01T00:00:00.123Z
716
+ let mjd = Time::<MJD>::from_utc(dt);
717
+ let back = mjd.to_utc().expect("mjd to utc");
718
+
719
+ assert_eq!(mjd.difference(&mjd), Days::new(0.0));
720
+ assert_eq!(
721
+ mjd.add_duration(Days::new(1.0)).quantity(),
722
+ mjd.quantity() + Days::new(1.0)
723
+ );
724
+ assert_eq!(
725
+ mjd.sub_duration(Days::new(0.5)).quantity(),
726
+ mjd.quantity() - Days::new(0.5)
727
+ );
728
+ let delta_ns = back.timestamp_nanos_opt().unwrap() - dt.timestamp_nanos_opt().unwrap();
729
+ assert!(delta_ns.abs() < 10_000, "nanos differ by {}", delta_ns);
730
+ }
731
+
732
+ #[test]
733
+ fn timeinstant_for_datetime_uses_chrono_durations() {
734
+ let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
735
+ let later = Utc.with_ymd_and_hms(2024, 1, 2, 6, 0, 0).unwrap();
736
+ let diff = later.difference(&base);
737
+
738
+ assert_eq!(diff.num_hours(), 30);
739
+ assert_eq!(
740
+ base.add_duration(diff + chrono::Duration::hours(6)),
741
+ later + chrono::Duration::hours(6)
742
+ );
743
+ assert_eq!(later.sub_duration(diff), base);
744
+ assert_eq!(TimeInstant::to_utc(&later), Some(later));
745
+ }
746
+
747
+ // ── New coverage tests ────────────────────────────────────────────
748
+
749
+ #[test]
750
+ fn test_non_finite_error_display() {
751
+ let err = NonFiniteTimeError;
752
+ let msg = format!("{err}");
753
+ assert!(msg.contains("finite"), "unexpected: {msg}");
754
+ }
755
+
756
+ #[test]
757
+ fn test_julian_day_and_julian_day_value() {
758
+ // MJD 51544.5 == JD 2451545.0 (J2000.0 in TT).
759
+ let mjd = Time::<MJD>::new(51_544.5);
760
+ let jd_days = mjd.julian_day();
761
+ assert!(
762
+ (jd_days - Days::new(2_451_545.0)).abs() < Days::new(1e-10),
763
+ "julian_day mismatch: {jd_days}"
764
+ );
765
+ assert!(
766
+ (mjd.julian_day_value() - 2_451_545.0).abs() < 1e-10,
767
+ "julian_day_value mismatch: {}",
768
+ mjd.julian_day_value()
769
+ );
770
+ }
771
+
772
+ #[test]
773
+ fn test_timeinstant_trait_to_utc_and_from_utc_for_time() {
774
+ // Call to_utc / from_utc through the TimeInstant trait (UFCS) so that
775
+ // the forwarding wrapper functions in the TimeInstant impl are covered.
776
+ let jd = Time::<JD>::new(2_451_545.0);
777
+ let utc: Option<_> = TimeInstant::to_utc(&jd);
778
+ assert!(utc.is_some());
779
+ let back: Time<JD> = TimeInstant::from_utc(utc.unwrap());
780
+ assert!((back.value() - jd.value()).abs() < 1e-6);
781
+ }
782
+
783
+ #[test]
784
+ fn test_datetime_timeinstant_from_utc() {
785
+ // Exercises TimeInstant::from_utc for DateTime<Utc>.
786
+ let dt = DateTime::from_timestamp(0, 0).unwrap();
787
+ let back: DateTime<Utc> = TimeInstant::from_utc(dt);
788
+ assert_eq!(back, dt);
789
+ }
790
+
791
+ #[cfg(feature = "serde")]
792
+ #[test]
793
+ fn test_serde_serialize_time() {
794
+ let jd = Time::<JD>::new(2_451_545.0);
795
+ let json = serde_json::to_string(&jd).unwrap();
796
+ assert!(json.contains("2451545"), "serialized: {json}");
797
+ let back: Time<JD> = serde_json::from_str(&json).unwrap();
798
+ assert_eq!(jd.value(), back.value());
799
+ }
800
+
801
+ #[cfg(feature = "serde")]
802
+ #[test]
803
+ fn test_serde_deserialize_nan_rejected() {
804
+ use serde::{de::IntoDeserializer, Deserialize};
805
+ let result: Result<Time<JD>, serde::de::value::Error> =
806
+ Time::<JD>::deserialize(f64::NAN.into_deserializer());
807
+ assert!(result.is_err());
808
+ let msg = result.unwrap_err().to_string();
809
+ assert!(msg.contains("finite"), "unexpected error: {msg}");
810
+ }
811
+ }