Skip to main content

ucal_core/
error.rs

1//! Diagnostics (Appendix E).
2//!
3//! Every error carries its `UCAL-Ennnn` code, because the codes are the stable
4//! contract: §22's conformance classes and §21.3's required assertions are
5//! written in terms of them, not in terms of Rust type names.
6
7use core::fmt;
8
9/// A diagnostic code from Appendix E.
10#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
11#[non_exhaustive]
12pub enum Code {
13    // --- text and structure ---
14    /// Malformed timestamp.
15    E0001,
16    /// Unknown profile tag.
17    E0002,
18    /// Mixed text forms in one string (Rule D).
19    E0003,
20    /// Group value out of range (> 3124).
21    E0004,
22    /// Invalid base-5 digit.
23    E0005,
24    /// Non-contiguous tier sequence.
25    E0006,
26    /// Calendar rendering without a kind/id qualifier (§6.6).
27    E0007,
28    /// Locale table load failure.
29    E0010,
30    /// Duplicate name in the active locale table (Rule N).
31    E0011,
32    /// Unknown key in an HJSON data file.
33    E0012,
34    /// Profile lacks a `datum_provenance` record (Rule Q.4).
35    E0013,
36
37    // --- domain ---
38    /// Result precedes the datum (Rule Z).
39    E0020,
40    /// Result exceeds DOMAIN (Rules O, W).
41    E0021,
42    /// Window inversion, `lo > hi` (Rule U).
43    E0022,
44    /// Comparison indeterminate at the stated precision (Rule T).
45    E0023,
46    /// Lossy rendering requested without a rounding mode (Rule R).
47    E0024,
48    /// `BIG_BANG_CLAIM` or its half-width used as a computational operand
49    /// (Rule Q.3). Reaching this at runtime is an internal invariant violation;
50    /// the type system is supposed to make it unreachable, and §21.3-3 requires
51    /// a compile-fail test proving so.
52    E0025,
53
54    // --- binary and identifiers ---
55    /// Binary form is not 64 bytes (Rule B).
56    E0030,
57    /// Instant outside UCID range (Rule I).
58    E0031,
59    /// Invalid Crockford base-32.
60    E0032,
61
62    // --- civil bridge (§8, §14) ---
63    /// Civil date outside the renderable range (§14.3). Never a panic.
64    E0040,
65    /// Invalid civil date for the stated calendar.
66    E0041,
67    /// `second = 60` outside a leap-second instant.
68    E0042,
69    /// Foreign-unit input finer than the bridge constant permits (Rules A, R, Y).
70    E0043,
71
72    // --- profile ---
73    /// Profile mismatch (Rule P).
74    E0050,
75
76    // --- calendars (§9, §8.6) ---
77    /// Body parameter missing required provenance or as-measured value
78    /// (Rules C, Y).
79    E0060,
80    /// Leap-rule derivation cannot meet the requested drift bound.
81    E0061,
82    /// Calendar has no anchor; local fields cannot be produced (Rule J.3).
83    E0062,
84    /// Anchor phase definition not evaluable for this body's parameters
85    /// (Rule J.4).
86    E0063,
87    /// Grouping cycle requested but none derivable from any satellite (§9.6).
88    E0064,
89    /// Legacy calendar supplied where a derived calendar is required (Rule K.6).
90    E0065,
91
92    // --- numerics and cosmology (Appendix H, Rule X) ---
93    /// Division by zero, or by an interval containing zero (Appendix H.3);
94    /// also a cosmology inversion that failed to bracket.
95    E0070,
96    /// Requested enclosure width unreachable at the permitted depth (Rule X).
97    E0071,
98
99    // --- tier grid ---
100    /// Tier index outside the profile's grid.
101    E0080,
102}
103
104impl Code {
105    /// The wire-stable code string, e.g. `"UCAL-E0021"`.
106    pub const fn as_str(self) -> &'static str {
107        match self {
108            Code::E0001 => "UCAL-E0001",
109            Code::E0002 => "UCAL-E0002",
110            Code::E0003 => "UCAL-E0003",
111            Code::E0004 => "UCAL-E0004",
112            Code::E0005 => "UCAL-E0005",
113            Code::E0006 => "UCAL-E0006",
114            Code::E0007 => "UCAL-E0007",
115            Code::E0010 => "UCAL-E0010",
116            Code::E0011 => "UCAL-E0011",
117            Code::E0012 => "UCAL-E0012",
118            Code::E0013 => "UCAL-E0013",
119            Code::E0020 => "UCAL-E0020",
120            Code::E0021 => "UCAL-E0021",
121            Code::E0022 => "UCAL-E0022",
122            Code::E0023 => "UCAL-E0023",
123            Code::E0024 => "UCAL-E0024",
124            Code::E0025 => "UCAL-E0025",
125            Code::E0030 => "UCAL-E0030",
126            Code::E0031 => "UCAL-E0031",
127            Code::E0032 => "UCAL-E0032",
128            Code::E0040 => "UCAL-E0040",
129            Code::E0041 => "UCAL-E0041",
130            Code::E0042 => "UCAL-E0042",
131            Code::E0043 => "UCAL-E0043",
132            Code::E0050 => "UCAL-E0050",
133            Code::E0060 => "UCAL-E0060",
134            Code::E0061 => "UCAL-E0061",
135            Code::E0062 => "UCAL-E0062",
136            Code::E0063 => "UCAL-E0063",
137            Code::E0064 => "UCAL-E0064",
138            Code::E0065 => "UCAL-E0065",
139            Code::E0070 => "UCAL-E0070",
140            Code::E0071 => "UCAL-E0071",
141            Code::E0080 => "UCAL-E0080",
142        }
143    }
144
145    /// One-line description, matching Appendix E.
146    pub const fn describe(self) -> &'static str {
147        match self {
148            Code::E0001 => "malformed timestamp",
149            Code::E0002 => "unknown profile tag",
150            Code::E0003 => "mixed text forms in one string",
151            Code::E0004 => "group value out of range (> 3124)",
152            Code::E0005 => "invalid base-5 digit",
153            Code::E0006 => "non-contiguous tier sequence",
154            Code::E0007 => "calendar rendering without a kind/id qualifier",
155            Code::E0010 => "locale table load failure",
156            Code::E0011 => "duplicate name in the active locale table",
157            Code::E0012 => "unknown key in HJSON data file",
158            Code::E0013 => "profile lacks a datum_provenance record",
159            Code::E0020 => "result precedes the datum",
160            Code::E0021 => "result exceeds DOMAIN",
161            Code::E0022 => "window inversion, lo > hi",
162            Code::E0023 => "comparison indeterminate at stated precision",
163            Code::E0024 => "lossy rendering requested without a rounding mode",
164            Code::E0025 => "BIG_BANG_CLAIM used as a computational operand",
165            Code::E0030 => "binary form is not 64 bytes",
166            Code::E0031 => "instant outside UCID range",
167            Code::E0032 => "invalid Crockford base-32",
168            Code::E0040 => "civil date outside renderable range",
169            Code::E0041 => "invalid civil date for the stated calendar",
170            Code::E0042 => "second = 60 outside a leap-second instant",
171            Code::E0043 => "foreign-unit input finer than the bridge constant permits",
172            Code::E0050 => "profile mismatch",
173            Code::E0060 => "body parameter missing required provenance or as-measured value",
174            Code::E0061 => "leap-rule derivation cannot meet the requested drift bound",
175            Code::E0062 => "calendar has no anchor; local fields cannot be produced",
176            Code::E0063 => "anchor phase definition not evaluable for this body",
177            Code::E0064 => "grouping cycle requested but none derivable from any satellite",
178            Code::E0065 => "legacy calendar supplied where a derived calendar is required",
179            Code::E0070 => "division by zero or by an interval containing zero",
180            Code::E0071 => "requested enclosure width unreachable at the permitted depth",
181            Code::E0080 => "tier index outside the profile grid",
182        }
183    }
184
185    /// CLI exit code for this diagnostic (§19.5).
186    pub const fn exit_code(self) -> u8 {
187        match self {
188            Code::E0001
189            | Code::E0002
190            | Code::E0003
191            | Code::E0004
192            | Code::E0005
193            | Code::E0006
194            | Code::E0007
195            | Code::E0032 => 2,
196            Code::E0020 | Code::E0021 | Code::E0022 | Code::E0030 | Code::E0031
197            | Code::E0080 => 3,
198            Code::E0023 | Code::E0024 | Code::E0043 => 4,
199            Code::E0040 | Code::E0041 | Code::E0042 => 2,
200            Code::E0050 => 5,
201            Code::E0060 | Code::E0061 | Code::E0062 | Code::E0063 | Code::E0064
202            | Code::E0065 => 7,
203            Code::E0070 | Code::E0071 => 8,
204            Code::E0010 | Code::E0011 | Code::E0012 | Code::E0013 => 6,
205            Code::E0025 => 9,
206        }
207    }
208}
209
210impl fmt::Display for Code {
211    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212        write!(f, "{}: {}", self.as_str(), self.describe())
213    }
214}
215
216/// An error from absolute-time arithmetic or representation.
217#[derive(Clone, Copy, PartialEq, Eq, Debug)]
218pub struct TimeError {
219    /// The Appendix E code.
220    pub code: Code,
221    /// Optional static context, e.g. which tier was out of range.
222    pub context: Option<&'static str>,
223}
224
225impl TimeError {
226    /// Construct from a code.
227    pub const fn new(code: Code) -> Self {
228        TimeError {
229            code,
230            context: None,
231        }
232    }
233    /// Construct with static context.
234    pub const fn with_context(code: Code, context: &'static str) -> Self {
235        TimeError {
236            code,
237            context: Some(context),
238        }
239    }
240}
241
242impl fmt::Display for TimeError {
243    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244        match self.context {
245            None => write!(f, "{}", self.code),
246            Some(c) => write!(f, "{} ({c})", self.code),
247        }
248    }
249}
250
251#[cfg(feature = "std")]
252impl std::error::Error for TimeError {}
253
254/// Shorthand for the crate's fallible operations.
255pub type Result<T> = core::result::Result<T, TimeError>;
256
257/// A diagnostic warning from Appendix E's W-series.
258///
259/// Warnings are separate from [`Code`] because they never abort an operation:
260/// they accompany a value that was produced, and the caller decides what to do.
261/// Rule R requires lossy renderings to be reported, and §8.4 requires a stale
262/// leap-second table to warn with a bounded error rather than convert silently.
263#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
264#[non_exhaustive]
265pub enum Warning {
266    /// Precision loss in the requested rendering (Rule R).
267    W0001,
268    /// Leap-second table may be stale; the bounded error is reported with it.
269    W0002,
270    /// Body parameter evaluated outside its validity window (Rule C).
271    W0003,
272    /// Cosmology enclosure width exceeds one tick.
273    W0004,
274    /// Value produced by a legacy, non-derived calendar (§8.6).
275    W0005,
276    /// Quantity comparable to or smaller than `BIG_BANG_CLAIM`; the datum's
277    /// physical identification is uncertain at this scale (§10.6).
278    W0006,
279}
280
281impl Warning {
282    /// The wire-stable code string, e.g. `"UCAL-W0001"`.
283    pub const fn as_str(self) -> &'static str {
284        match self {
285            Warning::W0001 => "UCAL-W0001",
286            Warning::W0002 => "UCAL-W0002",
287            Warning::W0003 => "UCAL-W0003",
288            Warning::W0004 => "UCAL-W0004",
289            Warning::W0005 => "UCAL-W0005",
290            Warning::W0006 => "UCAL-W0006",
291        }
292    }
293
294    /// One-line description, matching Appendix E.
295    pub const fn describe(self) -> &'static str {
296        match self {
297            Warning::W0001 => "precision loss in the requested rendering",
298            Warning::W0002 => "leap-second table may be stale; bounded error reported",
299            Warning::W0003 => "body parameter evaluated outside its validity window",
300            Warning::W0004 => "cosmology enclosure width exceeds one tick",
301            Warning::W0005 => "value produced by a legacy (non-derived) calendar",
302            Warning::W0006 => "quantity comparable to or smaller than BIG_BANG_CLAIM",
303        }
304    }
305}
306
307impl fmt::Display for Warning {
308    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309        write!(f, "{}: {}", self.as_str(), self.describe())
310    }
311}