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,1168 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (C) 2026 Vallés Puig, Ramon
3
+
4
+ //! Time period / interval implementation.
5
+ //!
6
+ //! This module provides:
7
+ //! - [`Interval<T>`]: generic interval over any [`TimeInstant`]
8
+ //! - [`Period<S>`]: scale-based alias for `Interval<Time<S>>`
9
+
10
+ use super::{Time, TimeInstant, TimeScale};
11
+ use chrono::{DateTime, Utc};
12
+ use qtty::Days;
13
+ use std::fmt;
14
+
15
+ #[cfg(feature = "serde")]
16
+ use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer};
17
+
18
+ /// Error returned when a period time-scale conversion fails.
19
+ ///
20
+ /// Currently the only failure mode is an out-of-range chrono conversion
21
+ /// (e.g. a Julian Date too far in the past/future for `DateTime<Utc>`).
22
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
23
+ pub enum ConversionError {
24
+ /// The time instant is outside the representable range of the target type.
25
+ OutOfRange,
26
+ }
27
+
28
+ impl fmt::Display for ConversionError {
29
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30
+ match self {
31
+ ConversionError::OutOfRange => {
32
+ write!(f, "time instant out of representable range for target type")
33
+ }
34
+ }
35
+ }
36
+ }
37
+
38
+ impl std::error::Error for ConversionError {}
39
+
40
+ /// Error returned when constructing an [`Interval`] with invalid bounds.
41
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
42
+ pub enum InvalidIntervalError {
43
+ /// The start instant is after the end instant (`!(start <= end)`).
44
+ ///
45
+ /// This also triggers for `NaN` endpoints, since `NaN` comparisons
46
+ /// always return `false`.
47
+ StartAfterEnd,
48
+ }
49
+
50
+ impl fmt::Display for InvalidIntervalError {
51
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52
+ match self {
53
+ InvalidIntervalError::StartAfterEnd => {
54
+ write!(f, "interval start must not be after end")
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ impl std::error::Error for InvalidIntervalError {}
61
+
62
+ /// Error indicating a period list violates sorted/non-overlapping invariants.
63
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
64
+ pub enum PeriodListError {
65
+ /// An interval at the given index has `start > end`.
66
+ InvalidInterval {
67
+ /// Index of the malformed interval.
68
+ index: usize,
69
+ },
70
+ /// The interval at the given index has a start time earlier than its predecessor.
71
+ Unsorted {
72
+ /// Index of the out-of-order interval.
73
+ index: usize,
74
+ },
75
+ /// The interval at the given index overlaps with its predecessor.
76
+ Overlapping {
77
+ /// Index of the overlapping interval.
78
+ index: usize,
79
+ },
80
+ }
81
+
82
+ impl fmt::Display for PeriodListError {
83
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84
+ match self {
85
+ PeriodListError::InvalidInterval { index } => {
86
+ write!(f, "interval at index {index} has start > end")
87
+ }
88
+ PeriodListError::Unsorted { index } => {
89
+ write!(f, "interval at index {index} is not sorted by start time")
90
+ }
91
+ PeriodListError::Overlapping { index } => {
92
+ write!(f, "interval at index {index} overlaps with its predecessor")
93
+ }
94
+ }
95
+ }
96
+ }
97
+
98
+ impl std::error::Error for PeriodListError {}
99
+
100
+ /// Target type adapter for [`Interval<Time<S>>::to`].
101
+ ///
102
+ /// This allows converting a period of `Time<S>` either to another time scale
103
+ /// marker (`MJD`, `JD`, `UT`, ...) or directly to `chrono::DateTime<Utc>`.
104
+ pub trait PeriodTimeTarget<S: TimeScale> {
105
+ type Instant: TimeInstant;
106
+
107
+ fn convert(value: Time<S>) -> Result<Self::Instant, ConversionError>;
108
+ }
109
+
110
+ impl<S: TimeScale, T: TimeScale> PeriodTimeTarget<S> for T {
111
+ type Instant = Time<T>;
112
+
113
+ #[inline]
114
+ fn convert(value: Time<S>) -> Result<Self::Instant, ConversionError> {
115
+ Ok(value.to::<T>())
116
+ }
117
+ }
118
+
119
+ impl<S: TimeScale, T: TimeScale> PeriodTimeTarget<S> for Time<T> {
120
+ type Instant = Time<T>;
121
+
122
+ #[inline]
123
+ fn convert(value: Time<S>) -> Result<Self::Instant, ConversionError> {
124
+ Ok(value.to::<T>())
125
+ }
126
+ }
127
+
128
+ impl<S: TimeScale> PeriodTimeTarget<S> for DateTime<Utc> {
129
+ type Instant = DateTime<Utc>;
130
+
131
+ #[inline]
132
+ fn convert(value: Time<S>) -> Result<Self::Instant, ConversionError> {
133
+ value.to_utc().ok_or(ConversionError::OutOfRange)
134
+ }
135
+ }
136
+
137
+ /// Target type adapter for [`Interval<DateTime<Utc>>::to`].
138
+ pub trait PeriodUtcTarget {
139
+ type Instant: TimeInstant;
140
+
141
+ fn convert(value: DateTime<Utc>) -> Self::Instant;
142
+ }
143
+
144
+ impl<S: TimeScale> PeriodUtcTarget for S {
145
+ type Instant = Time<S>;
146
+
147
+ #[inline]
148
+ fn convert(value: DateTime<Utc>) -> Self::Instant {
149
+ Time::<S>::from_utc(value)
150
+ }
151
+ }
152
+
153
+ impl<S: TimeScale> PeriodUtcTarget for Time<S> {
154
+ type Instant = Time<S>;
155
+
156
+ #[inline]
157
+ fn convert(value: DateTime<Utc>) -> Self::Instant {
158
+ Time::<S>::from_utc(value)
159
+ }
160
+ }
161
+
162
+ impl PeriodUtcTarget for DateTime<Utc> {
163
+ type Instant = DateTime<Utc>;
164
+
165
+ #[inline]
166
+ fn convert(value: DateTime<Utc>) -> Self::Instant {
167
+ value
168
+ }
169
+ }
170
+
171
+ /// Represents an interval between two instants.
172
+ ///
173
+ /// An `Interval` is defined by a start and end time instant of type `T`,
174
+ /// where `T` implements the `TimeInstant` trait. This allows for periods
175
+ /// defined in different time systems (Julian Date, Modified Julian Date, UTC, etc.).
176
+ ///
177
+ /// # Examples
178
+ ///
179
+ /// ```
180
+ /// # use tempoch_core as tempoch;
181
+ /// use tempoch::{Interval, ModifiedJulianDate};
182
+ ///
183
+ /// let start = ModifiedJulianDate::new(59000.0);
184
+ /// let end = ModifiedJulianDate::new(59001.0);
185
+ /// let period = Interval::new(start, end);
186
+ ///
187
+ /// // Duration in days
188
+ /// let duration = period.duration();
189
+ /// ```
190
+ #[derive(Debug, Clone, Copy, PartialEq)]
191
+ pub struct Interval<T: TimeInstant> {
192
+ pub start: T,
193
+ pub end: T,
194
+ }
195
+
196
+ /// Time-scale period alias.
197
+ ///
198
+ /// This follows the same marker pattern as [`Time<S>`], so callers can write:
199
+ /// `Period<MJD>`, `Period<JD>`, etc.
200
+ pub type Period<S> = Interval<Time<S>>;
201
+
202
+ /// UTC interval alias.
203
+ pub type UtcPeriod = Interval<DateTime<Utc>>;
204
+
205
+ impl<T: TimeInstant> Interval<T> {
206
+ /// Creates a new period between two time instants.
207
+ ///
208
+ /// **Note:** this constructor does not validate that `start <= end`.
209
+ /// Prefer [`try_new`](Self::try_new) when endpoints come from untrusted
210
+ /// or computed input.
211
+ ///
212
+ /// # Arguments
213
+ ///
214
+ /// * `start` - The start time instant
215
+ /// * `end` - The end time instant
216
+ ///
217
+ /// # Examples
218
+ ///
219
+ /// ```
220
+ /// # use tempoch_core as tempoch;
221
+ /// use tempoch::{Interval, JulianDate};
222
+ ///
223
+ /// let start = JulianDate::new(2451545.0);
224
+ /// let end = JulianDate::new(2451546.0);
225
+ /// let period = Interval::new(start, end);
226
+ /// ```
227
+ pub fn new(start: T, end: T) -> Self {
228
+ Interval { start, end }
229
+ }
230
+
231
+ /// Creates a new interval, validating that `start <= end`.
232
+ ///
233
+ /// Returns [`InvalidIntervalError::StartAfterEnd`] if the start instant
234
+ /// is after the end instant. This also rejects `NaN`-based instants,
235
+ /// since `NaN` comparisons always return `false`.
236
+ ///
237
+ /// # Examples
238
+ ///
239
+ /// ```
240
+ /// # use tempoch_core as tempoch;
241
+ /// use tempoch::{Interval, JulianDate};
242
+ ///
243
+ /// let ok = Interval::try_new(JulianDate::new(100.0), JulianDate::new(200.0));
244
+ /// assert!(ok.is_ok());
245
+ ///
246
+ /// let err = Interval::try_new(JulianDate::new(200.0), JulianDate::new(100.0));
247
+ /// assert!(err.is_err());
248
+ /// ```
249
+ pub fn try_new(start: T, end: T) -> Result<Self, InvalidIntervalError> {
250
+ if start <= end {
251
+ Ok(Interval { start, end })
252
+ } else {
253
+ Err(InvalidIntervalError::StartAfterEnd)
254
+ }
255
+ }
256
+
257
+ /// Returns the duration of the period as the difference between end and start.
258
+ ///
259
+ /// # Examples
260
+ ///
261
+ /// ```
262
+ /// # use tempoch_core as tempoch;
263
+ /// use tempoch::{Interval, JulianDate};
264
+ /// use qtty::Days;
265
+ ///
266
+ /// let start = JulianDate::new(2451545.0);
267
+ /// let end = JulianDate::new(2451546.5);
268
+ /// let period = Interval::new(start, end);
269
+ ///
270
+ /// let duration = period.duration();
271
+ /// assert_eq!(duration, Days::new(1.5));
272
+ /// ```
273
+ pub fn duration(&self) -> T::Duration {
274
+ self.end.difference(&self.start)
275
+ }
276
+
277
+ /// Returns the overlapping sub-period between `self` and `other`.
278
+ ///
279
+ /// Periods are treated as half-open ranges `[start, end)`: if one period
280
+ /// ends exactly when the other starts, the intersection is empty and `None`
281
+ /// is returned.
282
+ pub fn intersection(&self, other: &Self) -> Option<Self> {
283
+ let start = if self.start >= other.start {
284
+ self.start
285
+ } else {
286
+ other.start
287
+ };
288
+ let end = if self.end <= other.end {
289
+ self.end
290
+ } else {
291
+ other.end
292
+ };
293
+
294
+ if start < end {
295
+ Some(Self::new(start, end))
296
+ } else {
297
+ None
298
+ }
299
+ }
300
+ }
301
+
302
+ // Display implementation
303
+ impl<T: TimeInstant + fmt::Display> fmt::Display for Interval<T> {
304
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305
+ write!(f, "{} to {}", self.start, self.end)
306
+ }
307
+ }
308
+
309
+ impl<S: TimeScale> Interval<Time<S>> {
310
+ /// Convert this period to another time scale.
311
+ ///
312
+ /// Each endpoint is converted preserving the represented absolute interval.
313
+ ///
314
+ /// Supported targets:
315
+ /// - Any time-scale marker (`JD`, `MJD`, `UT`, ...)
316
+ /// - `chrono::DateTime<Utc>`
317
+ ///
318
+ /// # Errors
319
+ ///
320
+ /// Returns [`ConversionError::OutOfRange`] if the endpoints fall outside
321
+ /// the representable range of the target type (only possible when
322
+ /// converting to `DateTime<Utc>`).
323
+ ///
324
+ /// # Examples
325
+ ///
326
+ /// ```
327
+ /// use chrono::{DateTime, Utc};
328
+ /// # use tempoch_core as tempoch;
329
+ /// use tempoch::{Interval, JD, MJD, Period, Time};
330
+ ///
331
+ /// let period_jd = Period::new(Time::<JD>::new(2451545.0), Time::<JD>::new(2451546.0));
332
+ /// let period_mjd = period_jd.to::<MJD>().unwrap();
333
+ /// let _period_utc: Interval<DateTime<Utc>> = period_jd.to::<DateTime<Utc>>().unwrap();
334
+ ///
335
+ /// assert!((period_mjd.start.value() - 51544.5).abs() < 1e-12);
336
+ /// assert!((period_mjd.end.value() - 51545.5).abs() < 1e-12);
337
+ /// ```
338
+ #[inline]
339
+ pub fn to<Target>(
340
+ &self,
341
+ ) -> Result<Interval<<Target as PeriodTimeTarget<S>>::Instant>, ConversionError>
342
+ where
343
+ Target: PeriodTimeTarget<S>,
344
+ {
345
+ Ok(Interval::new(
346
+ Target::convert(self.start)?,
347
+ Target::convert(self.end)?,
348
+ ))
349
+ }
350
+ }
351
+
352
+ // Specific implementation for periods with Days duration (JD and MJD)
353
+ impl<T: TimeInstant<Duration = Days>> Interval<T> {
354
+ /// Returns the duration of the period in days as a floating-point value.
355
+ ///
356
+ /// This method is available for time instants with `Days` as their duration type
357
+ /// (e.g., `JulianDate` and `ModifiedJulianDate`).
358
+ ///
359
+ /// # Examples
360
+ ///
361
+ /// ```
362
+ /// # use tempoch_core as tempoch;
363
+ /// use tempoch::{Interval, ModifiedJulianDate};
364
+ /// use qtty::Days;
365
+ ///
366
+ /// let start = ModifiedJulianDate::new(59000.0);
367
+ /// let end = ModifiedJulianDate::new(59001.5);
368
+ /// let period = Interval::new(start, end);
369
+ ///
370
+ /// assert_eq!(period.duration_days(), Days::new(1.5));
371
+ /// ```
372
+ pub fn duration_days(&self) -> Days {
373
+ self.duration()
374
+ }
375
+ }
376
+
377
+ // Specific implementation for UTC periods
378
+ impl Interval<DateTime<Utc>> {
379
+ /// Convert this UTC interval to another target.
380
+ ///
381
+ /// Supported targets:
382
+ /// - Any time-scale marker (`JD`, `MJD`, `UT`, ...)
383
+ /// - Any `Time<...>` alias (`JulianDate`, `ModifiedJulianDate`, ...)
384
+ /// - `chrono::DateTime<Utc>`
385
+ #[inline]
386
+ pub fn to<Target>(&self) -> Interval<<Target as PeriodUtcTarget>::Instant>
387
+ where
388
+ Target: PeriodUtcTarget,
389
+ {
390
+ Interval::new(Target::convert(self.start), Target::convert(self.end))
391
+ }
392
+
393
+ /// Returns the duration in days as a floating-point value.
394
+ ///
395
+ /// This converts the chrono::Duration to days.
396
+ pub fn duration_days(&self) -> f64 {
397
+ const NANOS_PER_DAY: f64 = 86_400_000_000_000.0;
398
+ const SECONDS_PER_DAY: f64 = 86_400.0;
399
+
400
+ let duration = self.duration();
401
+ match duration.num_nanoseconds() {
402
+ Some(ns) => ns as f64 / NANOS_PER_DAY,
403
+ // Fallback for exceptionally large durations that do not fit in i64 nanoseconds.
404
+ None => duration.num_seconds() as f64 / SECONDS_PER_DAY,
405
+ }
406
+ }
407
+
408
+ /// Returns the duration in seconds.
409
+ pub fn duration_seconds(&self) -> i64 {
410
+ self.duration().num_seconds()
411
+ }
412
+ }
413
+
414
+ // Serde support for Period<MJD> (= Interval<Time<MJD>>)
415
+ //
416
+ // Uses the historical field names `start_mjd` / `end_mjd` for backward
417
+ // compatibility with existing JSON reference data.
418
+ #[cfg(feature = "serde")]
419
+ impl Serialize for Interval<crate::ModifiedJulianDate> {
420
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
421
+ where
422
+ S: Serializer,
423
+ {
424
+ let mut s = serializer.serialize_struct("Period", 2)?;
425
+ s.serialize_field("start_mjd", &self.start.value())?;
426
+ s.serialize_field("end_mjd", &self.end.value())?;
427
+ s.end()
428
+ }
429
+ }
430
+
431
+ #[cfg(feature = "serde")]
432
+ impl<'de> Deserialize<'de> for Interval<crate::ModifiedJulianDate> {
433
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
434
+ where
435
+ D: Deserializer<'de>,
436
+ {
437
+ #[derive(Deserialize)]
438
+ struct Raw {
439
+ start_mjd: f64,
440
+ end_mjd: f64,
441
+ }
442
+
443
+ let raw = Raw::deserialize(deserializer)?;
444
+ if !raw.start_mjd.is_finite() || !raw.end_mjd.is_finite() {
445
+ return Err(serde::de::Error::custom(
446
+ "period MJD values must be finite (not NaN or infinity)",
447
+ ));
448
+ }
449
+ if raw.start_mjd > raw.end_mjd {
450
+ return Err(serde::de::Error::custom(
451
+ "period start must not be after end",
452
+ ));
453
+ }
454
+ Ok(Interval::new(
455
+ crate::ModifiedJulianDate::new(raw.start_mjd),
456
+ crate::ModifiedJulianDate::new(raw.end_mjd),
457
+ ))
458
+ }
459
+ }
460
+
461
+ // Serde support for Period<JD> (= Interval<Time<JD>>)
462
+ #[cfg(feature = "serde")]
463
+ impl Serialize for Interval<crate::JulianDate> {
464
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
465
+ where
466
+ S: Serializer,
467
+ {
468
+ let mut s = serializer.serialize_struct("Period", 2)?;
469
+ s.serialize_field("start_jd", &self.start.value())?;
470
+ s.serialize_field("end_jd", &self.end.value())?;
471
+ s.end()
472
+ }
473
+ }
474
+
475
+ #[cfg(feature = "serde")]
476
+ impl<'de> Deserialize<'de> for Interval<crate::JulianDate> {
477
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
478
+ where
479
+ D: Deserializer<'de>,
480
+ {
481
+ #[derive(Deserialize)]
482
+ struct Raw {
483
+ start_jd: f64,
484
+ end_jd: f64,
485
+ }
486
+
487
+ let raw = Raw::deserialize(deserializer)?;
488
+ if !raw.start_jd.is_finite() || !raw.end_jd.is_finite() {
489
+ return Err(serde::de::Error::custom(
490
+ "period JD values must be finite (not NaN or infinity)",
491
+ ));
492
+ }
493
+ if raw.start_jd > raw.end_jd {
494
+ return Err(serde::de::Error::custom(
495
+ "period start must not be after end",
496
+ ));
497
+ }
498
+ Ok(Interval::new(
499
+ crate::JulianDate::new(raw.start_jd),
500
+ crate::JulianDate::new(raw.end_jd),
501
+ ))
502
+ }
503
+ }
504
+
505
+ /// Returns the gaps (complement) of `periods` within the bounding `outer` period.
506
+ ///
507
+ /// Given a sorted, non-overlapping list of sub-periods and a bounding period,
508
+ /// this returns the time intervals NOT covered by any sub-period.
509
+ ///
510
+ /// Both `outer` and every element of `periods` must have `start <= end`.
511
+ /// The function runs in O(n) time with a single pass.
512
+ ///
513
+ /// # Arguments
514
+ /// * `outer` - The bounding period
515
+ /// * `periods` - Sorted, non-overlapping sub-periods within `outer`
516
+ ///
517
+ /// # Returns
518
+ /// The complement periods (gaps) in chronological order.
519
+ pub fn complement_within<T: TimeInstant>(
520
+ outer: Interval<T>,
521
+ periods: &[Interval<T>],
522
+ ) -> Vec<Interval<T>> {
523
+ let mut gaps = Vec::new();
524
+ let mut cursor = outer.start;
525
+ for p in periods {
526
+ if p.start > cursor {
527
+ gaps.push(Interval::new(cursor, p.start));
528
+ }
529
+ if p.end > cursor {
530
+ cursor = p.end;
531
+ }
532
+ }
533
+ if cursor < outer.end {
534
+ gaps.push(Interval::new(cursor, outer.end));
535
+ }
536
+ gaps
537
+ }
538
+
539
+ /// Returns the intersection of two sorted, non-overlapping period lists.
540
+ ///
541
+ /// Uses an O(n+m) merge algorithm to find all overlapping spans.
542
+ ///
543
+ /// # Arguments
544
+ /// * `a` - First sorted, non-overlapping period list
545
+ /// * `b` - Second sorted, non-overlapping period list
546
+ ///
547
+ /// # Returns
548
+ /// Periods where both `a` and `b` overlap, in chronological order.
549
+ pub fn intersect_periods<T: TimeInstant>(a: &[Interval<T>], b: &[Interval<T>]) -> Vec<Interval<T>> {
550
+ let mut result = Vec::new();
551
+ let (mut i, mut j) = (0, 0);
552
+ while i < a.len() && j < b.len() {
553
+ let start = if a[i].start >= b[j].start {
554
+ a[i].start
555
+ } else {
556
+ b[j].start
557
+ };
558
+ let end = if a[i].end <= b[j].end {
559
+ a[i].end
560
+ } else {
561
+ b[j].end
562
+ };
563
+ if start < end {
564
+ result.push(Interval::new(start, end));
565
+ }
566
+ if a[i].end <= b[j].end {
567
+ i += 1;
568
+ } else {
569
+ j += 1;
570
+ }
571
+ }
572
+ result
573
+ }
574
+
575
+ /// Validate that a period list is sorted by start time and non-overlapping.
576
+ ///
577
+ /// Checks three invariants on every element:
578
+ /// 1. Each interval has `start <= end`.
579
+ /// 2. Intervals are sorted by start time (monotonically non-decreasing).
580
+ /// 3. Adjacent intervals do not overlap (previous `end <= next start`).
581
+ ///
582
+ /// Returns `Ok(())` if all invariants hold, or the first violation found.
583
+ ///
584
+ /// # Examples
585
+ ///
586
+ /// ```
587
+ /// # use tempoch_core as tempoch;
588
+ /// use tempoch::{validate_period_list, Interval, ModifiedJulianDate};
589
+ ///
590
+ /// let sorted = vec![
591
+ /// Interval::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(3.0)),
592
+ /// Interval::new(ModifiedJulianDate::new(5.0), ModifiedJulianDate::new(8.0)),
593
+ /// ];
594
+ /// assert!(validate_period_list(&sorted).is_ok());
595
+ /// ```
596
+ pub fn validate_period_list<T: TimeInstant>(
597
+ periods: &[Interval<T>],
598
+ ) -> Result<(), PeriodListError> {
599
+ for (i, p) in periods.iter().enumerate() {
600
+ if p.start
601
+ .partial_cmp(&p.end)
602
+ .is_none_or(|o| o == std::cmp::Ordering::Greater)
603
+ {
604
+ return Err(PeriodListError::InvalidInterval { index: i });
605
+ }
606
+ }
607
+ for i in 1..periods.len() {
608
+ if periods[i - 1]
609
+ .start
610
+ .partial_cmp(&periods[i].start)
611
+ .is_none_or(|o| o == std::cmp::Ordering::Greater)
612
+ {
613
+ return Err(PeriodListError::Unsorted { index: i });
614
+ }
615
+ if periods[i - 1].end > periods[i].start {
616
+ return Err(PeriodListError::Overlapping { index: i });
617
+ }
618
+ }
619
+ Ok(())
620
+ }
621
+
622
+ /// Sort periods by start time and merge overlapping/adjacent intervals.
623
+ ///
624
+ /// Produces a sorted, non-overlapping list suitable for [`complement_within`]
625
+ /// and [`intersect_periods`].
626
+ ///
627
+ /// # Examples
628
+ ///
629
+ /// ```
630
+ /// # use tempoch_core as tempoch;
631
+ /// use tempoch::{normalize_periods, Interval, ModifiedJulianDate};
632
+ ///
633
+ /// let periods = vec![
634
+ /// Interval::new(ModifiedJulianDate::new(5.0), ModifiedJulianDate::new(8.0)),
635
+ /// Interval::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(3.0)),
636
+ /// Interval::new(ModifiedJulianDate::new(2.0), ModifiedJulianDate::new(6.0)),
637
+ /// ];
638
+ /// let merged = normalize_periods(&periods);
639
+ /// assert_eq!(merged.len(), 1); // [0, 8)
640
+ /// ```
641
+ pub fn normalize_periods<T: TimeInstant>(periods: &[Interval<T>]) -> Vec<Interval<T>> {
642
+ if periods.is_empty() {
643
+ return Vec::new();
644
+ }
645
+ let mut sorted: Vec<_> = periods.to_vec();
646
+ sorted.sort_by(|a, b| {
647
+ a.start
648
+ .partial_cmp(&b.start)
649
+ .unwrap_or(std::cmp::Ordering::Equal)
650
+ });
651
+ let mut merged = vec![sorted[0]];
652
+ for p in &sorted[1..] {
653
+ let last = merged.last_mut().unwrap();
654
+ if p.start <= last.end {
655
+ // Overlapping or adjacent — extend
656
+ if p.end > last.end {
657
+ last.end = p.end;
658
+ }
659
+ } else {
660
+ merged.push(*p);
661
+ }
662
+ }
663
+ merged
664
+ }
665
+
666
+ #[cfg(test)]
667
+ mod tests {
668
+ use super::*;
669
+ use crate::{JulianDate, ModifiedJulianDate, JD, MJD};
670
+
671
+ #[test]
672
+ fn test_try_new_valid() {
673
+ let p = Interval::try_new(
674
+ ModifiedJulianDate::new(59000.0),
675
+ ModifiedJulianDate::new(59001.0),
676
+ );
677
+ assert!(p.is_ok());
678
+ }
679
+
680
+ #[test]
681
+ fn test_try_new_equal_bounds() {
682
+ let p = Interval::try_new(
683
+ ModifiedJulianDate::new(59000.0),
684
+ ModifiedJulianDate::new(59000.0),
685
+ );
686
+ assert!(p.is_ok()); // zero-length interval is valid
687
+ }
688
+
689
+ #[test]
690
+ fn test_try_new_invalid() {
691
+ let p = Interval::try_new(
692
+ ModifiedJulianDate::new(59001.0),
693
+ ModifiedJulianDate::new(59000.0),
694
+ );
695
+ assert_eq!(p, Err(InvalidIntervalError::StartAfterEnd));
696
+ }
697
+
698
+ #[test]
699
+ fn test_try_new_nan_rejected() {
700
+ let p = Interval::try_new(
701
+ ModifiedJulianDate::new(f64::NAN),
702
+ ModifiedJulianDate::new(59000.0),
703
+ );
704
+ assert!(p.is_err());
705
+ }
706
+
707
+ #[test]
708
+ fn test_validate_period_list_ok() {
709
+ let periods = vec![
710
+ Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(3.0)),
711
+ Period::new(ModifiedJulianDate::new(5.0), ModifiedJulianDate::new(8.0)),
712
+ ];
713
+ assert!(validate_period_list(&periods).is_ok());
714
+ }
715
+
716
+ #[test]
717
+ fn test_validate_period_list_unsorted() {
718
+ let periods = vec![
719
+ Period::new(ModifiedJulianDate::new(5.0), ModifiedJulianDate::new(8.0)),
720
+ Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(3.0)),
721
+ ];
722
+ assert_eq!(
723
+ validate_period_list(&periods),
724
+ Err(PeriodListError::Unsorted { index: 1 })
725
+ );
726
+ }
727
+
728
+ #[test]
729
+ fn test_validate_period_list_overlapping() {
730
+ let periods = vec![
731
+ Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(5.0)),
732
+ Period::new(ModifiedJulianDate::new(3.0), ModifiedJulianDate::new(8.0)),
733
+ ];
734
+ assert_eq!(
735
+ validate_period_list(&periods),
736
+ Err(PeriodListError::Overlapping { index: 1 })
737
+ );
738
+ }
739
+
740
+ #[test]
741
+ fn test_validate_period_list_invalid_interval() {
742
+ let periods = vec![Period::new(
743
+ ModifiedJulianDate::new(5.0),
744
+ ModifiedJulianDate::new(3.0),
745
+ )];
746
+ assert_eq!(
747
+ validate_period_list(&periods),
748
+ Err(PeriodListError::InvalidInterval { index: 0 })
749
+ );
750
+ }
751
+
752
+ #[test]
753
+ fn test_normalize_periods_empty() {
754
+ let periods: Vec<Period<MJD>> = vec![];
755
+ assert!(normalize_periods(&periods).is_empty());
756
+ }
757
+
758
+ #[test]
759
+ fn test_normalize_periods_unsorted_and_overlapping() {
760
+ let periods = vec![
761
+ Period::new(ModifiedJulianDate::new(5.0), ModifiedJulianDate::new(8.0)),
762
+ Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(3.0)),
763
+ Period::new(ModifiedJulianDate::new(2.0), ModifiedJulianDate::new(6.0)),
764
+ ];
765
+ let merged = normalize_periods(&periods);
766
+ assert_eq!(merged.len(), 1);
767
+ assert_eq!(merged[0].start.quantity(), Days::new(0.0));
768
+ assert_eq!(merged[0].end.quantity(), Days::new(8.0));
769
+ }
770
+
771
+ #[test]
772
+ fn test_normalize_periods_disjoint() {
773
+ let periods = vec![
774
+ Period::new(ModifiedJulianDate::new(5.0), ModifiedJulianDate::new(6.0)),
775
+ Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(2.0)),
776
+ ];
777
+ let merged = normalize_periods(&periods);
778
+ assert_eq!(merged.len(), 2);
779
+ assert_eq!(merged[0].start.quantity(), Days::new(0.0));
780
+ assert_eq!(merged[1].start.quantity(), Days::new(5.0));
781
+ }
782
+
783
+ #[test]
784
+ fn test_period_creation_jd() {
785
+ let start = JulianDate::new(2451545.0);
786
+ let end = JulianDate::new(2451546.0);
787
+ let period = Period::new(start, end);
788
+
789
+ assert_eq!(period.start, start);
790
+ assert_eq!(period.end, end);
791
+ }
792
+
793
+ #[test]
794
+ fn test_period_scale_conversion_jd_to_mjd() {
795
+ let period_jd = Period::new(Time::<JD>::new(2_451_545.0), Time::<JD>::new(2_451_546.0));
796
+ let period_mjd = period_jd.to::<MJD>().unwrap();
797
+
798
+ assert!((period_mjd.start.value() - 51_544.5).abs() < 1e-12);
799
+ assert!((period_mjd.end.value() - 51_545.5).abs() < 1e-12);
800
+ }
801
+
802
+ #[test]
803
+ fn test_period_scale_conversion_roundtrip() {
804
+ let original = Period::new(Time::<MJD>::new(59_000.125), Time::<MJD>::new(59_001.75));
805
+ let roundtrip = original.to::<JD>().unwrap().to::<MJD>().unwrap();
806
+
807
+ assert!((roundtrip.start.value() - original.start.value()).abs() < 1e-12);
808
+ assert!((roundtrip.end.value() - original.end.value()).abs() < 1e-12);
809
+ }
810
+
811
+ #[test]
812
+ fn test_period_scale_conversion_to_utc() {
813
+ let start_utc = DateTime::from_timestamp(1_700_000_000, 0).unwrap();
814
+ let end_utc = DateTime::from_timestamp(1_700_000_600, 0).unwrap();
815
+ let period_jd = Period::new(
816
+ Time::<JD>::from_utc(start_utc),
817
+ Time::<JD>::from_utc(end_utc),
818
+ );
819
+
820
+ let period_utc = period_jd.to::<DateTime<Utc>>().unwrap();
821
+ let start_delta_ns = period_utc.start.timestamp_nanos_opt().unwrap()
822
+ - start_utc.timestamp_nanos_opt().unwrap();
823
+ let end_delta_ns =
824
+ period_utc.end.timestamp_nanos_opt().unwrap() - end_utc.timestamp_nanos_opt().unwrap();
825
+ assert!(start_delta_ns.abs() < 10_000);
826
+ assert!(end_delta_ns.abs() < 10_000);
827
+ }
828
+
829
+ #[test]
830
+ fn test_period_creation_mjd() {
831
+ let start = ModifiedJulianDate::new(59000.0);
832
+ let end = ModifiedJulianDate::new(59001.0);
833
+ let period = Period::new(start, end);
834
+
835
+ assert_eq!(period.start, start);
836
+ assert_eq!(period.end, end);
837
+ }
838
+
839
+ #[test]
840
+ fn test_period_duration_jd() {
841
+ let start = JulianDate::new(2451545.0);
842
+ let end = JulianDate::new(2451546.5);
843
+ let period = Period::new(start, end);
844
+
845
+ assert_eq!(period.duration_days(), Days::new(1.5));
846
+ }
847
+
848
+ #[test]
849
+ fn test_period_duration_mjd() {
850
+ let start = ModifiedJulianDate::new(59000.0);
851
+ let end = ModifiedJulianDate::new(59001.5);
852
+ let period = Period::new(start, end);
853
+
854
+ assert_eq!(period.duration_days(), Days::new(1.5));
855
+ }
856
+
857
+ #[test]
858
+ fn test_period_duration_utc() {
859
+ let start = DateTime::from_timestamp(0, 0).unwrap();
860
+ let end = DateTime::from_timestamp(86400, 0).unwrap(); // 1 day later
861
+ let period = Interval::new(start, end);
862
+
863
+ assert_eq!(period.duration_days(), 1.0);
864
+ assert_eq!(period.duration_seconds(), 86400);
865
+ }
866
+
867
+ #[test]
868
+ fn test_period_duration_utc_subsecond_precision() {
869
+ let start = DateTime::from_timestamp(0, 0).unwrap();
870
+ let end = DateTime::from_timestamp(0, 500_000_000).unwrap();
871
+ let period = Interval::new(start, end);
872
+
873
+ let expected_days = 0.5 / 86_400.0;
874
+ assert!((period.duration_days() - expected_days).abs() < 1e-15);
875
+ assert_eq!(period.duration_seconds(), 0);
876
+ }
877
+
878
+ #[test]
879
+ fn test_period_to_conversion() {
880
+ let mjd_start = ModifiedJulianDate::new(59000.0);
881
+ let mjd_end = ModifiedJulianDate::new(59001.0);
882
+ let mjd_period = Period::new(mjd_start, mjd_end);
883
+
884
+ let utc_period = mjd_period.to::<DateTime<Utc>>().unwrap();
885
+
886
+ // The converted period should have approximately the same duration (within 1 second due to ΔT)
887
+ let duration_secs = utc_period.duration().num_seconds();
888
+ assert!(
889
+ (duration_secs - 86400).abs() <= 1,
890
+ "Duration was {} seconds",
891
+ duration_secs
892
+ );
893
+
894
+ // Convert back and check that it's close (within small tolerance due to floating point)
895
+ let back_to_mjd = utc_period.to::<ModifiedJulianDate>();
896
+ let start_diff = (back_to_mjd.start.quantity() - mjd_start.quantity())
897
+ .value()
898
+ .abs();
899
+ let end_diff = (back_to_mjd.end.quantity() - mjd_end.quantity())
900
+ .value()
901
+ .abs();
902
+ assert!(start_diff < 1e-6, "Start difference: {}", start_diff);
903
+ assert!(end_diff < 1e-6, "End difference: {}", end_diff);
904
+ }
905
+
906
+ #[test]
907
+ fn test_period_display() {
908
+ let start = ModifiedJulianDate::new(59000.0);
909
+ let end = ModifiedJulianDate::new(59001.0);
910
+ let period = Period::new(start, end);
911
+
912
+ let display = format!("{}", period);
913
+ assert!(display.contains("MJD 59000"));
914
+ assert!(display.contains("MJD 59001"));
915
+ assert!(display.contains("to"));
916
+ }
917
+
918
+ #[test]
919
+ fn test_period_intersection_overlap() {
920
+ let a = Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(5.0));
921
+ let b = Period::new(ModifiedJulianDate::new(3.0), ModifiedJulianDate::new(8.0));
922
+
923
+ let overlap = a.intersection(&b).expect("expected overlap");
924
+ assert_eq!(overlap.start.quantity(), Days::new(3.0));
925
+ assert_eq!(overlap.end.quantity(), Days::new(5.0));
926
+ }
927
+
928
+ #[test]
929
+ fn test_period_intersection_disjoint() {
930
+ let a = Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(3.0));
931
+ let b = Period::new(ModifiedJulianDate::new(5.0), ModifiedJulianDate::new(8.0));
932
+
933
+ assert_eq!(a.intersection(&b), None);
934
+ }
935
+
936
+ #[test]
937
+ fn test_period_intersection_touching_edges() {
938
+ let a = Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(3.0));
939
+ let b = Period::new(ModifiedJulianDate::new(3.0), ModifiedJulianDate::new(8.0));
940
+
941
+ assert_eq!(a.intersection(&b), None);
942
+ }
943
+
944
+ #[test]
945
+ fn test_complement_within_gaps() {
946
+ let outer = Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(10.0));
947
+ let periods = vec![
948
+ Period::new(ModifiedJulianDate::new(2.0), ModifiedJulianDate::new(4.0)),
949
+ Period::new(ModifiedJulianDate::new(6.0), ModifiedJulianDate::new(8.0)),
950
+ ];
951
+ let gaps = complement_within(outer, &periods);
952
+ assert_eq!(gaps.len(), 3);
953
+ assert_eq!(gaps[0].start.quantity(), Days::new(0.0));
954
+ assert_eq!(gaps[0].end.quantity(), Days::new(2.0));
955
+ assert_eq!(gaps[1].start.quantity(), Days::new(4.0));
956
+ assert_eq!(gaps[1].end.quantity(), Days::new(6.0));
957
+ assert_eq!(gaps[2].start.quantity(), Days::new(8.0));
958
+ assert_eq!(gaps[2].end.quantity(), Days::new(10.0));
959
+ }
960
+
961
+ #[test]
962
+ fn test_complement_within_empty() {
963
+ let outer = Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(10.0));
964
+ let gaps = complement_within(outer, &[]);
965
+ assert_eq!(gaps.len(), 1);
966
+ assert_eq!(gaps[0].start.quantity(), Days::new(0.0));
967
+ assert_eq!(gaps[0].end.quantity(), Days::new(10.0));
968
+ }
969
+
970
+ #[test]
971
+ fn test_complement_within_full() {
972
+ let outer = Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(10.0));
973
+ let periods = vec![Period::new(
974
+ ModifiedJulianDate::new(0.0),
975
+ ModifiedJulianDate::new(10.0),
976
+ )];
977
+ let gaps = complement_within(outer, &periods);
978
+ assert!(gaps.is_empty());
979
+ }
980
+
981
+ #[test]
982
+ fn test_intersect_periods_overlap() {
983
+ let a = vec![Period::new(
984
+ ModifiedJulianDate::new(0.0),
985
+ ModifiedJulianDate::new(5.0),
986
+ )];
987
+ let b = vec![Period::new(
988
+ ModifiedJulianDate::new(3.0),
989
+ ModifiedJulianDate::new(8.0),
990
+ )];
991
+ let overlap = intersect_periods(&a, &b);
992
+ assert_eq!(overlap.len(), 1);
993
+ assert_eq!(overlap[0].start.quantity(), Days::new(3.0));
994
+ assert_eq!(overlap[0].end.quantity(), Days::new(5.0));
995
+ }
996
+
997
+ #[test]
998
+ fn test_intersect_periods_no_overlap() {
999
+ let a = vec![Period::new(
1000
+ ModifiedJulianDate::new(0.0),
1001
+ ModifiedJulianDate::new(3.0),
1002
+ )];
1003
+ let b = vec![Period::new(
1004
+ ModifiedJulianDate::new(5.0),
1005
+ ModifiedJulianDate::new(8.0),
1006
+ )];
1007
+ let overlap = intersect_periods(&a, &b);
1008
+ assert!(overlap.is_empty());
1009
+ }
1010
+
1011
+ #[test]
1012
+ fn test_complement_intersect_roundtrip() {
1013
+ // above(min) ∩ complement(above(max)) = between(min, max)
1014
+ let outer = Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(10.0));
1015
+ let above_min = vec![
1016
+ Period::new(ModifiedJulianDate::new(1.0), ModifiedJulianDate::new(3.0)),
1017
+ Period::new(ModifiedJulianDate::new(5.0), ModifiedJulianDate::new(9.0)),
1018
+ ];
1019
+ let above_max = vec![
1020
+ Period::new(ModifiedJulianDate::new(2.0), ModifiedJulianDate::new(4.0)),
1021
+ Period::new(ModifiedJulianDate::new(7.0), ModifiedJulianDate::new(8.0)),
1022
+ ];
1023
+ let below_max = complement_within(outer, &above_max);
1024
+ let between = intersect_periods(&above_min, &below_max);
1025
+ // above_min: [1,3), [5,9)
1026
+ // above_max: [2,4), [7,8)
1027
+ // below_max (complement): [0,2), [4,7), [8,10)
1028
+ // intersection: [1,2), [5,7), [8,9)
1029
+ assert_eq!(between.len(), 3);
1030
+ assert_eq!(between[0].start.quantity(), Days::new(1.0));
1031
+ assert_eq!(between[0].end.quantity(), Days::new(2.0));
1032
+ assert_eq!(between[1].start.quantity(), Days::new(5.0));
1033
+ assert_eq!(between[1].end.quantity(), Days::new(7.0));
1034
+ assert_eq!(between[2].start.quantity(), Days::new(8.0));
1035
+ assert_eq!(between[2].end.quantity(), Days::new(9.0));
1036
+ }
1037
+
1038
+ // ── New coverage tests ────────────────────────────────────────────
1039
+
1040
+ #[test]
1041
+ fn test_conversion_error_display() {
1042
+ let err = ConversionError::OutOfRange;
1043
+ let msg = format!("{err}");
1044
+ assert!(msg.contains("out of representable range"), "got: {msg}");
1045
+ }
1046
+
1047
+ #[test]
1048
+ fn test_conversion_error_is_error() {
1049
+ let err = ConversionError::OutOfRange;
1050
+ // Verify it satisfies std::error::Error
1051
+ let _: &dyn std::error::Error = &err;
1052
+ }
1053
+
1054
+ #[test]
1055
+ fn test_invalid_interval_error_display() {
1056
+ let err = InvalidIntervalError::StartAfterEnd;
1057
+ let msg = format!("{err}");
1058
+ assert!(msg.contains("start must not be after end"), "got: {msg}");
1059
+ }
1060
+
1061
+ #[test]
1062
+ fn test_invalid_interval_error_is_error() {
1063
+ let err = InvalidIntervalError::StartAfterEnd;
1064
+ let _: &dyn std::error::Error = &err;
1065
+ }
1066
+
1067
+ #[test]
1068
+ fn test_period_list_error_invalid_interval_display() {
1069
+ let e = PeriodListError::InvalidInterval { index: 0 };
1070
+ let msg = format!("{e}");
1071
+ assert!(msg.contains("index 0"), "got: {msg}");
1072
+ }
1073
+
1074
+ #[test]
1075
+ fn test_period_list_error_unsorted_display() {
1076
+ let e = PeriodListError::Unsorted { index: 2 };
1077
+ let msg = format!("{e}");
1078
+ assert!(msg.contains("index 2"), "got: {msg}");
1079
+ }
1080
+
1081
+ #[test]
1082
+ fn test_period_list_error_overlapping_display() {
1083
+ let e = PeriodListError::Overlapping { index: 3 };
1084
+ let msg = format!("{e}");
1085
+ assert!(msg.contains("index 3"), "got: {msg}");
1086
+ }
1087
+
1088
+ #[test]
1089
+ fn test_period_list_error_is_error() {
1090
+ let e = PeriodListError::InvalidInterval { index: 0 };
1091
+ let _: &dyn std::error::Error = &e;
1092
+ }
1093
+
1094
+ #[test]
1095
+ fn test_intersection_self_larger_than_other() {
1096
+ // a.start > b.start AND a.end > b.end → intersection picks a.start and b.end.
1097
+ // Exercises the `self.start` branch (line 284) and the `other.end` branch (line 291).
1098
+ let a = Period::new(ModifiedJulianDate::new(2.0), ModifiedJulianDate::new(8.0));
1099
+ let b = Period::new(ModifiedJulianDate::new(0.0), ModifiedJulianDate::new(5.0));
1100
+ let overlap = a.intersection(&b).expect("should overlap");
1101
+ assert_eq!(overlap.start.quantity(), Days::new(2.0));
1102
+ assert_eq!(overlap.end.quantity(), Days::new(5.0));
1103
+ }
1104
+
1105
+ #[test]
1106
+ fn test_period_time_target_for_time_type() {
1107
+ // Use `ModifiedJulianDate` (= Time<MJD>) as the Target type parameter,
1108
+ // not the bare `MJD` marker, to exercise the PeriodTimeTarget impl for Time<T>.
1109
+ let period_jd = Period::new(Time::<JD>::new(2_451_545.0), Time::<JD>::new(2_451_546.0));
1110
+ let period_mjd: Interval<ModifiedJulianDate> =
1111
+ period_jd.to::<ModifiedJulianDate>().unwrap();
1112
+ assert!((period_mjd.start.value() - 51_544.5).abs() < 1e-12);
1113
+ assert!((period_mjd.end.value() - 51_545.5).abs() < 1e-12);
1114
+ }
1115
+
1116
+ #[test]
1117
+ fn test_utc_period_to_datetime_utc_identity() {
1118
+ // Converting an Interval<DateTime<Utc>> to DateTime<Utc> again is a
1119
+ // no-op; exercises PeriodUtcTarget for DateTime<Utc>.
1120
+ let start = DateTime::from_timestamp(0, 0).unwrap();
1121
+ let end = DateTime::from_timestamp(86400, 0).unwrap();
1122
+ let utc_period = Interval::new(start, end);
1123
+ let same: Interval<DateTime<Utc>> = utc_period.to::<DateTime<Utc>>();
1124
+ assert_eq!(same.start, start);
1125
+ assert_eq!(same.end, end);
1126
+ }
1127
+
1128
+ #[cfg(feature = "serde")]
1129
+ #[test]
1130
+ fn test_period_mjd_serde_roundtrip() {
1131
+ let p = Period::new(
1132
+ ModifiedJulianDate::new(59000.0),
1133
+ ModifiedJulianDate::new(59001.0),
1134
+ );
1135
+ let json = serde_json::to_string(&p).unwrap();
1136
+ assert!(json.contains("start_mjd"), "serialized: {json}");
1137
+ let back: Period<MJD> = serde_json::from_str(&json).unwrap();
1138
+ assert!((back.start.value() - 59000.0).abs() < 1e-12);
1139
+ assert!((back.end.value() - 59001.0).abs() < 1e-12);
1140
+ }
1141
+
1142
+ #[cfg(feature = "serde")]
1143
+ #[test]
1144
+ fn test_period_mjd_deserialize_start_after_end_rejected() {
1145
+ let json = r#"{"start_mjd": 59001.0, "end_mjd": 59000.0}"#;
1146
+ let result: Result<Period<MJD>, _> = serde_json::from_str(json);
1147
+ assert!(result.is_err());
1148
+ }
1149
+
1150
+ #[cfg(feature = "serde")]
1151
+ #[test]
1152
+ fn test_period_jd_serde_roundtrip() {
1153
+ let p = Period::new(JulianDate::new(2_451_545.0), JulianDate::new(2_451_546.0));
1154
+ let json = serde_json::to_string(&p).unwrap();
1155
+ assert!(json.contains("start_jd"), "serialized: {json}");
1156
+ let back: Period<JD> = serde_json::from_str(&json).unwrap();
1157
+ assert!((back.start.value() - 2_451_545.0).abs() < 1e-12);
1158
+ assert!((back.end.value() - 2_451_546.0).abs() < 1e-12);
1159
+ }
1160
+
1161
+ #[cfg(feature = "serde")]
1162
+ #[test]
1163
+ fn test_period_jd_deserialize_start_after_end_rejected() {
1164
+ let json = r#"{"start_jd": 2451546.0, "end_jd": 2451545.0}"#;
1165
+ let result: Result<Period<JD>, _> = serde_json::from_str(json);
1166
+ assert!(result.is_err());
1167
+ }
1168
+ }