Skip to main content

tempoch_core/
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    /// Input or arithmetic produced `NaN` or `±∞`.
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("time value must be finite (not NaN or infinity)"),
46            Self::UtcBeforeDefinition => f.write_str(
47                "date precedes 1961-01-01, before which UTC was not defined; \
48                 use TimeContext::allow_pre_definition_utc() to permit extrapolation",
49            ),
50        }
51    }
52}
53
54impl std::error::Error for ConversionError {}
55
56/// Error surface for runtime time-data operations.
57///
58/// Returned by [`crate::update_runtime_time_data`] and
59/// [`crate::refresh_runtime_time_data`] when the runtime data bundle cannot be
60/// loaded or refreshed.
61#[derive(Debug)]
62pub enum TimeDataError {
63    /// An I/O error occurred while reading or writing the data bundle.
64    Io(std::io::Error),
65    /// A network download failed.
66    Download(String),
67    /// The data could not be parsed.
68    Parse(String),
69    /// The data bundle failed an integrity check.
70    Integrity(String),
71}
72
73impl core::fmt::Display for TimeDataError {
74    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
75        match self {
76            Self::Io(err) => write!(f, "I/O error: {err}"),
77            Self::Download(msg) => write!(f, "download error: {msg}"),
78            Self::Parse(msg) => write!(f, "parse error: {msg}"),
79            Self::Integrity(msg) => write!(f, "integrity error: {msg}"),
80        }
81    }
82}
83
84impl std::error::Error for TimeDataError {
85    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
86        match self {
87            Self::Io(err) => Some(err),
88            _ => None,
89        }
90    }
91}
92
93impl From<std::io::Error> for TimeDataError {
94    fn from(err: std::io::Error) -> Self {
95        Self::Io(err)
96    }
97}
98
99impl From<tempoch_time_data::TimeDataError> for TimeDataError {
100    fn from(err: tempoch_time_data::TimeDataError) -> Self {
101        match err {
102            tempoch_time_data::TimeDataError::Io(e) => Self::Io(e),
103            tempoch_time_data::TimeDataError::Download(msg) => Self::Download(msg),
104            tempoch_time_data::TimeDataError::Parse(msg) => Self::Parse(msg),
105            tempoch_time_data::TimeDataError::Integrity(msg) => Self::Integrity(msg),
106        }
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use std::error::Error;
114
115    fn io_error(msg: &str) -> std::io::Error {
116        std::io::Error::other(msg.to_string())
117    }
118
119    #[test]
120    fn display_all_variants() {
121        let cases: &[(ConversionError, &str)] = &[
122            (ConversionError::UtcHistoryUnsupported, "history"),
123            (ConversionError::InvalidLeapSecond, "leap-second"),
124            (ConversionError::OutOfRange, "range"),
125            (ConversionError::Ut1HorizonExceeded, "horizon"),
126            (ConversionError::NonFinite, "finite"),
127            (ConversionError::UtcBeforeDefinition, "1961"),
128        ];
129        for (variant, fragment) in cases {
130            let s = variant.to_string();
131            assert!(s.contains(fragment), "{variant:?}: got {s:?}");
132        }
133    }
134
135    #[test]
136    fn time_data_error_display_and_source() {
137        let io = TimeDataError::Io(io_error("disk full"));
138        assert!(io.to_string().contains("I/O error: disk full"));
139        assert!(io.source().is_some());
140
141        let download = TimeDataError::Download("network timeout".to_string());
142        assert!(download
143            .to_string()
144            .contains("download error: network timeout"));
145        assert!(download.source().is_none());
146
147        let parse = TimeDataError::Parse("bad row".to_string());
148        assert!(parse.to_string().contains("parse error: bad row"));
149        assert!(parse.source().is_none());
150
151        let integrity = TimeDataError::Integrity("checksum mismatch".to_string());
152        assert!(integrity
153            .to_string()
154            .contains("integrity error: checksum mismatch"));
155        assert!(integrity.source().is_none());
156    }
157
158    #[test]
159    fn time_data_error_from_mappings_cover_all_variants() {
160        let io_mapped: TimeDataError = io_error("io map").into();
161        assert!(matches!(io_mapped, TimeDataError::Io(_)));
162
163        let mapped_download: TimeDataError =
164            tempoch_time_data::TimeDataError::Download("d".to_string()).into();
165        assert!(matches!(mapped_download, TimeDataError::Download(msg) if msg == "d"));
166
167        let mapped_parse: TimeDataError =
168            tempoch_time_data::TimeDataError::Parse("p".to_string()).into();
169        assert!(matches!(mapped_parse, TimeDataError::Parse(msg) if msg == "p"));
170
171        let mapped_integrity: TimeDataError =
172            tempoch_time_data::TimeDataError::Integrity("i".to_string()).into();
173        assert!(matches!(mapped_integrity, TimeDataError::Integrity(msg) if msg == "i"));
174
175        let mapped_io: TimeDataError = tempoch_time_data::TimeDataError::Io(io_error("x")).into();
176        assert!(matches!(mapped_io, TimeDataError::Io(_)));
177    }
178}