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