parsidate/error.rs
1// ~/src/error.rs
2//
3// * Copyright (C) ParsiCore (parsidate) 2024-2025 <parsicore.dev@gmail.com>
4// * Package : parsidate
5// * License : Apache-2.0
6// * Version : 1.7.1
7// * URL : https://github.com/parsicore/parsidate
8// * Sign: parsidate-20250607-fea13e856dcd-459c6e73c83e49e10162ee28b26ac7cd
9//
10//! # Centralized Error Handling for `parsidate`
11//!
12//! This module defines the error types used throughout the `parsidate` library. The primary
13//! error type is [`DateError`], which encapsulates all possible failures that can occur during
14//! date and time operations.
15//!
16//! For parsing operations, `DateError` contains a more granular [`ParseErrorKind`] to provide
17//! specific details about why a string could not be parsed into a `ParsiDate` or `ParsiDateTime`.
18//!
19//! Both error types implement the standard `std::error::Error` and `std::fmt::Display` traits,
20//! ensuring they integrate seamlessly into the Rust ecosystem for error handling and reporting.
21
22use std::fmt;
23
24// --- Data Structures ---
25
26/// The primary error enum for all operations in the `parsidate` library.
27///
28/// This enum covers all failure modes, from invalid date construction and parsing
29/// to arithmetic overflows and conversion issues.
30#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
31pub enum DateError {
32 /// Indicates that a given combination of year, month, and day is not a valid
33 /// date in the Persian calendar.
34 ///
35 /// This error occurs when:
36 /// - The year is outside the supported range of `1-9999`.
37 /// - The month is not between `1` and `12`.
38 /// - The day is `0` or greater than the number of days in the given month and year
39 /// (e.g., day `31` in Mehr, or day `30` in Esfand of a common year).
40 ///
41 /// Returned by: [`ParsiDate::new`](crate::date::ParsiDate::new), [`ParsiDateTime::new`](crate::datetime::ParsiDateTime::new), and various arithmetic methods.
42 InvalidDate,
43
44 /// Indicates that a provided hour, minute, or second is outside its valid range.
45 ///
46 /// This error occurs when:
47 /// - `hour` is greater than `23`.
48 /// - `minute` is greater than `59`.
49 /// - `second` is greater than `59`.
50 ///
51 /// Returned by: [`ParsiDateTime::new`](crate::datetime::ParsiDateTime::new) and `with_*` methods for time components.
52 InvalidTime,
53
54 /// An error occurred during conversion to or from the Gregorian calendar.
55 ///
56 /// This can happen if:
57 /// - The Gregorian date is before the Persian epoch (approximately March 21, 622 CE).
58 /// - The resulting date would fall outside the range supported by `chrono::NaiveDate`.
59 /// - An internal calculation error or overflow occurred during the conversion process.
60 ///
61 /// Returned by: [`ParsiDate::from_gregorian`](crate::date::ParsiDate::from_gregorian), [`ParsiDate::to_gregorian`](crate::date::ParsiDate::to_gregorian), and methods that rely on them.
62 GregorianConversionError,
63
64 /// A string could not be parsed into a `ParsiDate` or `ParsiDateTime`.
65 ///
66 /// This variant wraps a [`ParseErrorKind`] which provides specific details about the
67 /// nature of the parsing failure.
68 ParseError(ParseErrorKind),
69
70 /// A date arithmetic operation resulted in an overflow or an out-of-range date.
71 ///
72 /// This error occurs when adding or subtracting durations, months, or years results in
73 /// a Persian year outside the supported `1-9999` range, or if an internal integer
74 /// overflow happens during the calculation.
75 ArithmeticOverflow,
76
77 /// An invalid ordinal day number was provided.
78 ///
79 /// The ordinal day must be between `1` and `365` for a common year, or `1` and `366`
80 /// for a leap year.
81 ///
82 /// Returned by: [`ParsiDate::from_ordinal`](crate::date::ParsiDate::from_ordinal).
83 InvalidOrdinal,
84}
85
86/// Provides specific reasons for a parsing failure.
87///
88/// This enum is wrapped by [`DateError::ParseError`] to give detailed feedback when
89/// parsing a string into a date or date-time fails.
90#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
91pub enum ParseErrorKind {
92 /// The input string's structure or literal characters did not match the format string.
93 /// For example, expecting a `/` but finding a `-`, or the input string has trailing characters.
94 FormatMismatch,
95
96 /// A numeric component (e.g., `%Y`, `%m`, `%d`, `%H`) contained non-digit characters,
97 /// or did not have the required number of digits.
98 InvalidNumber,
99
100 /// The components were parsed successfully but form a logically invalid date.
101 /// For example, parsing `"1404/12/30"` with `"%Y/%m/%d"`, where 1404 is not a leap year.
102 InvalidDateValue,
103
104 /// The components were parsed successfully but form a logically invalid time.
105 /// For example, parsing `"25:10:00"` with `"%H:%M:%S"`.
106 InvalidTimeValue,
107
108 /// The format string contained an unrecognized or unsupported specifier for parsing.
109 /// For example, using `%A` (weekday name) or `%j` (ordinal day), which are for formatting only.
110 UnsupportedSpecifier,
111
112 /// A Persian month name required by the `%B` specifier was not found or recognized in the input.
113 InvalidMonthName,
114
115 /// Reserved for future use if weekday parsing is implemented. Currently not returned.
116 InvalidWeekdayName,
117}
118
119// --- Trait Implementations ---
120
121impl fmt::Display for DateError {
122 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123 match self {
124 DateError::InvalidDate => write!(
125 f,
126 "Invalid Persian date: year, month, or day is out of range or inconsistent"
127 ),
128 DateError::InvalidTime => {
129 write!(f, "Invalid time: hour, minute, or second is out of range")
130 }
131 DateError::GregorianConversionError => {
132 write!(f, "Failed to convert to or from the Gregorian calendar")
133 }
134 DateError::ParseError(kind) => write!(f, "Parsing error: {}", kind),
135 DateError::ArithmeticOverflow => write!(
136 f,
137 "Date arithmetic resulted in an overflow or a date outside the supported range"
138 ),
139 DateError::InvalidOrdinal => {
140 write!(f, "Invalid ordinal day: must be between 1 and 365/366")
141 }
142 }
143 }
144}
145
146impl fmt::Display for ParseErrorKind {
147 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148 match self {
149 ParseErrorKind::FormatMismatch => write!(f, "input string does not match the format string's structure"),
150 ParseErrorKind::InvalidNumber => write!(f, "a numeric component could not be parsed or had an incorrect number of digits"),
151 ParseErrorKind::InvalidDateValue => write!(f, "the parsed components form a logically invalid date (e.g., day 30 in Esfand of a common year)"),
152 ParseErrorKind::InvalidTimeValue => write!(f, "the parsed components form a logically invalid time (e.g., hour 24)"),
153 ParseErrorKind::UnsupportedSpecifier => write!(f, "the format string contains a specifier that is not supported for parsing"),
154 ParseErrorKind::InvalidMonthName => write!(f, "could not recognize a valid Persian month name for the '%B' specifier"),
155 ParseErrorKind::InvalidWeekdayName => write!(f, "could not recognize a valid Persian weekday name (currently unused)"),
156 }
157 }
158}
159
160/// Implements the standard `Error` trait for `DateError`.
161///
162/// This allows `DateError` to be used with standard Rust error handling mechanisms,
163/// such as the `?` operator and error-handling libraries like `anyhow` or `thiserror`.
164impl std::error::Error for DateError {
165 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
166 // This implementation does not wrap other errors, so `source` returns `None`.
167 // If, in the future, DateError were to wrap an error from `chrono` or another
168 // library, that underlying error could be returned here.
169 None
170 }
171}