parsidate/
lib.rs

1//! # ParsiDate: Persian (Jalali) Calendar Implementation in Rust
2//!
3//! This crate provides comprehensive functionality for working with the Persian (Jalali) calendar system.
4//! It allows for:
5//!
6//! *   **Conversion:** Seamlessly convert dates between the Gregorian and Persian calendars.
7//! *   **Validation:** Check if a given year, month, and day combination forms a valid Persian date.
8//! *   **Formatting:** Display Persian dates in various common formats (e.g., "YYYY/MM/DD", "D MMMM YYYY", "YYYY-MM-DD").
9//! *   **Date Arithmetic:** Calculate the number of days between two Persian dates.
10//! *   **Leap Year Calculation:** Determine if a Persian or Gregorian year is a leap year.
11//! *   **Weekday Calculation:** Find the Persian name for the day of the week.
12//!
13//! It relies on the `chrono` crate for underlying Gregorian date representations and calculations.
14//!
15//! ## Examples
16//!
17//! ```rust
18//! use chrono::NaiveDate;
19//! use parsidate::{ParsiDate, DateError};
20//!
21//! // Convert Gregorian to Persian
22//! let gregorian_date = NaiveDate::from_ymd_opt(2024, 7, 23).unwrap();
23//! let persian_date = ParsiDate::from_gregorian(gregorian_date).unwrap();
24//! assert_eq!(persian_date.year, 1403);
25//! assert_eq!(persian_date.month, 5); // Mordad
26//! assert_eq!(persian_date.day, 2);
27//!
28//! // Convert Persian to Gregorian
29//! let p_date = ParsiDate { year: 1403, month: 1, day: 1 }; // Farvardin 1st, 1403
30//! let g_date = p_date.to_gregorian().unwrap();
31//! assert_eq!(g_date, NaiveDate::from_ymd_opt(2024, 3, 20).unwrap()); // March 20th, 2024 (Persian New Year)
32//!
33//! // Formatting
34//! assert_eq!(persian_date.format("short"), "1403/05/02");
35//! assert_eq!(persian_date.format("long"), "2 مرداد 1403");
36//! assert_eq!(persian_date.format("iso"), "1403-05-02");
37//!
38//! // Validation
39//! assert!(ParsiDate { year: 1403, month: 12, day: 30 }.is_valid()); // 1403 is a leap year
40//! assert!(!ParsiDate { year: 1404, month: 12, day: 30 }.is_valid());// 1404 is not a leap year
41//! assert!(!ParsiDate { year: 1403, month: 13, day: 1 }.is_valid()); // Invalid month
42//!
43//! // Weekday
44//! assert_eq!(persian_date.weekday(), "سه‌شنبه"); // Tuesday
45//!
46//! // Days Between
47//! let date1 = ParsiDate { year: 1403, month: 5, day: 2 };
48//! let date2 = ParsiDate { year: 1403, month: 5, day: 12 };
49//! assert_eq!(date1.days_between(&date2), 10);
50//! ```
51
52// Import necessary items from the chrono crate for Gregorian date handling.
53use chrono::{Datelike, NaiveDate, Weekday};
54
55/// Represents a date in the Persian (Jalali or Shamsi) calendar system.
56///
57/// This struct holds the year, month, and day components of a Persian date.
58/// It provides methods for conversion, validation, formatting, and basic date calculations.
59///
60/// # Fields
61///
62/// *   `year` - The Persian year (e.g., 1403). Represents the number of years passed since the Hijra (migration) of Prophet Muhammad, adjusted for the solar calendar.
63/// *   `month` - The Persian month number, ranging from 1 (Farvardin) to 12 (Esfand).
64/// *   `day` - The day of the month, ranging from 1 to 31 (depending on the month and whether the year is a leap year).
65#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)] // Added more useful derives
66pub struct ParsiDate {
67    /// The year component of the Persian date (e.g., 1403).
68    pub year: i32,
69    /// The month component of the Persian date (1 = Farvardin, ..., 12 = Esfand).
70    pub month: u32,
71    /// The day component of the Persian date (1-29/30/31).
72    pub day: u32,
73}
74
75/// Enumerates potential errors during date operations within the `parsidate` crate.
76///
77/// Currently, it only includes a variant for invalid date representations.
78#[derive(Debug, PartialEq, Eq, Clone, Copy)] // Added more useful derives
79pub enum DateError {
80    /// Indicates that a given set of year, month, and day does not form a valid date
81    /// in the Persian calendar (e.g., month 13, day 32, or day 30 in Esfand of a non-leap year)
82    /// or that a conversion resulted in an invalid date.
83    InvalidDate,
84}
85
86// Implementation block for the ParsiDate struct.
87impl ParsiDate {
88    /// Creates a new `ParsiDate` instance from year, month, and day components.
89    ///
90    /// This function validates the date upon creation.
91    ///
92    /// # Arguments
93    ///
94    /// * `year` - The Persian year.
95    /// * `month` - The Persian month (1-12).
96    /// * `day` - The Persian day (1-31).
97    ///
98    /// # Returns
99    ///
100    /// * `Ok(ParsiDate)` if the provided components form a valid Persian date.
101    /// * `Err(DateError::InvalidDate)` if the date is invalid.
102    ///
103    /// # Examples
104    ///
105    /// ```
106    /// use parsidate::{ParsiDate, DateError};
107    ///
108    /// let date = ParsiDate::new(1403, 5, 2).unwrap();
109    /// assert_eq!(date.year, 1403);
110    /// assert_eq!(date.month, 5);
111    /// assert_eq!(date.day, 2);
112    ///
113    /// let invalid_date_result = ParsiDate::new(1404, 12, 30);
114    /// assert_eq!(invalid_date_result, Err(DateError::InvalidDate)); // 1404 is not a leap year
115    /// ```
116    pub fn new(year: i32, month: u32, day: u32) -> Result<Self, DateError> {
117        let date = ParsiDate { year, month, day };
118        if date.is_valid() {
119            Ok(date)
120        } else {
121            Err(DateError::InvalidDate)
122        }
123    }
124
125    /// Converts a Gregorian date (`chrono::NaiveDate`) to its equivalent Persian (Jalali) date.
126    ///
127    /// This method implements a conversion algorithm based on comparing the input date
128    /// with the start of the corresponding Persian year (March 20th or 21st).
129    /// It accurately handles Gregorian leap years during the conversion.
130    ///
131    /// The core logic determines the Persian year first, then calculates the day difference
132    /// from the start of that Persian year (Nowruz) to find the month and day.
133    ///
134    /// # Arguments
135    ///
136    /// * `date` - A `chrono::NaiveDate` representing the Gregorian date to be converted.
137    ///
138    /// # Returns
139    ///
140    /// * `Ok(ParsiDate)` containing the equivalent Persian date if the conversion is successful.
141    /// * `Err(DateError::InvalidDate)` if the internal calculations lead to an invalid state,
142    ///   though this is unlikely with a valid `NaiveDate` input.
143    ///
144    /// # Examples
145    ///
146    /// ```
147    /// use chrono::NaiveDate;
148    /// use parsidate::{ParsiDate, DateError};
149    ///
150    /// // A date after Nowruz
151    /// let gregorian_date = NaiveDate::from_ymd_opt(2024, 7, 23).unwrap();
152    /// let persian_date = ParsiDate::from_gregorian(gregorian_date).unwrap();
153    /// assert_eq!(persian_date, ParsiDate { year: 1403, month: 5, day: 2 }); // 2 Mordad 1403
154    ///
155    /// // A date before Nowruz (falls in the previous Persian year)
156    /// let gregorian_date_early = NaiveDate::from_ymd_opt(2024, 3, 19).unwrap();
157    /// let persian_date_early = ParsiDate::from_gregorian(gregorian_date_early).unwrap();
158    /// assert_eq!(persian_date_early, ParsiDate { year: 1402, month: 12, day: 29 }); // 29 Esfand 1402
159    ///
160    /// // Nowruz itself (first day of the Persian year)
161    /// let nowruz_gregorian = NaiveDate::from_ymd_opt(2024, 3, 20).unwrap(); // 2024 is a leap year, Nowruz is Mar 20
162    /// let nowruz_persian = ParsiDate::from_gregorian(nowruz_gregorian).unwrap();
163    /// assert_eq!(nowruz_persian, ParsiDate { year: 1403, month: 1, day: 1 });
164    /// ```
165
166    pub fn from_gregorian(date: NaiveDate) -> Result<Self, DateError> {
167        // Define the Gregorian base date used as the reference point (epoch) for calculations.
168        // This corresponds roughly to the start of the Persian calendar count (Year 0 or 1).
169        // March 21, 621 AD is used here.
170        // Panic here is acceptable as 621-03-21 is a fixed, valid date.
171        let base_date = NaiveDate::from_ymd_opt(621, 3, 21).unwrap();
172
173        // Calculate the total number of days elapsed between the input Gregorian date and the base date.
174        let days_since_base = (date - base_date).num_days();
175
176        // Check if the input date is before the established base date.
177        // This algorithm does not support dates prior to March 21, 621 AD.
178        if days_since_base < 0 {
179            // Return an error indicating an invalid/unsupported date range.
180            return Err(DateError::InvalidDate);
181        }
182
183        // Initialize the Persian year counter. Starts assuming year 0 relative to the base date.
184        // `jy` will eventually hold the target Persian year number.
185        let mut jy = 0; // Represents the number of full Persian years passed since the base epoch.
186
187        // Initialize a mutable variable with the total days since the base date.
188        // This variable will be reduced as we account for full years and months.
189        // Cast to i32 for calculations involving subtraction.
190        let mut remaining_days = days_since_base as i32;
191
192        // --- Determine the Persian Year (jy) ---
193        // Loop while the number of remaining days is sufficient to constitute at least one full Persian year.
194        // This loop iteratively subtracts the days of each Persian year (starting from year 0)
195        // until `remaining_days` holds the number of days *into* the target Persian year `jy`.
196        while remaining_days
197            >= (if Self::is_persian_leap_year(jy) {
198                // Check if the current year `jy` is leap
199                366 // Days in a leap year
200            } else {
201                365 // Days in a common year
202            })
203        {
204            // Calculate the number of days in the current Persian year `jy`.
205            let year_days = if Self::is_persian_leap_year(jy) {
206                366
207            } else {
208                365
209            };
210            // Subtract the days of this full year from the remaining days.
211            remaining_days -= year_days;
212            // Increment the Persian year counter, moving to the next year.
213            jy += 1;
214        }
215        // At this point, `jy` holds the correct Persian year, and `remaining_days` holds
216        // the 0-indexed day number within that year (e.g., 0 for Farvardin 1st, 31 for Ordibehesht 1st).
217
218        // --- Determine the Persian Month (jm) and Day (jd) ---
219        // Check if the determined Persian year `jy` is a leap year to know Esfand's length.
220        let is_persian_leap = Self::is_persian_leap_year(jy);
221
222        // Define the lengths of the months for the determined Persian year `jy`.
223        // The last month (Esfand) has 30 days if it's a leap year, 29 otherwise.
224        let month_lengths = [
225            31,                                    // Farvardin
226            31,                                    // Ordibehesht
227            31,                                    // Khordad
228            31,                                    // Tir
229            31,                                    // Mordad
230            31,                                    // Shahrivar
231            30,                                    // Mehr
232            30,                                    // Aban
233            30,                                    // Azar
234            30,                                    // Dey
235            30,                                    // Bahman
236            if is_persian_leap { 30 } else { 29 }, // Esfand (length depends on leap status)
237        ];
238
239        // Initialize Persian month and day variables.
240        let mut jm = 0; // Target Persian month (1-12)
241        let mut jd = 0; // Target Persian day (1-31)
242
243        // Iterate through the months of the year `jy`.
244        // `i` is the 0-indexed month number (0=Farvardin, 1=Ordibehesht, ..., 11=Esfand).
245        // `length` is the number of days in that month.
246        for (i, &length) in month_lengths.iter().enumerate() {
247            // Check if the `remaining_days` (0-indexed day within the year) falls into the current month.
248            if remaining_days < length {
249                // If yes, we've found the month and day.
250                jm = i as u32 + 1; // Convert 0-indexed `i` to 1-indexed month number.
251                jd = remaining_days + 1; // Convert 0-indexed `remaining_days` to 1-indexed day number.
252                                         // Exit the loop as the correct month and day have been found.
253                break;
254            }
255            // If the day is not in this month, subtract the length of this month
256            // and continue to check the next month.
257            remaining_days -= length;
258        }
259
260        // --- Final Validation and Result ---
261        // If the loop completed without finding a month (jm is still 0), it implies an error
262        // in the calculation (e.g., `days_since_base` was inconsistent or led to an impossible state).
263        if jm == 0 {
264            // This should theoretically not happen if `days_since_base` was correctly calculated
265            // and the year/month loops worked as expected.
266            return Err(DateError::InvalidDate);
267        }
268
269        // Construct the `ParsiDate` result using the calculated year, month, and day.
270        // Cast the day `jd` (i32) back to u32 for the struct field.
271        let result = ParsiDate {
272            year: jy,
273            month: jm,
274            day: jd as u32,
275        };
276
277        // Perform a final validation check on the constructed date using the `is_valid` method.
278        // This acts as a safeguard against potential edge-case errors in the algorithm
279        // (e.g., if the calculations somehow resulted in day 30 for Esfand in a non-leap year).
280        if !result.is_valid() {
281            // If the generated date is somehow invalid by Persian calendar rules, return an error.
282            return Err(DateError::InvalidDate);
283        }
284
285        // Return the successfully converted and validated Persian date.
286        Ok(result)
287    }
288    /// Converts this Persian (Jalali) date to its equivalent Gregorian date (`chrono::NaiveDate`).
289    ///
290    /// This method calculates the number of days passed since a known reference point
291    /// (the start of the Persian calendar epoch, corresponding roughly to March 21, 622 AD in Gregorian,
292    /// although the calculation uses a relative approach) and adds these days to the Gregorian date
293    /// corresponding to the start of the Persian epoch (Farvardin 1, Year 1).
294    ///
295    /// It accurately accounts for Persian leap years when calculating the total number of days.
296    ///
297    /// # Returns
298    ///
299    /// * `Ok(NaiveDate)` containing the equivalent Gregorian date if the current `ParsiDate` is valid.
300    /// * `Err(DateError::InvalidDate)` if the current `ParsiDate` instance itself represents an invalid date
301    ///   (e.g., month 13 or day 30 in Esfand of a non-leap year).
302    ///
303    /// # Examples
304    ///
305    /// ```
306    /// use chrono::NaiveDate;
307    /// use parsidate::ParsiDate;
308    ///
309    /// // Convert a standard date
310    /// let persian_date = ParsiDate { year: 1403, month: 5, day: 2 }; // 2 Mordad 1403
311    /// let gregorian_date = persian_date.to_gregorian().unwrap();
312    /// assert_eq!(gregorian_date, NaiveDate::from_ymd_opt(2024, 7, 23).unwrap());
313    ///
314    /// // Convert Nowruz (start of the year)
315    /// let nowruz_persian = ParsiDate { year: 1403, month: 1, day: 1 };
316    /// let nowruz_gregorian = nowruz_persian.to_gregorian().unwrap();
317    /// assert_eq!(nowruz_gregorian, NaiveDate::from_ymd_opt(2024, 3, 20).unwrap()); // 2024 is Gregorian leap
318    ///
319    /// // Convert end of a leap year
320    /// let leap_end_persian = ParsiDate { year: 1403, month: 12, day: 30 }; // 1403 is leap
321    /// let leap_end_gregorian = leap_end_persian.to_gregorian().unwrap();
322    /// assert_eq!(leap_end_gregorian, NaiveDate::from_ymd_opt(2025, 3, 20).unwrap());
323    ///
324    /// // Attempt to convert an invalid date
325    /// let invalid_persian = ParsiDate { year: 1404, month: 12, day: 30 }; // 1404 not leap
326    /// assert!(invalid_persian.to_gregorian().is_err());
327    /// ```
328    pub fn to_gregorian(&self) -> Result<NaiveDate, DateError> {
329        // First, ensure the ParsiDate instance itself is valid.
330        if !self.is_valid() {
331            return Err(DateError::InvalidDate);
332        }
333
334        // --- Calculate days passed since the Persian epoch reference point ---
335
336        // Reference Gregorian date for Farvardin 1, Year 1 (Persian epoch start).
337        // This corresponds to March 22, 622 AD Julian, or March 19, 622 AD Proleptic Gregorian?
338        // Most algorithms use March 21, 622 AD as the practical reference start for day counting.
339        // Let's use the widely accepted reference: March 21, 622 (Gregorian) as day 1 of year 1.
340        // However, calculating relative to a known recent Nowruz is often simpler and less error-prone.
341
342        // Let's calculate days relative to Farvardin 1 of the *given* Persian year `self.year`.
343        // First, find the Gregorian date for Farvardin 1 of `self.year`.
344        // This is March 21st of Gregorian year `gy = self.year + 621`, adjusted to March 20th if `gy` is a leap year.
345
346        // Alternative: Calculate total days from Persian Year 1, Month 1, Day 1.
347        let mut total_days: i64 = 0;
348
349        // Add days for full years passed before the current year `self.year`.
350        // We iterate from year 1 up to (but not including) `self.year`.
351        for y in 1..self.year {
352            total_days += if Self::is_persian_leap_year(y) {
353                366
354            } else {
355                365
356            };
357        }
358
359        // Add days for full months passed in the current year `self.year` before the current month `self.month`.
360        for m in 1..self.month {
361            total_days += match m {
362                1..=6 => 31,  // Farvardin to Shahrivar have 31 days
363                7..=11 => 30, // Mehr to Bahman have 30 days
364                12 => {
365                    // Esfand depends on leap year
366                    // This case shouldn't be reached here as we loop up to `self.month`.
367                    // Included for completeness, but the logic focuses on months *before* `self.month`.
368                    // If self.month is 12, this loop runs up to 11.
369                    panic!("Logic error: Month 12 encountered in loop for previous months.");
370                }
371                _ => unreachable!("Invalid month {} encountered during day calculation", m), // Should be caught by is_valid
372            };
373        }
374        // Add days for the current month (adjusting because day is 1-based).
375        total_days += (self.day - 1) as i64;
376
377        // Define the Gregorian date corresponding to Farvardin 1, Year 1 (Persian).
378        // Based on common astronomical sources and algorithms (e.g., Dershowitz & Reingold),
379        // Persian date 1/1/1 corresponds to March 19, 622, in the Proleptic Gregorian calendar.
380        // Let's test this reference point.
381        // If ParsiDate {1, 1, 1}, total_days = 0.
382        // Gregorian base date needs to be March 19, 622.
383        // Note: Chrono's NaiveDate might have limitations for years that far back.
384        // Let's use a more modern reference if possible or stick to relative calculations.
385
386        // Using the common reference start day calculation (e.g., from Calendar FAQ by Claus Tøndering)
387        // Day number `N` from March 21, 622 AD (Gregorian).
388        // Gregorian year `gy = self.year + 621`.
389        // Find Gregorian date of Nowruz (Farvardin 1) for `self.year`.
390        let gy_start = self.year + 621;
391        // Nowruz is Mar 21 unless gy_start+1 is a leap year, then it's Mar 20? No, check gy_start.
392        // Let's use the reference: March 21, 622 AD is Parsi 1/1/1 approximately.
393        // March 21, 622 was chosen as a practical starting point.
394        // Let's use NaiveDate::from_ymd(622, 3, 21) as the Gregorian equivalent of day 1 of year 1.
395        // Use unwrap: Assuming 622-03-21 is a valid date.
396        let persian_epoch_gregorian_start = NaiveDate::from_ymd_opt(622, 3, 21).unwrap();
397
398        // Add the calculated total_days to the Gregorian epoch start date.
399        // Note: The day added should be the difference from Parsi 1/1/1.
400        // Our current `total_days` counts days *since* 1/1/1 (0 for 1/1/1, 1 for 1/1/2, etc.)
401        match persian_epoch_gregorian_start.checked_add_days(chrono::Days::new(total_days as u64)) {
402            Some(date) => Ok(date),
403            None => Err(DateError::InvalidDate), // Indicates overflow or invalid calculation
404        }
405
406        /* // Previous calculation based on adding days to a calculated Nowruz:
407         // Seems more complex than the epoch-based approach if the epoch date is reliable.
408
409        // Calculate the Gregorian year corresponding to the start of this Persian year.
410        let gregorian_equiv_year = self.year + 621;
411
412        // Determine the exact Gregorian date of Nowruz (Farvardin 1) for `self.year`.
413        // This depends on Gregorian leap years affecting the vernal equinox timing.
414        // A common approximation: March 21st, unless the *next* Gregorian year is leap, then March 20th.
415        // Let's test: 1 Farvardin 1399 -> March 20, 2020 (because 2020 was leap)
416        // 1 Farvardin 1400 -> March 21, 2021 (because 2021 was not leap)
417        // 1 Farvardin 1403 -> March 20, 2024 (because 2024 was leap)
418        // Rule seems to be: If Gregorian year `gregorian_equiv_year` is leap, Nowruz is March 20, else March 21.
419        let nowruz_day = if Self::is_gregorian_leap_year(gregorian_equiv_year) { 20 } else { 21 };
420        let nowruz_gregorian = NaiveDate::from_ymd_opt(gregorian_equiv_year, 3, nowruz_day).unwrap();
421
422        // Calculate the number of days passed *within* the current Persian year `self.year` until the given date.
423        let mut days_into_persian_year: u32 = 0;
424        for m in 1..self.month {
425            days_into_persian_year += match m {
426                1..=6 => 31,
427                7..=11 => 30,
428                12 => if Self::is_persian_leap_year(self.year) { 30 } else { 29 },
429                _ => 0, // Invalid month, should be caught by is_valid
430            };
431        }
432        days_into_persian_year += self.day - 1; // Add days of the current month (0-indexed)
433
434        // Add these days to the Gregorian date of Nowruz.
435        Ok(nowruz_gregorian + chrono::Duration::days(days_into_persian_year as i64))
436        */
437    }
438
439    /// Checks if the current `ParsiDate` instance represents a valid date within the Persian calendar rules.
440    ///
441    /// Validation criteria:
442    /// *   Month must be between 1 and 12 (inclusive).
443    /// *   Day must be between 1 and 31 (inclusive).
444    /// *   Day must not exceed the number of days in the given month:
445    ///     *   Months 1-6 (Farvardin to Shahrivar) have 31 days.
446    ///     *   Months 7-11 (Mehr to Bahman) have 30 days.
447    ///     *   Month 12 (Esfand) has 30 days in a leap year, 29 otherwise.
448    ///
449    /// # Returns
450    ///
451    /// * `true` if the date (year, month, day combination) is valid.
452    /// * `false` otherwise.
453    ///
454    /// # Examples
455    ///
456    /// ```
457    /// use parsidate::ParsiDate;
458    ///
459    /// // Valid date
460    /// assert!(ParsiDate { year: 1403, month: 1, day: 31 }.is_valid());
461    /// // Valid date in leap year Esfand
462    /// assert!(ParsiDate { year: 1403, month: 12, day: 30 }.is_valid()); // 1403 is leap
463    /// // Valid date in non-leap year Esfand
464    /// assert!(ParsiDate { year: 1404, month: 12, day: 29 }.is_valid()); // 1404 is not leap
465    ///
466    /// // Invalid month
467    /// assert!(!ParsiDate { year: 1403, month: 0, day: 1 }.is_valid());
468    /// assert!(!ParsiDate { year: 1403, month: 13, day: 1 }.is_valid());
469    /// // Invalid day (too low)
470    /// assert!(!ParsiDate { year: 1403, month: 1, day: 0 }.is_valid());
471    /// // Invalid day (too high for month)
472    /// assert!(!ParsiDate { year: 1403, month: 2, day: 32 }.is_valid()); // Ordibehesht has 31 days
473    /// assert!(!ParsiDate { year: 1403, month: 7, day: 31 }.is_valid()); // Mehr has 30 days
474    /// // Invalid day (too high for Esfand in non-leap year)
475    /// assert!(!ParsiDate { year: 1404, month: 12, day: 30 }.is_valid()); // 1404 is not leap
476    /// ```
477    pub fn is_valid(&self) -> bool {
478        // Check if month is within the valid range (1 to 12).
479        if self.month == 0 || self.month > 12 {
480            return false;
481        }
482        // Check if day is positive.
483        if self.day == 0 {
484            return false;
485        }
486
487        // Determine the maximum allowed days for the given month.
488        let max_days = match self.month {
489            // Months 1 through 6 (Farvardin to Shahrivar) have 31 days.
490            1..=6 => 31,
491            // Months 7 through 11 (Mehr to Bahman) have 30 days.
492            7..=11 => 30,
493            // Month 12 (Esfand) has 30 days in a Persian leap year, 29 otherwise.
494            12 => {
495                if Self::is_persian_leap_year(self.year) {
496                    30
497                } else {
498                    29
499                }
500            }
501            // This case should be unreachable due to the initial month check, but included for exhaustive matching.
502            _ => return false,
503        };
504
505        // Check if the day is within the valid range for the determined month length.
506        self.day <= max_days
507    }
508
509    /// Determines if a given Persian year is a leap year.
510    ///
511    /// The Persian calendar uses an observational VERNAL EQUINOX rule for leap years, which is very accurate.
512    /// A common algorithmic approximation involves a 33-year cycle, attributed to Omar Khayyam or later astronomers.
513    /// This implementation uses the widely accepted 33-year cycle approximation where specific years within the cycle are designated as leap years.
514    ///
515    /// The leap years within a 33-year cycle (starting from a reference year) typically fall on years:
516    /// 1, 5, 9, 13, 17, 21, 25, **29** or **30**. (Most sources use 30, some older ones might use 29).
517    /// This implementation uses the pattern with 30.
518    /// Year `y` is leap if `(y * 8 + 21) % 33 < 8`. This is equivalent to checking `y % 33` against the set {1, 5, 9, 13, 17, 21, 25, 30}.
519    ///
520    /// # Arguments
521    ///
522    /// * `year` - The Persian year to check (e.g., 1403).
523    ///
524    /// # Returns
525    ///
526    /// * `true` if the given Persian year is a leap year according to the 33-year cycle approximation.
527    /// * `false` otherwise.
528    ///
529    /// # Examples
530    ///
531    /// ```
532    /// use parsidate::ParsiDate;
533    ///
534    /// assert!(ParsiDate::is_persian_leap_year(1399)); // 1399 % 33 = 5 -> Leap
535    /// assert!(ParsiDate::is_persian_leap_year(1403)); // 1403 % 33 = 9 -> Leap
536    /// assert!(!ParsiDate::is_persian_leap_year(1400)); // 1400 % 33 = 6 -> Not Leap
537    /// assert!(!ParsiDate::is_persian_leap_year(1401)); // 1401 % 33 = 7 -> Not Leap
538    /// assert!(!ParsiDate::is_persian_leap_year(1402)); // 1402 % 33 = 8 -> Not Leap
539    /// assert!(ParsiDate::is_persian_leap_year(1408)); // 1408 % 33 = 12 -> Leap
540    /// assert!(ParsiDate::is_persian_leap_year(1424)); // 1423 % 33 = 28 -> Leap
541    /// ```
542    pub fn is_persian_leap_year(year: i32) -> bool {
543        // Ensure year is positive, though the cycle works for negative years mathematically.
544        // Assume non-positive years are not meaningful in this calendar context or handle as needed.
545        if year <= 0 {
546            return false;
547        } // Or adjust based on desired behavior for year 0 or BC dates.
548
549        // Calculate the position within the 33-year cycle.
550        // The cycle reference point can affect the result if not aligned, but modulo 33 gives the pattern.
551        let cycle_position = year % 33;
552
553        // Check if the cycle position matches one of the designated leap year positions in the 33-year pattern.
554        // The pattern is {1, 5, 9, 13, 17, 21, 25, 30}.
555        matches!(cycle_position, 1 | 5 | 9 | 13 | 17 | 22 | 26 | 30)
556    }
557
558    /// Determines if a given Gregorian year is a leap year.
559    ///
560    /// Implements the standard Gregorian calendar leap year rules:
561    /// 1.  A year is a leap year if it is divisible by 4.
562    /// 2.  However, if the year is divisible by 100, it is *not* a leap year...
563    /// 3.  Unless the year is also divisible by 400, in which case it *is* a leap year.
564    ///
565    /// # Arguments
566    ///
567    /// * `year` - The Gregorian year to check (e.g., 2024).
568    ///
569    /// # Returns
570    ///
571    /// * `true` if the given Gregorian year is a leap year.
572    /// * `false` otherwise.
573    ///
574    /// # Examples
575    ///
576    /// ```
577    /// use parsidate::ParsiDate;
578    ///
579    /// assert!(ParsiDate::is_gregorian_leap_year(2020)); // Divisible by 4, not by 100
580    /// assert!(ParsiDate::is_gregorian_leap_year(2024)); // Divisible by 4, not by 100
581    /// assert!(!ParsiDate::is_gregorian_leap_year(2021)); // Not divisible by 4
582    /// assert!(!ParsiDate::is_gregorian_leap_year(1900)); // Divisible by 100, but not by 400
583    /// assert!(ParsiDate::is_gregorian_leap_year(2000)); // Divisible by 400
584    /// ```
585    pub fn is_gregorian_leap_year(year: i32) -> bool {
586        // Check divisibility by 4, 100, and 400 according to the rules.
587        (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
588    }
589
590    /// Formats the `ParsiDate` into a string based on the specified style.
591    ///
592    /// Supported styles:
593    /// *   `"short"` (Default): Formats as "YYYY/MM/DD" (e.g., "1403/05/02"). Uses leading zeros for month and day.
594    /// *   `"long"`: Formats as "D MMMM YYYY" using Persian month names (e.g., "2 مرداد 1403").
595    /// *   `"iso"`: Formats according to ISO 8601 style for dates: "YYYY-MM-DD" (e.g., "1403-05-02"). Uses leading zeros.
596    /// *   Any other string: Currently defaults to the `"short"` format.
597    ///
598    /// # Arguments
599    ///
600    /// * `style` - A string slice (`&str`) specifying the desired format ("short", "long", or "iso"). Case-sensitive.
601    ///
602    /// # Returns
603    ///
604    /// * A `String` containing the formatted date. Returns the "short" format if the style is unrecognized.
605    ///
606    /// # Panics
607    ///
608    /// *   Panics if `self.month` is outside the valid range 1-12, which shouldn't happen if the `ParsiDate` was created via `new` or `from_gregorian` or validated beforehand.
609    ///
610    /// # Examples
611    ///
612    /// ```
613    /// use parsidate::ParsiDate;
614    ///
615    /// let date = ParsiDate { year: 1403, month: 5, day: 2 }; // 2 Mordad 1403
616    ///
617    /// assert_eq!(date.format("short"), "1403/05/02");
618    /// assert_eq!(date.format("long"), "2 مرداد 1403");
619    /// assert_eq!(date.format("iso"), "1403-05-02");
620    ///
621    /// // Default format (same as "short")
622    /// assert_eq!(date.to_string(), "1403/05/02");
623    ///
624    /// // Unrecognized style defaults to "short"
625    /// assert_eq!(date.format("medium"), "1403/05/02");
626    /// ```
627    pub fn format(&self, style: &str) -> String {
628        // Array of Persian month names, indexed 0 to 11.
629        let month_names = [
630            "فروردین",  // 1
631            "اردیبهشت", // 2
632            "خرداد",    // 3
633            "تیر",      // 4
634            "مرداد",    // 5
635            "شهریور",   // 6
636            "مهر",      // 7
637            "آبان",     // 8
638            "آذر",      // 9
639            "دی",       // 10
640            "بهمن",     // 11
641            "اسفند",    // 12
642        ];
643
644        match style {
645            // Long format: Day MonthName Year (e.g., "7 فروردین 1404")
646            "long" => format!(
647                "{} {} {}",
648                self.day,
649                // Access month name using month number (1-based) converted to 0-based index.
650                // Panic potential if self.month is invalid (e.g., 0 or > 12).
651                month_names[(self.month - 1) as usize],
652                self.year
653            ),
654            // ISO 8601 format: YYYY-MM-DD (e.g., "1404-01-07")
655            "iso" => format!("{}-{:02}-{:02}", self.year, self.month, self.day),
656            // Short format (also default): YYYY/MM/DD (e.g., "1404/01/07")
657            "short" | _ => self.to_string(), // Use the Display impl (via to_string method) for short/default
658        }
659    }
660
661    /// Returns the Persian name of the weekday for this date.
662    ///
663    /// This method first converts the `ParsiDate` to its Gregorian equivalent using `to_gregorian`,
664    /// then uses `chrono`'s `weekday()` method to find the day of the week (Monday=0 to Sunday=6),
665    /// and finally maps this to the corresponding Persian name (شنبه to جمعه).
666    ///
667    /// # Returns
668    ///
669    /// * A `String` containing the Persian name of the weekday (e.g., "شنبه", "یکشنبه", ... "جمعه").
670    ///
671    /// # Panics
672    ///
673    /// *   Panics if the internal call to `to_gregorian()` fails (e.g., if the `ParsiDate` is invalid). Ensure the date is valid before calling.
674    /// *   Panics if the weekday number from `chrono` is outside the expected 0-6 range, which should not happen.
675    ///
676    /// # Examples
677    ///
678    /// ```
679    /// use parsidate::ParsiDate;
680    ///
681    /// // 7 Farvardin 1404 corresponds to March 27, 2025, which is a Thursday.
682    /// let date1 = ParsiDate { year: 1404, month: 1, day: 7 };
683    /// assert_eq!(date1.weekday(), "پنجشنبه"); // Thursday
684    ///
685    /// // 1 Farvardin 1403 corresponds to March 20, 2024, which is a Wednesday.
686    /// let date2 = ParsiDate { year: 1403, month: 1, day: 1 };
687    /// assert_eq!(date2.weekday(), "چهارشنبه"); // Wednesday
688    ///
689    /// // 29 Esfand 1402 corresponds to March 19, 2024, which is a Tuesday.
690    /// let date3 = ParsiDate { year: 1402, month: 12, day: 29 };
691    /// assert_eq!(date3.weekday(), "سه‌شنبه"); // Tuesday
692    /// ```
693    pub fn weekday(&self) -> String {
694        // Convert the Persian date to Gregorian. Panics if self is invalid.
695        let gregorian_date = self
696            .to_gregorian()
697            .expect("Cannot get weekday of an invalid ParsiDate");
698
699        // Get the weekday from the Gregorian date.
700        // chrono::Weekday: Mon=0, Tue=1, ..., Sun=6
701        let day_num = gregorian_date.weekday().num_days_from_monday(); // Using Monday as 0 for consistency if needed
702
703        // Alternative: Get Sunday as 0.
704        // let day_num_sun0 = gregorian_date.weekday().num_days_from_sunday();
705
706        // Persian weekdays start with Shanbeh (Saturday) corresponding to Gregorian Saturday.
707        // Mapping from chrono::Weekday (Mon=0..Sun=6) to Persian Names (Shanbeh..Jomeh)
708        // Gregorian: Mon(0), Tue(1), Wed(2), Thu(3), Fri(4), Sat(5), Sun(6)
709        // Persian:   Doshanbeh, Seshanbeh, Chaharshanbeh, Panjshanbeh, Jomeh, Shanbeh, Yekshanbeh
710        // Need a mapping:
711        // Sat (5) -> Shanbeh (شنبه) - Index 0
712        // Sun (6) -> Yekshanbeh (یکشنبه) - Index 1
713        // Mon (0) -> Doshanbeh (دوشنبه) - Index 2
714        // Tue (1) -> Seshanbeh (سه‌شنبه) - Index 3
715        // Wed (2) -> Chaharshanbeh (چهارشنبه) - Index 4
716        // Thu (3) -> Panjshanbeh (پنجشنبه) - Index 5
717        // Fri (4) -> Jomeh (جمعه) - Index 6
718
719        // Array of Persian weekday names, ordered Saturday to Friday.
720        let persian_day_names = [
721            "شنبه",     // Saturday
722            "یکشنبه",   // Sunday
723            "دوشنبه",   // Monday
724            "سه‌شنبه",   // Tuesday
725            "چهارشنبه", // Wednesday
726            "پنجشنبه",  // Thursday
727            "جمعه",     // Friday
728        ];
729
730        // Calculate the index into persian_day_names based on chrono's weekday.
731        // chrono Sun=6 -> persian index 1
732        // chrono Mon=0 -> persian index 2
733        // chrono Tue=1 -> persian index 3
734        // ...
735        // chrono Sat=5 -> persian index 0
736        let persian_index = (day_num + 2) % 7; // Map Mon(0)..Sun(6) to Sat(0)..Fri(6) effectively
737
738        // Let's re-verify the mapping with num_days_from_sunday() (Sun=0..Sat=6)
739        // Gregorian: Sun(0), Mon(1), Tue(2), Wed(3), Thu(4), Fri(5), Sat(6)
740        // Persian:   Yekshanbeh, Doshanbeh, Seshanbeh, Chaharshanbeh, Panjshanbeh, Jomeh, Shanbeh
741        // Need mapping:
742        // Sun(0) -> Yekshanbeh (یکشنبه) - Index 1
743        // Mon(1) -> Doshanbeh (دوشنبه) - Index 2
744        // Tue(2) -> Seshanbeh (سه‌شنبه) - Index 3
745        // Wed(3) -> Chaharshanbeh (چهارشنبه) - Index 4
746        // Thu(4) -> Panjshanbeh (پنجشنبه) - Index 5
747        // Fri(5) -> Jomeh (جمعه) - Index 6
748        // Sat(6) -> Shanbeh (شنبه) - Index 0
749
750        let day_num_sun0 = gregorian_date.weekday().num_days_from_sunday();
751        // Mapping Sun(0)..Sat(6) to persian_day_names index (Sat=0 .. Fri=6)
752        let persian_index_from_sun0 = (day_num_sun0 + 1) % 7; // Check this map
753                                                              // Sun(0) -> (0+1)%7 = 1 -> Yekshanbeh (Correct)
754                                                              // Mon(1) -> (1+1)%7 = 2 -> Doshanbeh (Correct)
755                                                              // ...
756                                                              // Fri(5) -> (5+1)%7 = 6 -> Jomeh (Correct)
757                                                              // Sat(6) -> (6+1)%7 = 0 -> Shanbeh (Correct)
758                                                              // This mapping seems correct.
759
760        persian_day_names[persian_index_from_sun0 as usize].to_string()
761    }
762
763    /// Calculates the absolute difference in days between this `ParsiDate` and another `ParsiDate`.
764    ///
765    /// This method converts both Persian dates to their Gregorian equivalents and then calculates
766    /// the duration between them using `chrono`'s capabilities. The result is always non-negative.
767    ///
768    /// # Arguments
769    ///
770    /// * `other` - A reference to another `ParsiDate` instance to compare against.
771    ///
772    /// # Returns
773    ///
774    /// * An `i64` representing the absolute number of days between the two dates.
775    ///
776    /// # Panics
777    ///
778    /// *   Panics if either `self` or `other` represents an invalid Persian date, as `to_gregorian()` would panic.
779    ///
780    /// # Examples
781    ///
782    /// ```
783    /// use parsidate::ParsiDate;
784    ///
785    /// let date1 = ParsiDate { year: 1404, month: 1, day: 7 };
786    /// let date2 = ParsiDate { year: 1404, month: 1, day: 10 };
787    /// let date3 = ParsiDate { year: 1403, month: 12, day: 25 }; // Earlier date
788    ///
789    /// // Difference within the same month
790    /// assert_eq!(date1.days_between(&date2), 3);
791    /// assert_eq!(date2.days_between(&date1), 3); // Order doesn't matter
792    ///
793    /// // Difference across years
794    /// // 1404/01/07 is March 27, 2025
795    /// // 1403/12/25 is March 15, 2025 (1403 is leap)
796    /// assert_eq!(date1.days_between(&date3), 12);
797    /// ```
798    pub fn days_between(&self, other: &ParsiDate) -> i64 {
799        // Convert both dates to Gregorian. Panics if either is invalid.
800        let g1 = self
801            .to_gregorian()
802            .expect("Cannot calculate days_between with invalid 'self' date");
803        let g2 = other
804            .to_gregorian()
805            .expect("Cannot calculate days_between with invalid 'other' date");
806
807        // Calculate the signed duration and return the absolute number of days.
808        (g1 - g2).num_days().abs()
809    }
810
811    /// Returns the string representation of the date in the default "short" format: "YYYY/MM/DD".
812    ///
813    /// This method provides the standard string conversion, often used by the `Display` trait (if implemented).
814    /// It ensures that the month and day are zero-padded to two digits.
815    ///
816    /// # Returns
817    ///
818    /// * A `String` formatted as "YYYY/MM/DD".
819    ///
820    /// # Examples
821    ///
822    /// ```
823    /// use parsidate::ParsiDate;
824    ///
825    /// let date = ParsiDate { year: 1403, month: 5, day: 2 };
826    /// assert_eq!(date.to_string(), "1403/05/02");
827    ///
828    /// let date_early = ParsiDate { year: 1399, month: 11, day: 22 };
829    /// assert_eq!(date_early.to_string(), "1399/11/22");
830    /// ```
831    // Note: If `impl std::fmt::Display for ParsiDate` is added, this method might become redundant
832    // or the Display implementation could call this.
833    pub fn to_string(&self) -> String {
834        // Format the year, month (zero-padded), and day (zero-padded) separated by slashes.
835        format!("{}/{:02}/{:02}", self.year, self.month, self.day)
836    }
837}
838
839// Implement the Display trait for easy printing using the default format.
840impl std::fmt::Display for ParsiDate {
841    /// Formats the `ParsiDate` using the default "short" style ("YYYY/MM/DD").
842    ///
843    /// This allows `ParsiDate` instances to be easily printed or converted to strings
844    /// using standard formatting macros like `println!` or `format!`.
845    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
846        // Use the dedicated to_string method which implements the short format.
847        write!(f, "{}", self.to_string())
848    }
849}
850
851// --- Unit Tests ---
852#[cfg(test)]
853mod tests {
854    // Import necessary items from the parent module (the code being tested) and chrono.
855    use super::*;
856    use chrono::NaiveDate;
857
858    // --- Conversion Tests ---
859
860    #[test]
861    /// Tests converting a typical Gregorian date (after Nowruz) to Persian.
862    fn test_gregorian_to_persian_standard() {
863        let gregorian = NaiveDate::from_ymd_opt(2024, 7, 23).unwrap(); // July 23, 2024
864        let expected_persian = ParsiDate {
865            year: 1403,
866            month: 5,
867            day: 2,
868        }; // 2 Mordad 1403
869        let calculated_persian = ParsiDate::from_gregorian(gregorian).unwrap();
870        assert_eq!(calculated_persian, expected_persian);
871    }
872
873    #[test]
874    /// Tests converting a Gregorian date that falls before Nowruz (should be in the previous Persian year).
875    fn test_gregorian_to_persian_before_nowruz() {
876        let gregorian = NaiveDate::from_ymd_opt(2024, 3, 19).unwrap(); // March 19, 2024
877        let expected_persian = ParsiDate {
878            year: 1402,
879            month: 12,
880            day: 29,
881        }; // 29 Esfand 1402 (1402 not leap)
882        let calculated_persian = ParsiDate::from_gregorian(gregorian).unwrap();
883        assert_eq!(calculated_persian, expected_persian);
884    }
885
886    #[test]
887    /// Tests converting the Gregorian date of Nowruz itself (start of Persian year).
888    /// Note: Nowruz date in Gregorian can be March 20 or 21. 2024 Gregorian is leap year.
889    fn test_gregorian_to_persian_nowruz_leap_gregorian() {
890        let gregorian = NaiveDate::from_ymd_opt(2024, 3, 20).unwrap(); // March 20, 2024 is Nowruz
891        let expected_persian = ParsiDate {
892            year: 1403,
893            month: 1,
894            day: 1,
895        }; // 1 Farvardin 1403
896        let calculated_persian = ParsiDate::from_gregorian(gregorian).unwrap();
897        assert_eq!(calculated_persian, expected_persian);
898    }
899
900    #[test]
901    /// Tests converting the Gregorian date of Nowruz itself (start of Persian year).
902    /// Note: Nowruz date in Gregorian can be March 20 or 21. 2025 Gregorian is not leap year.
903    fn test_gregorian_to_persian_nowruz_non_leap_gregorian() {
904        let gregorian = NaiveDate::from_ymd_opt(2025, 3, 21).unwrap(); // March 21, 2025 is Nowruz
905        let expected_persian = ParsiDate {
906            year: 1404,
907            month: 1,
908            day: 1,
909        }; // 1 Farvardin 1404
910        let calculated_persian = ParsiDate::from_gregorian(gregorian).unwrap();
911        assert_eq!(calculated_persian, expected_persian);
912    }
913
914    #[test]
915    /// Tests converting a standard Persian date back to Gregorian.
916    fn test_persian_to_gregorian_standard() {
917        let persian = ParsiDate {
918            year: 1403,
919            month: 5,
920            day: 2,
921        }; // 2 Mordad 1403
922        let expected_gregorian = NaiveDate::from_ymd_opt(2024, 7, 23).unwrap(); // July 23, 2024
923        let calculated_gregorian = persian.to_gregorian().unwrap();
924        assert_eq!(calculated_gregorian, expected_gregorian);
925    }
926
927    #[test]
928    /// Tests converting the start of a Persian year (Nowruz) to Gregorian.
929    fn test_persian_to_gregorian_nowruz() {
930        let persian = ParsiDate {
931            year: 1403,
932            month: 1,
933            day: 1,
934        }; // 1 Farvardin 1403
935        let expected_gregorian = NaiveDate::from_ymd_opt(2024, 3, 20).unwrap(); // March 20, 2024
936        let calculated_gregorian = persian.to_gregorian().unwrap();
937        assert_eq!(calculated_gregorian, expected_gregorian);
938    }
939
940    #[test]
941    /// Tests converting the end of a Persian leap year (Esfand 30th) to Gregorian.
942    fn test_persian_to_gregorian_leap_year_end() {
943        assert!(ParsiDate::is_persian_leap_year(1403)); // Ensure 1403 is leap
944        let persian = ParsiDate {
945            year: 1403,
946            month: 12,
947            day: 30,
948        }; // 30 Esfand 1403
949           // This should be the day before Nowruz 1404, which is March 21, 2025.
950        let expected_gregorian = NaiveDate::from_ymd_opt(2025, 3, 20).unwrap();
951        let calculated_gregorian = persian.to_gregorian().unwrap();
952        assert_eq!(calculated_gregorian, expected_gregorian);
953    }
954
955    #[test]
956    /// Tests converting the end of a Persian non-leap year (Esfand 29th) to Gregorian.
957    fn test_persian_to_gregorian_non_leap_year_end() {
958        assert!(!ParsiDate::is_persian_leap_year(1402)); // Ensure 1402 is not leap
959        let persian = ParsiDate {
960            year: 1402,
961            month: 12,
962            day: 29,
963        }; // 29 Esfand 1402
964           // This should be the day before Nowruz 1403, which is March 20, 2024.
965        let expected_gregorian = NaiveDate::from_ymd_opt(2024, 3, 19).unwrap();
966        let calculated_gregorian = persian.to_gregorian().unwrap();
967        assert_eq!(calculated_gregorian, expected_gregorian);
968    }
969
970    #[test]
971    /// Tests converting a date from a more distant past year.
972    fn test_persian_to_gregorian_past_year() {
973        let persian = ParsiDate {
974            year: 1357,
975            month: 11,
976            day: 22,
977        }; // 22 Bahman 1357 (Iranian Revolution)
978        let expected_gregorian = NaiveDate::from_ymd_opt(1979, 2, 11).unwrap(); // February 11, 1979
979        let calculated_gregorian = persian.to_gregorian().unwrap();
980        assert_eq!(calculated_gregorian, expected_gregorian);
981    }
982
983    #[test]
984    /// Tests converting a date from a future year.
985    fn test_persian_to_gregorian_future_year() {
986        let persian = ParsiDate {
987            year: 1410,
988            month: 6,
989            day: 15,
990        }; // 15 Shahrivar 1410
991        let expected_gregorian = NaiveDate::from_ymd_opt(2031, 9, 6).unwrap(); // September 6, 2031
992        let calculated_gregorian = persian.to_gregorian().unwrap();
993        assert_eq!(calculated_gregorian, expected_gregorian);
994    }
995
996    // --- Validation Tests ---
997
998    #[test]
999    /// Tests the `is_valid` method with various valid dates.
1000    fn test_is_valid_true_cases() {
1001        // Standard date
1002        assert!(ParsiDate {
1003            year: 1403,
1004            month: 5,
1005            day: 2
1006        }
1007        .is_valid());
1008        // Start of year
1009        assert!(ParsiDate {
1010            year: 1403,
1011            month: 1,
1012            day: 1
1013        }
1014        .is_valid());
1015        // End of 31-day month
1016        assert!(ParsiDate {
1017            year: 1403,
1018            month: 6,
1019            day: 31
1020        }
1021        .is_valid());
1022        // End of 30-day month
1023        assert!(ParsiDate {
1024            year: 1403,
1025            month: 7,
1026            day: 30
1027        }
1028        .is_valid());
1029        // End of Esfand in leap year
1030        assert!(ParsiDate {
1031            year: 1403,
1032            month: 12,
1033            day: 30
1034        }
1035        .is_valid()); // 1403 is leap
1036                      // End of Esfand in non-leap year
1037        assert!(ParsiDate {
1038            year: 1404,
1039            month: 12,
1040            day: 29
1041        }
1042        .is_valid()); // 1404 is not leap
1043    }
1044
1045    #[test]
1046    /// Tests the `is_valid` method with various invalid dates.
1047    fn test_is_valid_false_cases() {
1048        // Invalid month (0)
1049        assert!(!ParsiDate {
1050            year: 1403,
1051            month: 0,
1052            day: 1
1053        }
1054        .is_valid());
1055        // Invalid month (13)
1056        assert!(!ParsiDate {
1057            year: 1403,
1058            month: 13,
1059            day: 1
1060        }
1061        .is_valid());
1062        // Invalid day (0)
1063        assert!(!ParsiDate {
1064            year: 1403,
1065            month: 1,
1066            day: 0
1067        }
1068        .is_valid());
1069        // Invalid day (32 in 31-day month)
1070        assert!(!ParsiDate {
1071            year: 1403,
1072            month: 1,
1073            day: 32
1074        }
1075        .is_valid());
1076        // Invalid day (31 in 30-day month)
1077        assert!(!ParsiDate {
1078            year: 1403,
1079            month: 7,
1080            day: 31
1081        }
1082        .is_valid());
1083        // Invalid day (30 in Esfand, non-leap year)
1084        assert!(!ParsiDate {
1085            year: 1404,
1086            month: 12,
1087            day: 30
1088        }
1089        .is_valid()); // 1404 not leap
1090                      // Invalid day (31 in Esfand, leap year)
1091        assert!(!ParsiDate {
1092            year: 1403,
1093            month: 12,
1094            day: 31
1095        }
1096        .is_valid()); // 1403 is leap, but Esfand only has 30 days max
1097    }
1098
1099    #[test]
1100    /// Tests that `to_gregorian` returns an error for an invalid ParsiDate.
1101    fn test_to_gregorian_invalid_input() {
1102        let invalid_persian = ParsiDate {
1103            year: 1404,
1104            month: 12,
1105            day: 30,
1106        }; // Invalid day
1107        assert!(!invalid_persian.is_valid()); // Double check validity check
1108        let result = invalid_persian.to_gregorian();
1109        assert!(result.is_err());
1110        assert_eq!(result.err().unwrap(), DateError::InvalidDate);
1111    }
1112
1113    #[test]
1114    /// Tests that the `new` constructor validates input.
1115    fn test_new_constructor_validation() {
1116        // Valid case
1117        assert!(ParsiDate::new(1403, 5, 2).is_ok());
1118        // Invalid case
1119        let result = ParsiDate::new(1404, 12, 30); // 1404 not leap
1120        assert!(result.is_err());
1121        assert_eq!(result.err().unwrap(), DateError::InvalidDate);
1122    }
1123
1124    // --- Leap Year Tests ---
1125
1126    #[test]
1127    /// Tests the Persian leap year calculation for known leap years.
1128    fn test_is_persian_leap_year_true() {
1129        assert!(
1130            ParsiDate::is_persian_leap_year(1399),
1131            "Year 1399 should be leap (cycle pos 5)"
1132        );
1133        assert!(
1134            ParsiDate::is_persian_leap_year(1403),
1135            "Year 1403 should be leap (cycle pos 9)"
1136        );
1137        assert!(
1138            ParsiDate::is_persian_leap_year(1408),
1139            "Year 1408 should be leap (cycle pos 14)"
1140        );
1141        assert!(
1142            ParsiDate::is_persian_leap_year(1412),
1143            "Year 1412 should be leap (cycle pos 18)"
1144        );
1145        assert!(
1146            ParsiDate::is_persian_leap_year(1416),
1147            "Year 1416 should be leap (cycle pos 22)"
1148        );
1149        assert!(
1150            ParsiDate::is_persian_leap_year(1420),
1151            "Year 1420 should be leap (cycle pos 26)"
1152        );
1153        assert!(
1154            ParsiDate::is_persian_leap_year(1424),
1155            "Year 1424 should be leap (cycle pos 31)"
1156        );
1157        assert!(
1158            ParsiDate::is_persian_leap_year(1428),
1159            "Year 1428 should be leap (cycle pos 1)"
1160        ); // Next cycle start
1161    }
1162
1163    #[test]
1164    /// Tests the Persian leap year calculation for known non-leap years.
1165    fn test_is_persian_leap_year_false() {
1166        assert!(
1167            !ParsiDate::is_persian_leap_year(1400),
1168            "Year 1400 should not be leap (cycle pos 6)"
1169        );
1170        assert!(
1171            !ParsiDate::is_persian_leap_year(1401),
1172            "Year 1401 should not be leap (cycle pos 7)"
1173        );
1174        assert!(
1175            !ParsiDate::is_persian_leap_year(1402),
1176            "Year 1402 should not be leap (cycle pos 8)"
1177        );
1178        assert!(
1179            !ParsiDate::is_persian_leap_year(1404),
1180            "Year 1404 should not be leap (cycle pos 10)"
1181        );
1182        assert!(
1183            !ParsiDate::is_persian_leap_year(1425),
1184            "Year 1424 should not be leap (cycle pos 32)"
1185        );
1186    }
1187
1188    #[test]
1189    /// Tests the Gregorian leap year calculation.
1190    fn test_is_gregorian_leap_year() {
1191        assert!(
1192            ParsiDate::is_gregorian_leap_year(2000),
1193            "Year 2000 divisible by 400"
1194        );
1195        assert!(
1196            ParsiDate::is_gregorian_leap_year(2020),
1197            "Year 2020 divisible by 4, not 100"
1198        );
1199        assert!(
1200            ParsiDate::is_gregorian_leap_year(2024),
1201            "Year 2024 divisible by 4, not 100"
1202        );
1203        assert!(
1204            !ParsiDate::is_gregorian_leap_year(1900),
1205            "Year 1900 divisible by 100, not 400"
1206        );
1207        assert!(
1208            !ParsiDate::is_gregorian_leap_year(2021),
1209            "Year 2021 not divisible by 4"
1210        );
1211        assert!(
1212            !ParsiDate::is_gregorian_leap_year(2023),
1213            "Year 2023 not divisible by 4"
1214        );
1215    }
1216
1217    // --- Formatting Tests ---
1218
1219    #[test]
1220    /// Tests the different formatting styles.
1221    fn test_format_styles() {
1222        let date = ParsiDate {
1223            year: 1404,
1224            month: 1,
1225            day: 7,
1226        }; // 7 Farvardin 1404
1227           // Short format (YYYY/MM/DD) - Also tests Display trait via to_string()
1228        assert_eq!(date.format("short"), "1404/01/07");
1229        assert_eq!(date.to_string(), "1404/01/07");
1230        // Long format (D MMMM YYYY)
1231        assert_eq!(date.format("long"), "7 فروردین 1404");
1232        // ISO format (YYYY-MM-DD)
1233        assert_eq!(date.format("iso"), "1404-01-07");
1234        // Default/Unknown format should fallback to short
1235        assert_eq!(date.format("unknown_style"), "1404/01/07");
1236    }
1237
1238    #[test]
1239    /// Tests formatting for a date requiring two-digit padding for day/month.
1240    fn test_format_padding() {
1241        let date = ParsiDate {
1242            year: 1400,
1243            month: 11,
1244            day: 20,
1245        }; // 20 Bahman 1400
1246        assert_eq!(date.format("short"), "1400/11/20");
1247        assert_eq!(date.format("iso"), "1400-11-20");
1248        assert_eq!(date.format("long"), "20 بهمن 1400");
1249    }
1250
1251    // --- Weekday Tests ---
1252
1253    #[test]
1254    /// Tests the weekday calculation for known dates.
1255    fn test_weekday() {
1256        // 7 Farvardin 1404 -> March 27, 2025 -> Thursday
1257        let date1 = ParsiDate {
1258            year: 1404,
1259            month: 1,
1260            day: 7,
1261        };
1262        assert_eq!(date1.weekday(), "پنجشنبه");
1263
1264        // 1 Farvardin 1403 -> March 20, 2024 -> Wednesday
1265        let date2 = ParsiDate {
1266            year: 1403,
1267            month: 1,
1268            day: 1,
1269        };
1270        assert_eq!(date2.weekday(), "چهارشنبه");
1271
1272        // 29 Esfand 1402 -> March 19, 2024 -> Tuesday
1273        let date3 = ParsiDate {
1274            year: 1402,
1275            month: 12,
1276            day: 29,
1277        };
1278        assert_eq!(date3.weekday(), "سه‌شنبه");
1279
1280        // 22 Bahman 1357 -> Feb 11, 1979 -> Sunday
1281        let date4 = ParsiDate {
1282            year: 1357,
1283            month: 11,
1284            day: 22,
1285        };
1286        assert_eq!(date4.weekday(), "یکشنبه");
1287
1288        // 2 Mordad 1403 -> July 23, 2024 -> Tuesday
1289        let date5 = ParsiDate {
1290            year: 1403,
1291            month: 5,
1292            day: 2,
1293        };
1294        assert_eq!(date5.weekday(), "سه‌شنبه");
1295
1296        // Test a Friday
1297        // 9 Farvardin 1404 -> March 29, 2025 -> Saturday // Let's find a Friday...
1298        // 1 Tir 1403 -> June 21, 2024 -> Friday
1299        let date_fri = ParsiDate {
1300            year: 1403,
1301            month: 4,
1302            day: 1,
1303        };
1304        assert_eq!(date_fri.weekday(), "جمعه");
1305
1306        // Test a Saturday
1307        // 2 Tir 1403 -> June 22, 2024 -> Saturday
1308        let date_sat = ParsiDate {
1309            year: 1403,
1310            month: 4,
1311            day: 2,
1312        };
1313        assert_eq!(date_sat.weekday(), "شنبه");
1314
1315        // Test a Monday
1316        // 4 Tir 1403 -> June 24, 2024 -> Monday
1317        let date_mon = ParsiDate {
1318            year: 1403,
1319            month: 4,
1320            day: 4,
1321        };
1322        assert_eq!(date_mon.weekday(), "دوشنبه");
1323    }
1324
1325    // --- Days Between Tests ---
1326
1327    #[test]
1328    /// Tests calculating days between dates within the same month and year.
1329    fn test_days_between_same_month() {
1330        let date1 = ParsiDate {
1331            year: 1404,
1332            month: 1,
1333            day: 7,
1334        };
1335        let date2 = ParsiDate {
1336            year: 1404,
1337            month: 1,
1338            day: 10,
1339        };
1340        assert_eq!(date1.days_between(&date2), 3);
1341        assert_eq!(date2.days_between(&date1), 3); // Test commutativity
1342    }
1343
1344    #[test]
1345    /// Tests calculating days between dates in different months but the same year.
1346    fn test_days_between_different_months() {
1347        let date1 = ParsiDate {
1348            year: 1403,
1349            month: 1,
1350            day: 1,
1351        }; // Mar 20, 2024
1352        let date2 = ParsiDate {
1353            year: 1403,
1354            month: 2,
1355            day: 1,
1356        }; // Apr 20, 2024 (31 days in Farvardin)
1357        assert_eq!(date1.days_between(&date2), 31);
1358    }
1359
1360    #[test]
1361    /// Tests calculating days between dates spanning across a Persian year boundary.
1362    fn test_days_between_crossing_year() {
1363        // End of non-leap year
1364        let date1 = ParsiDate {
1365            year: 1402,
1366            month: 12,
1367            day: 29,
1368        }; // Mar 19, 2024
1369           // Start of next year
1370        let date2 = ParsiDate {
1371            year: 1403,
1372            month: 1,
1373            day: 1,
1374        }; // Mar 20, 2024
1375        assert_eq!(date1.days_between(&date2), 1);
1376
1377        // End of leap year
1378        let date3 = ParsiDate {
1379            year: 1403,
1380            month: 12,
1381            day: 30,
1382        }; // Mar 20, 2025
1383           // Start of next year
1384        let date4 = ParsiDate {
1385            year: 1404,
1386            month: 1,
1387            day: 1,
1388        }; // Mar 21, 2025
1389        assert_eq!(date3.days_between(&date4), 1);
1390
1391        // Larger gap across year
1392        let date5 = ParsiDate {
1393            year: 1403,
1394            month: 10,
1395            day: 15,
1396        }; // Dec 5, 2024 approx? No, Jan 5, 2025
1397           // 1403/10/15 -> Jan 5, 2025
1398        let date6 = ParsiDate {
1399            year: 1404,
1400            month: 2,
1401            day: 10,
1402        }; // Apr 30, 2025 approx?
1403           // 1404/01/01 -> Mar 21, 2025
1404           // 1404/02/10 -> Day 31 (Far) + 10 (Ord) = 41 days after Nowruz -> Mar 21 + 40 days = Apr 30, 2025
1405        let g5 = date5.to_gregorian().unwrap(); // 2025-01-05
1406        let g6 = date6.to_gregorian().unwrap(); // 2025-04-30
1407        let expected_days = (g6 - g5).num_days(); // 115 days
1408        assert_eq!(date5.days_between(&date6), expected_days);
1409        assert_eq!(date5.days_between(&date6), 116);
1410    }
1411
1412    #[test]
1413    /// Tests calculating days between identical dates.
1414    fn test_days_between_same_date() {
1415        let date1 = ParsiDate {
1416            year: 1403,
1417            month: 5,
1418            day: 2,
1419        };
1420        let date2 = ParsiDate {
1421            year: 1403,
1422            month: 5,
1423            day: 2,
1424        };
1425        assert_eq!(date1.days_between(&date2), 0);
1426    }
1427}