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}