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.
- package/.github/workflows/ci.yml +166 -0
- package/.gitmodules +9 -0
- package/CHANGELOG.md +26 -0
- package/LICENSE +661 -0
- package/README.md +138 -0
- package/package.json +12 -0
- package/qtty-js/.github/workflows/ci.yml +151 -0
- package/qtty-js/.gitmodules +3 -0
- package/qtty-js/CHANGELOG.md +31 -0
- package/qtty-js/LICENSE +661 -0
- package/qtty-js/README.md +132 -0
- package/qtty-js/package.json +20 -0
- package/qtty-js/qtty/.github/workflows/ci.yml +155 -0
- package/qtty-js/qtty/CHANGELOG.md +120 -0
- package/qtty-js/qtty/Cargo.lock +1462 -0
- package/qtty-js/qtty/Cargo.toml +12 -0
- package/qtty-js/qtty/LICENSE +661 -0
- package/qtty-js/qtty/README.md +9 -0
- package/qtty-js/qtty/qtty/Cargo.toml +41 -0
- package/qtty-js/qtty/qtty/README.md +8 -0
- package/qtty-js/qtty/qtty/examples/angles.rs +14 -0
- package/qtty-js/qtty/qtty/examples/astronomy.rs +17 -0
- package/qtty-js/qtty/qtty/examples/dimensional_arithmetic.rs +83 -0
- package/qtty-js/qtty/qtty/examples/python_integration.rs +61 -0
- package/qtty-js/qtty/qtty/examples/quickstart.rs +15 -0
- package/qtty-js/qtty/qtty/examples/ratios.rs +12 -0
- package/qtty-js/qtty/qtty/examples/serde_with_unit.rs +234 -0
- package/qtty-js/qtty/qtty/examples/serialization.rs +141 -0
- package/qtty-js/qtty/qtty/examples/serialization_advanced.rs +155 -0
- package/qtty-js/qtty/qtty/src/f32.rs +108 -0
- package/qtty-js/qtty/qtty/src/f64.rs +30 -0
- package/qtty-js/qtty/qtty/src/i128.rs +111 -0
- package/qtty-js/qtty/qtty/src/i16.rs +111 -0
- package/qtty-js/qtty/qtty/src/i32.rs +111 -0
- package/qtty-js/qtty/qtty/src/i64.rs +111 -0
- package/qtty-js/qtty/qtty/src/i8.rs +111 -0
- package/qtty-js/qtty/qtty/src/lib.rs +238 -0
- package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std/Cargo.lock +83 -0
- package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std/Cargo.toml +10 -0
- package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std/src/lib.rs +7 -0
- package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std-alloc/Cargo.lock +83 -0
- package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std-alloc/Cargo.toml +10 -0
- package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std-alloc/src/lib.rs +7 -0
- package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-std/Cargo.lock +83 -0
- package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-std/Cargo.toml +10 -0
- package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-std/src/lib.rs +5 -0
- package/qtty-js/qtty/qtty/tests/integration_tests.rs +529 -0
- package/qtty-js/qtty/qtty/tests/qtty_vec_feature_matrix.rs +58 -0
- package/qtty-js/qtty/qtty-core/Cargo.toml +41 -0
- package/qtty-js/qtty/qtty-core/README.md +8 -0
- package/qtty-js/qtty/qtty-core/examples/diesel_integration.rs +145 -0
- package/qtty-js/qtty/qtty-core/examples/quantity_db_serde.rs +215 -0
- package/qtty-js/qtty/qtty-core/src/dimension.rs +249 -0
- package/qtty-js/qtty/qtty-core/src/feature_diesel.rs +318 -0
- package/qtty-js/qtty/qtty-core/src/feature_pyo3.rs +27 -0
- package/qtty-js/qtty/qtty-core/src/feature_serde.rs +203 -0
- package/qtty-js/qtty/qtty-core/src/feature_tiberius.rs +28 -0
- package/qtty-js/qtty/qtty-core/src/lib.rs +744 -0
- package/qtty-js/qtty/qtty-core/src/macros.rs +93 -0
- package/qtty-js/qtty/qtty-core/src/quantity.rs +810 -0
- package/qtty-js/qtty/qtty-core/src/scalar.rs +1742 -0
- package/qtty-js/qtty/qtty-core/src/unit.rs +332 -0
- package/qtty-js/qtty/qtty-core/src/units/angular.rs +1228 -0
- package/qtty-js/qtty/qtty-core/src/units/area.rs +243 -0
- package/qtty-js/qtty/qtty-core/src/units/frequency.rs +179 -0
- package/qtty-js/qtty/qtty-core/src/units/length.rs +1270 -0
- package/qtty-js/qtty/qtty-core/src/units/mass.rs +488 -0
- package/qtty-js/qtty/qtty-core/src/units/mod.rs +26 -0
- package/qtty-js/qtty/qtty-core/src/units/power.rs +324 -0
- package/qtty-js/qtty/qtty-core/src/units/time.rs +667 -0
- package/qtty-js/qtty/qtty-core/src/units/unitless.rs +212 -0
- package/qtty-js/qtty/qtty-core/src/units/velocity.rs +210 -0
- package/qtty-js/qtty/qtty-core/src/units/volume.rs +269 -0
- package/qtty-js/qtty/qtty-core/tests/core.rs +628 -0
- package/qtty-js/qtty/qtty-core/tests/diesel.rs +461 -0
- package/qtty-js/qtty/qtty-core/tests/integers.rs +632 -0
- package/qtty-js/qtty/qtty-core/tests/no_cross_unit_ops.rs +35 -0
- package/qtty-js/qtty/qtty-core/tests/pyo3.rs +334 -0
- package/qtty-js/qtty/qtty-core/tests/quantity_f32.rs +276 -0
- package/qtty-js/qtty/qtty-core/tests/scalar_decimal.rs +258 -0
- package/qtty-js/qtty/qtty-core/tests/scalar_f32.rs +286 -0
- package/qtty-js/qtty/qtty-core/tests/scalar_f64_real.rs +287 -0
- package/qtty-js/qtty/qtty-core/tests/scalar_rational.rs +260 -0
- package/qtty-js/qtty/qtty-core/tests/serde.rs +256 -0
- package/qtty-js/qtty/qtty-core/tests/tiberius.rs +208 -0
- package/qtty-js/qtty/qtty-derive/Cargo.toml +23 -0
- package/qtty-js/qtty/qtty-derive/README.md +8 -0
- package/qtty-js/qtty/qtty-derive/src/lib.rs +340 -0
- package/qtty-js/qtty/qtty-ffi/ARCHITECTURE.md +3 -0
- package/qtty-js/qtty/qtty-ffi/Cargo.toml +31 -0
- package/qtty-js/qtty/qtty-ffi/README.md +9 -0
- package/qtty-js/qtty/qtty-ffi/build.rs +326 -0
- package/qtty-js/qtty/qtty-ffi/cbindgen.toml +105 -0
- package/qtty-js/qtty/qtty-ffi/include/qtty_ffi.h +1126 -0
- package/qtty-js/qtty/qtty-ffi/src/ffi.rs +1251 -0
- package/qtty-js/qtty/qtty-ffi/src/ffi_serde.rs +294 -0
- package/qtty-js/qtty/qtty-ffi/src/helpers.rs +310 -0
- package/qtty-js/qtty/qtty-ffi/src/lib.rs +229 -0
- package/qtty-js/qtty/qtty-ffi/src/macros.rs +121 -0
- package/qtty-js/qtty/qtty-ffi/src/registry.rs +274 -0
- package/qtty-js/qtty/qtty-ffi/src/types.rs +620 -0
- package/qtty-js/qtty/qtty-ffi/tests/integration_tests.rs +842 -0
- package/qtty-js/qtty/qtty-ffi/units.csv +156 -0
- package/qtty-js/qtty/qtty-ffi/units.csv.md +3 -0
- package/qtty-js/qtty-node/.prettierignore +6 -0
- package/qtty-js/qtty-node/.prettierrc.json +6 -0
- package/qtty-js/qtty-node/README.md +250 -0
- package/qtty-js/qtty-node/c8.config.json +11 -0
- package/qtty-js/qtty-node/eslint.config.js +31 -0
- package/qtty-js/qtty-node/examples/arithmetic.mjs +64 -0
- package/qtty-js/qtty-node/examples/astronomy.mjs +90 -0
- package/qtty-js/qtty-node/examples/quickstart.mjs +36 -0
- package/qtty-js/qtty-node/examples/serialization.mjs +125 -0
- package/qtty-js/qtty-node/examples/unit_factories.mjs +74 -0
- package/qtty-js/qtty-node/index.d.ts +219 -0
- package/qtty-js/qtty-node/index.js +323 -0
- package/qtty-js/qtty-node/lib/DerivedQuantity.js +122 -0
- package/qtty-js/qtty-node/lib/Quantity.js +151 -0
- package/qtty-js/qtty-node/lib/backend.js +25 -0
- package/qtty-js/qtty-node/native.cjs +306 -0
- package/qtty-js/qtty-node/package-lock.json +3223 -0
- package/qtty-js/qtty-node/package.json +70 -0
- package/qtty-js/qtty-node/units.d.ts +299 -0
- package/qtty-js/qtty-node/units.js +210 -0
- package/qtty-js/qtty-web/Cargo.lock +767 -0
- package/qtty-js/qtty-web/Cargo.toml +21 -0
- package/qtty-js/qtty-web/index.d.ts +140 -0
- package/qtty-js/qtty-web/index.js +20 -0
- package/qtty-js/qtty-web/lib/DerivedQuantity.js +58 -0
- package/qtty-js/qtty-web/lib/Quantity.js +75 -0
- package/qtty-js/qtty-web/lib/backend.js +80 -0
- package/qtty-js/qtty-web/package.json +45 -0
- package/qtty-js/qtty-web/src/lib.rs +111 -0
- package/qtty-js/scripts/ci.sh +73 -0
- package/scripts/ci.sh +123 -0
- package/siderust-core/Cargo.lock +787 -0
- package/siderust-core/Cargo.toml +18 -0
- package/siderust-core/DEDUPLICATION.md +124 -0
- package/siderust-core/src/body.rs +120 -0
- package/siderust-core/src/events.rs +184 -0
- package/siderust-core/src/lib.rs +20 -0
- package/siderust-core/src/observer.rs +55 -0
- package/siderust-core/src/position.rs +213 -0
- package/siderust-node/.prettierignore +7 -0
- package/siderust-node/.prettierrc.json +6 -0
- package/siderust-node/Cargo.lock +906 -0
- package/siderust-node/Cargo.toml +29 -0
- package/siderust-node/README.md +109 -0
- package/siderust-node/__test__/index.test.mjs +248 -0
- package/siderust-node/build.rs +5 -0
- package/siderust-node/c8.config.json +3 -0
- package/siderust-node/eslint.config.js +31 -0
- package/siderust-node/examples/01_basic_coordinates.mjs +24 -0
- package/siderust-node/examples/02_coordinate_transformations.mjs +25 -0
- package/siderust-node/examples/03_all_frames_conversions.mjs +26 -0
- package/siderust-node/examples/04_all_center_conversions.mjs +24 -0
- package/siderust-node/examples/05_target_tracking.mjs +22 -0
- package/siderust-node/examples/06_night_events.mjs +18 -0
- package/siderust-node/examples/07_moon_properties.mjs +21 -0
- package/siderust-node/examples/08_solar_system.mjs +19 -0
- package/siderust-node/examples/09_star_observability.mjs +22 -0
- package/siderust-node/examples/10_time_periods.mjs +9 -0
- package/siderust-node/examples/11_serialization.mjs +31 -0
- package/siderust-node/examples/12_runtime_ephemeris.mjs +27 -0
- package/siderust-node/examples/13_coordinate_operations.mjs +20 -0
- package/siderust-node/index.d.ts +623 -0
- package/siderust-node/index.js +79 -0
- package/siderust-node/lib/Observer.js +112 -0
- package/siderust-node/lib/Star.js +118 -0
- package/siderust-node/lib/backend.js +63 -0
- package/siderust-node/lib/wrappers.js +566 -0
- package/siderust-node/main.js +20 -0
- package/siderust-node/native.cjs +360 -0
- package/siderust-node/package-lock.json +3261 -0
- package/siderust-node/package.json +71 -0
- package/siderust-node/src/body.rs +74 -0
- package/siderust-node/src/coordinates.rs +372 -0
- package/siderust-node/src/ephemeris.rs +462 -0
- package/siderust-node/src/events.rs +577 -0
- package/siderust-node/src/lib.rs +43 -0
- package/siderust-node/src/observer.rs +132 -0
- package/siderust-node/src/phase.rs +218 -0
- package/siderust-node/src/position.rs +292 -0
- package/siderust-node/src/star.rs +200 -0
- package/siderust-web/Cargo.lock +855 -0
- package/siderust-web/Cargo.toml +34 -0
- package/siderust-web/README.md +100 -0
- package/siderust-web/__test__/index.test.mjs +118 -0
- package/siderust-web/examples/github-pages/README.md +31 -0
- package/siderust-web/examples/github-pages/index.html +135 -0
- package/siderust-web/index.d.ts +311 -0
- package/siderust-web/index.js +66 -0
- package/siderust-web/lib/Observer.js +103 -0
- package/siderust-web/lib/Star.js +116 -0
- package/siderust-web/lib/backend.js +400 -0
- package/siderust-web/lib/wrappers.js +512 -0
- package/siderust-web/package.json +55 -0
- package/siderust-web/src/body.rs +69 -0
- package/siderust-web/src/coordinates.rs +302 -0
- package/siderust-web/src/ephemeris.rs +456 -0
- package/siderust-web/src/events.rs +520 -0
- package/siderust-web/src/lib.rs +51 -0
- package/siderust-web/src/observer.rs +117 -0
- package/siderust-web/src/phase.rs +190 -0
- package/siderust-web/src/position.rs +291 -0
- package/siderust-web/src/star.rs +178 -0
- package/tempoch-js/.github/workflows/ci.yml +142 -0
- package/tempoch-js/.gitmodules +3 -0
- package/tempoch-js/CHANGELOG.md +25 -0
- package/tempoch-js/LICENSE +661 -0
- package/tempoch-js/README.md +126 -0
- package/tempoch-js/package.json +20 -0
- package/tempoch-js/scripts/ci.sh +73 -0
- package/tempoch-js/tempoch/.github/workflows/ci.yml +113 -0
- package/tempoch-js/tempoch/CHANGELOG.md +82 -0
- package/tempoch-js/tempoch/Cargo.lock +947 -0
- package/tempoch-js/tempoch/Cargo.toml +3 -0
- package/tempoch-js/tempoch/LICENSE +661 -0
- package/tempoch-js/tempoch/README.md +76 -0
- package/tempoch-js/tempoch/tempoch/Cargo.toml +27 -0
- package/tempoch-js/tempoch/tempoch/examples/periods.rs +45 -0
- package/tempoch-js/tempoch/tempoch/examples/quickstart.rs +13 -0
- package/tempoch-js/tempoch/tempoch/src/lib.rs +49 -0
- package/tempoch-js/tempoch/tempoch/tests/integration.rs +57 -0
- package/tempoch-js/tempoch/tempoch-core/Cargo.toml +24 -0
- package/tempoch-js/tempoch/tempoch-core/src/delta_t.rs +345 -0
- package/tempoch-js/tempoch/tempoch-core/src/instant.rs +811 -0
- package/tempoch-js/tempoch/tempoch-core/src/julian_date_ext.rs +142 -0
- package/tempoch-js/tempoch/tempoch-core/src/lib.rs +81 -0
- package/tempoch-js/tempoch/tempoch-core/src/period.rs +1168 -0
- package/tempoch-js/tempoch/tempoch-core/src/scales.rs +779 -0
- package/tempoch-js/tempoch/tempoch-ffi/Cargo.lock +889 -0
- package/tempoch-js/tempoch/tempoch-ffi/Cargo.toml +26 -0
- package/tempoch-js/tempoch/tempoch-ffi/build.rs +24 -0
- package/tempoch-js/tempoch/tempoch-ffi/cbindgen.toml +30 -0
- package/tempoch-js/tempoch/tempoch-ffi/src/error.rs +19 -0
- package/tempoch-js/tempoch/tempoch-ffi/src/lib.rs +82 -0
- package/tempoch-js/tempoch/tempoch-ffi/src/period.rs +101 -0
- package/tempoch-js/tempoch/tempoch-ffi/src/time.rs +711 -0
- package/tempoch-js/tempoch/tempoch-ffi/tests/ffi.rs +265 -0
- package/tempoch-js/tempoch-node/.prettierignore +6 -0
- package/tempoch-js/tempoch-node/.prettierrc.json +6 -0
- package/tempoch-js/tempoch-node/Cargo.lock +496 -0
- package/tempoch-js/tempoch-node/Cargo.toml +29 -0
- package/tempoch-js/tempoch-node/README.md +265 -0
- package/tempoch-js/tempoch-node/__test__/index.test.mjs +598 -0
- package/tempoch-js/tempoch-node/build.rs +5 -0
- package/tempoch-js/tempoch-node/c8.config.json +3 -0
- package/tempoch-js/tempoch-node/eslint.config.js +31 -0
- package/tempoch-js/tempoch-node/examples/periods.mjs +79 -0
- package/tempoch-js/tempoch-node/examples/quickstart.mjs +71 -0
- package/tempoch-js/tempoch-node/examples/timescales.mjs +92 -0
- package/tempoch-js/tempoch-node/index.d.ts +280 -0
- package/tempoch-js/tempoch-node/index.js +32 -0
- package/tempoch-js/tempoch-node/lib/JulianDate.js +176 -0
- package/tempoch-js/tempoch-node/lib/ModifiedJulianDate.js +156 -0
- package/tempoch-js/tempoch-node/lib/Period.js +133 -0
- package/tempoch-js/tempoch-node/lib/backend.js +38 -0
- package/tempoch-js/tempoch-node/lib/qttyCompat.js +92 -0
- package/tempoch-js/tempoch-node/native.cjs +317 -0
- package/tempoch-js/tempoch-node/package-lock.json +3223 -0
- package/tempoch-js/tempoch-node/package.json +56 -0
- package/tempoch-js/tempoch-node/src/lib.rs +573 -0
- package/tempoch-js/tempoch-web/Cargo.toml +23 -0
- package/tempoch-js/tempoch-web/index.d.ts +95 -0
- package/tempoch-js/tempoch-web/index.js +27 -0
- package/tempoch-js/tempoch-web/lib/JulianDate.js +170 -0
- package/tempoch-js/tempoch-web/lib/ModifiedJulianDate.js +145 -0
- package/tempoch-js/tempoch-web/lib/Period.js +121 -0
- package/tempoch-js/tempoch-web/lib/backend.js +118 -0
- package/tempoch-js/tempoch-web/package.json +46 -0
- 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
|
+
}
|