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}