Skip to main content

tempoch_core/foundation/
error.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2// Copyright (C) 2026 Vallés Puig, Ramon
3
4/// Conversion error surface.
5///
6/// Variants are payload-free in v1 to keep the matrix small; they may carry
7/// context in later phases if a concrete call-site demands it.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ConversionError {
10    /// The active UTC history or policy cannot represent the requested date.
11    UtcHistoryUnsupported,
12    /// A leap-second label does not correspond to a leap second in the
13    /// compiled UTC history.
14    InvalidLeapSecond,
15    /// The converted value leaves the representable range of the target.
16    OutOfRange,
17    /// A UT1 conversion was requested outside the horizon of the configured
18    /// ΔT model or observed-data source.
19    Ut1HorizonExceeded,
20    /// Scalar was NaN, arithmetic collapsed to NaN, or a conversion requires a finite coordinate but received ±∞ / NaN.
21    NonFinite,
22    /// The requested date precedes 1961-01-01, before which UTC was not
23    /// defined as an international standard.
24    ///
25    /// The crate can back-extrapolate the first official UTC-TAI segment to
26    /// cover older civil labels, but that extrapolation is not historically
27    /// defined UTC and is therefore opt-in. Pass a context built with
28    /// [`crate::TimeContext::allow_pre_definition_utc`] to enable it.
29    UtcBeforeDefinition,
30}
31
32impl core::fmt::Display for ConversionError {
33    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
34        match self {
35            Self::UtcHistoryUnsupported => {
36                f.write_str("UTC history is unavailable for the requested date")
37            }
38            Self::InvalidLeapSecond => {
39                f.write_str("leap-second label is not present in the compiled UTC history")
40            }
41            Self::OutOfRange => f.write_str("converted value is out of representable range"),
42            Self::Ut1HorizonExceeded => {
43                f.write_str("UT1 conversion exceeds the ΔT model or data horizon")
44            }
45            Self::NonFinite => f.write_str(
46                "time coordinate is not usable for this operation (NaN or unsupported non-finite value)",
47            ),
48            Self::UtcBeforeDefinition => f.write_str(
49                "date precedes 1961-01-01, before which UTC was not defined; \
50                 use TimeContext::allow_pre_definition_utc() to permit extrapolation",
51            ),
52        }
53    }
54}
55
56impl std::error::Error for ConversionError {}
57
58/// Error surface for runtime time-data operations.
59///
60/// Returned by `update_runtime_time_data` and `refresh_runtime_time_data`
61/// (available with the `runtime-data-fetch` feature) when the runtime data
62/// bundle cannot be loaded or refreshed.
63#[derive(Debug)]
64pub enum TimeDataError {
65    /// An I/O error occurred while reading or writing the data bundle.
66    Io(std::io::Error),
67    /// A network download failed.
68    Download(String),
69    /// The data could not be parsed.
70    Parse(String),
71    /// The data bundle failed an integrity check.
72    Integrity(String),
73}
74
75impl core::fmt::Display for TimeDataError {
76    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
77        match self {
78            Self::Io(err) => write!(f, "I/O error: {err}"),
79            Self::Download(msg) => write!(f, "download error: {msg}"),
80            Self::Parse(msg) => write!(f, "parse error: {msg}"),
81            Self::Integrity(msg) => write!(f, "integrity error: {msg}"),
82        }
83    }
84}
85
86impl std::error::Error for TimeDataError {
87    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
88        match self {
89            Self::Io(err) => Some(err),
90            _ => None,
91        }
92    }
93}
94
95impl From<std::io::Error> for TimeDataError {
96    fn from(err: std::io::Error) -> Self {
97        Self::Io(err)
98    }
99}
100
101impl From<crate::archive::time::TimeDataError> for TimeDataError {
102    fn from(err: crate::archive::time::TimeDataError) -> Self {
103        match err {
104            crate::archive::time::TimeDataError::Io(e) => Self::Io(e),
105            crate::archive::time::TimeDataError::Download(msg) => Self::Download(msg),
106            crate::archive::time::TimeDataError::Parse(msg) => Self::Parse(msg),
107            crate::archive::time::TimeDataError::Integrity(msg) => Self::Integrity(msg),
108        }
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use std::error::Error;
116
117    fn io_error(msg: &str) -> std::io::Error {
118        std::io::Error::other(msg.to_string())
119    }
120
121    #[test]
122    fn display_all_variants() {
123        let cases: &[(ConversionError, &str)] = &[
124            (ConversionError::UtcHistoryUnsupported, "history"),
125            (ConversionError::InvalidLeapSecond, "leap-second"),
126            (ConversionError::OutOfRange, "range"),
127            (ConversionError::Ut1HorizonExceeded, "horizon"),
128            (ConversionError::NonFinite, "usable"),
129            (ConversionError::UtcBeforeDefinition, "1961"),
130        ];
131        for (variant, fragment) in cases {
132            let s = variant.to_string();
133            assert!(s.contains(fragment), "{variant:?}: got {s:?}");
134        }
135    }
136
137    #[test]
138    fn time_data_error_display_and_source() {
139        let io = TimeDataError::Io(io_error("disk full"));
140        assert!(io.to_string().contains("I/O error: disk full"));
141        assert!(io.source().is_some());
142
143        let download = TimeDataError::Download("network timeout".to_string());
144        assert!(download
145            .to_string()
146            .contains("download error: network timeout"));
147        assert!(download.source().is_none());
148
149        let parse = TimeDataError::Parse("bad row".to_string());
150        assert!(parse.to_string().contains("parse error: bad row"));
151        assert!(parse.source().is_none());
152
153        let integrity = TimeDataError::Integrity("checksum mismatch".to_string());
154        assert!(integrity
155            .to_string()
156            .contains("integrity error: checksum mismatch"));
157        assert!(integrity.source().is_none());
158    }
159
160    #[test]
161    fn time_data_error_from_mappings_cover_all_variants() {
162        let io_mapped: TimeDataError = io_error("io map").into();
163        assert!(matches!(io_mapped, TimeDataError::Io(_)));
164
165        let mapped_download: TimeDataError =
166            crate::archive::time::TimeDataError::Download("d".to_string()).into();
167        assert!(matches!(mapped_download, TimeDataError::Download(msg) if msg == "d"));
168
169        let mapped_parse: TimeDataError =
170            crate::archive::time::TimeDataError::Parse("p".to_string()).into();
171        assert!(matches!(mapped_parse, TimeDataError::Parse(msg) if msg == "p"));
172
173        let mapped_integrity: TimeDataError =
174            crate::archive::time::TimeDataError::Integrity("i".to_string()).into();
175        assert!(matches!(mapped_integrity, TimeDataError::Integrity(msg) if msg == "i"));
176
177        let mapped_io: TimeDataError =
178            crate::archive::time::TimeDataError::Io(io_error("x")).into();
179        assert!(matches!(mapped_io, TimeDataError::Io(_)));
180    }
181}