jalali_rs/
lib.rs

1//! # Jalali-rs
2//!
3//! A simple crate for converting dates between the Gregorian and Jalali (Persian) calendars.
4//!
5//! This crate provides functions for date conversions, including support for Unix timestamps and string formatting.
6//! All functions assume valid inputs where possible, but return `Option` for cases with potential invalid data to avoid panics.
7//!
8//! ## Core Features
9//! - Convert Gregorian to Jalali dates and vice versa.
10//! - Convert Unix timestamps to Jalali dates and vice versa (assuming UTC midnight; negative timestamps return `None`).
11//! - Parse and format date strings with custom separators, handling Persian/Arabic digits automatically.
12//! - Convert between Latin, Persian, and Arabic digits for flexible user input.
13//!
14//! ## Usage
15//!
16//! ```rust
17//! use jalali_rs::{gregorian_to_jalali, jalali_to_gregorian,unix_to_jalali,jalali_to_unix,persian_or_arabic_digits_to_latin,latin_digits_to_persian,parse_gregorian_string_to_jalali_string,parse_jalali_string_to_gregorian_string};
18//!
19//! // Basic date conversion
20//! let (jy, jm, jd) = gregorian_to_jalali(2025, 12, 27);
21//! assert_eq!((jy, jm, jd), (1404, 10, 6));
22//!
23//! let (gy, gm, gd) = jalali_to_gregorian(1404, 10, 6);
24//! assert_eq!((gy, gm, gd), (2025, 12, 27));
25//!
26//! // Unix timestamp conversion (0 -> 1970-01-01 Gregorian -> 1348-10-11 Jalali)
27//! if let Some((jy, jm, jd)) = unix_to_jalali(0) {
28//!     println!("Jalali from Unix 0: {}-{}-{}", jy, jm, jd);
29//! }
30//!
31//! if let Some(timestamp) = jalali_to_unix(1348, 10, 11) {
32//!     println!("Unix from Jalali 1348-10-11: {}", timestamp);
33//! }
34//!
35//! // String parsing with separator (handles Persian/Arabic digits)
36//! if let Some(jalali_str) = parse_gregorian_string_to_jalali_string("۲۰۲۵-۱۲-۲۷", '-') {
37//!     println!("Converted: {}", jalali_str); // Outputs: 1404-10-06
38//! }
39//!
40//! if let Some(gregorian_str) = parse_jalali_string_to_gregorian_string("۱۴۰۴-۱۰-۰۶", '-') {
41//!     println!("Converted: {}", gregorian_str); // Outputs: 2025-12-27
42//! }
43//!
44//! // Digit conversions
45//! let persian = latin_digits_to_persian("2025-12-27");
46//! println!("Persian digits: {}", persian); // ۲۰۲۵-۱۲-۲۷
47//!
48//! let latin = persian_or_arabic_digits_to_latin("۱۴۰۴-۱۰-۰۶");
49//! println!("Latin digits: {}", latin); // 1404-10-06
50//! ```
51
52/// Converts a Gregorian date to a Jalali (Persian) date.
53///
54/// # Arguments
55///
56/// * `gregorian_year` - The Gregorian year (e.g., 2025).
57/// * `gregorian_month` - The Gregorian month (1-12).
58/// * `gregorian_day` - The Gregorian day (1-31).
59///
60/// # Returns
61///
62/// A tuple containing (jalali_year, jalali_month, jalali_day).
63///
64/// # Examples
65///
66/// ```
67/// let (jy, jm, jd) = jalali_rs::gregorian_to_jalali(2025, 12, 27);
68/// assert_eq!((jy, jm, jd), (1404, 10, 6));
69/// ```
70pub fn gregorian_to_jalali(
71    gregorian_year: i32,
72    gregorian_month: usize,
73    gregorian_day: i32,
74) -> (i32, u32, u32) {
75    // cumulative days at the end of each Gregorian month (non-leap year, adjusted later)
76    let gregorian_cumulative_days: [i64; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
77
78    // adjust year for leap year calculation if month is after February
79    let adjusted_year: i64 = if gregorian_month > 2 {
80        gregorian_year as i64 + 1
81    } else {
82        gregorian_year as i64
83    };
84
85    // calculate total days from a fixed epoch, including leap year adjustments
86    let mut total_days: i64 = 355666
87        + (365 * gregorian_year as i64)
88        + ((adjusted_year + 3) / 4)
89        - ((adjusted_year + 99) / 100)
90        + ((adjusted_year + 399) / 400)
91        + gregorian_day as i64
92        + gregorian_cumulative_days[gregorian_month - 1];
93
94    // compute Jalali year using divisions based on Jalali cycle lengths
95    let mut jalali_year: i64 = -1595 + (33 * (total_days / 12053));
96    total_days %= 12053;
97    jalali_year += 4 * (total_days / 1461);
98    total_days %= 1461;
99
100    // handle extra days beyond a standard year
101    if total_days > 365 {
102        jalali_year += (total_days - 1) / 365;
103        total_days = (total_days - 1) % 365;
104    }
105
106    let (jalali_month, jalali_day);
107    if total_days < 186 {
108        // first half of Jalali year (months 1-6, 31 days each)
109        jalali_month = 1 + (total_days / 31) as u32;
110        jalali_day = 1 + (total_days % 31) as u32;
111    } else {
112        // second half of Jalali year (months 7-12, 30 days each except possibly last)
113        jalali_month = 7 + ((total_days - 186) / 30) as u32;
114        jalali_day = 1 + ((total_days - 186) % 30) as u32;
115    }
116
117    (jalali_year as i32, jalali_month, jalali_day)
118}
119
120/// Converts a Jalali (Persian) date to a Gregorian date.
121///
122/// # Arguments
123///
124/// * `jalali_year` - The Jalali year (e.g., 1404).
125/// * `jalali_month` - The Jalali month (1-12).
126/// * `jalali_day` - The Jalali day (1-31).
127///
128/// # Returns
129///
130/// A tuple containing (gregorian_year, gregorian_month, gregorian_day).
131///
132/// # Examples
133///
134/// ```
135/// let (gy, gm, gd) = jalali_rs::jalali_to_gregorian(1404, 10, 6);
136/// assert_eq!((gy, gm, gd), (2025, 12, 27));
137/// ```
138pub fn jalali_to_gregorian(
139    jalali_year: i32,
140    jalali_month: usize,
141    jalali_day: i32,
142) -> (i32, u32, u32) {
143    let  jalali_year_i64: i64 = jalali_year as i64 + 1595;
144
145    // calculate total days from a fixed epoch, including Jalali leap adjustments
146    let mut total_days: i64 = -355668
147        + (365 * jalali_year_i64)
148        + ((jalali_year_i64 / 33) * 8)
149        + (((jalali_year_i64 % 33) + 3) / 4)
150        + jalali_day as i64
151        + if jalali_month < 7 {
152        (jalali_month as i64 - 1) * 31
153    } else {
154        ((jalali_month as i64 - 7) * 30) + 186
155    };
156
157    // compute Gregorian year using divisions based on Gregorian cycle lengths
158    let mut gregorian_year: i64 = 400 * (total_days / 146097);
159    total_days %= 146097;
160    if total_days > 36524 {
161        total_days -= 1;
162        gregorian_year += 100 * (total_days / 36524);
163        total_days %= 36524;
164        if total_days >= 365 {
165            total_days += 1;
166        }
167    }
168    gregorian_year += 4 * (total_days / 1461);
169    total_days %= 1461;
170    if total_days > 365 {
171        gregorian_year += (total_days - 1) / 365;
172        total_days = (total_days - 1) % 365;
173    }
174
175    // determine Gregorian day and advance through months
176    let mut gregorian_day: i64 = total_days + 1;
177
178    // array of days in each Gregorian month, adjusting February for leap year
179    let is_leap_year = (gregorian_year % 4 == 0 && gregorian_year % 100 != 0)
180        || (gregorian_year % 400 == 0);
181    let gregorian_days_in_month: [i64; 13] = [
182        0,
183        31,
184        if is_leap_year { 29 } else { 28 },
185        31,
186        30,
187        31,
188        30,
189        31,
190        31,
191        30,
192        31,
193        30,
194        31,
195    ];
196
197    let mut gregorian_month: usize = 0;
198    while gregorian_month < 13 && gregorian_day > gregorian_days_in_month[gregorian_month] {
199        gregorian_day -= gregorian_days_in_month[gregorian_month];
200        gregorian_month += 1;
201    }
202
203    (gregorian_year as i32, gregorian_month as u32, gregorian_day as u32)
204}
205
206/// Converts a Unix timestamp (seconds since 1970-01-01 UTC) to a Jalali date.
207///
208/// Returns `None` for negative timestamps or invalid calculations.
209///
210/// # Arguments
211///
212/// * `timestamp` - Unix timestamp in seconds.
213///
214/// # Returns
215///
216/// An `Option` containing (jalali_year, jalali_month, jalali_day) or `None`.
217///
218/// # Examples
219///
220/// ```
221/// if let Some((jy, jm, jd)) = jalali_rs::unix_to_jalali(0) {
222///     assert_eq!((jy, jm, jd), (1348, 10, 11));
223/// }
224/// if let Some((jy, jm, jd)) = jalali_rs::unix_to_jalali(1766806014) {
225///     assert_eq!((jy, jm, jd), (1404, 10, 6));
226/// }
227/// ```
228pub fn unix_to_jalali(timestamp: i64) -> Option<(i32, u32, u32)> {
229    unix_to_gregorian(timestamp).map(|(gy, gm, gd)| {
230        gregorian_to_jalali(gy, gm as usize, gd as i32)
231    })
232}
233
234/// Converts a Jalali date to a Unix timestamp (seconds since 1970-01-01 UTC at midnight).
235///
236/// Returns `None` if the date is before 1970-01-01 or invalid.
237///
238/// # Arguments
239///
240/// * `jalali_year` - The Jalali year.
241/// * `jalali_month` - The Jalali month (1-12).
242/// * `jalali_day` - The Jalali day (1-31).
243///
244/// # Returns
245///
246/// An `Option` containing the Unix timestamp or `None`.
247///
248/// # Examples
249///
250/// ```
251/// if let Some(ts) = jalali_rs::jalali_to_unix(1348, 10, 11) {
252///     assert_eq!(ts, 0);
253/// }
254/// ```
255pub fn jalali_to_unix(jalali_year: i32, jalali_month: u32, jalali_day: u32) -> Option<i64> {
256    let (gy, gm, gd) = jalali_to_gregorian(jalali_year, jalali_month as usize, jalali_day as i32);
257    gregorian_to_unix(gy, gm, gd)
258}
259
260/// Parses a Gregorian date string (e.g., "2025-12-27") and converts to Jalali string format.
261///
262/// Handles Persian/Arabic digits in input. Returns `None` for invalid formats.
263///
264/// # Arguments
265///
266/// * `date_str` - The date string.
267/// * `separator` - The separator character (e.g., '-').
268///
269/// # Returns
270///
271/// An `Option` containing the Jalali date string (e.g., "1404-10-06") or `None`.
272///
273/// # Examples
274///
275/// ```
276/// let result = jalali_rs::parse_gregorian_string_to_jalali_string("2025-12-27", '-');
277/// assert_eq!(result, Some("1404-10-06".to_string()));
278///
279/// let result_persian = jalali_rs::parse_gregorian_string_to_jalali_string("۲۰۲۵-۱۲-۲۷", '-');
280/// assert_eq!(result_persian, Some("1404-10-06".to_string()));
281/// ```
282pub fn parse_gregorian_string_to_jalali_string(date_str: &str, separator: char) -> Option<String> {
283    let normalized = persian_or_arabic_digits_to_latin(date_str);
284    let parts: Vec<&str> = normalized.split(separator).collect();
285    if parts.len() != 3 {
286        return None;
287    }
288    let gy = parts[0].parse::<i32>().ok()?;
289    let gm = parts[1].parse::<usize>().ok()?;
290    let gd = parts[2].parse::<i32>().ok()?;
291    if gm < 1 || gm > 12 || gd < 1 || gd > 31 {
292        return None; // basic validation
293    }
294    let (jy, jm, jd) = gregorian_to_jalali(gy, gm, gd);
295    Some(format!("{:04}-{:02}-{:02}", jy, jm, jd))
296}
297
298/// Parses a Jalali date string (e.g., "1404-10-06") and converts to Gregorian string format.
299///
300/// Handles Persian/Arabic digits in input. Returns `None` for invalid formats.
301///
302/// # Arguments
303///
304/// * `date_str` - The date string.
305/// * `separator` - The separator character (e.g., '-').
306///
307/// # Returns
308///
309/// An `Option` containing the Gregorian date string (e.g., "2025-12-27") or `None`.
310///
311/// # Examples
312///
313/// ```
314/// let result = jalali_rs::parse_jalali_string_to_gregorian_string("1404-10-06", '-');
315/// assert_eq!(result, Some("2025-12-27".to_string()));
316///
317/// let result_persian = jalali_rs::parse_jalali_string_to_gregorian_string("۱۴۰۴-۱۰-۰۶", '-');
318/// assert_eq!(result_persian, Some("2025-12-27".to_string()));
319/// ```
320pub fn parse_jalali_string_to_gregorian_string(date_str: &str, separator: char) -> Option<String> {
321    let normalized = persian_or_arabic_digits_to_latin(date_str);
322    let parts: Vec<&str> = normalized.split(separator).collect();
323    if parts.len() != 3 {
324        return None;
325    }
326    let jy = parts[0].parse::<i32>().ok()?;
327    let jm = parts[1].parse::<usize>().ok()?;
328    let jd = parts[2].parse::<i32>().ok()?;
329    if jm < 1 || jm > 12 || jd < 1 || jd > 31 {
330        return None; // basic validation
331    }
332    let (gy, gm, gd) = jalali_to_gregorian(jy, jm, jd);
333    Some(format!("{:04}-{:02}-{:02}", gy, gm, gd))
334}
335
336/// Converts Latin digits in a string to Persian digits.
337///
338/// Non-digit characters remain unchanged.
339///
340/// # Arguments
341///
342/// * `s` - The input string.
343///
344/// # Returns
345///
346/// A new string with Latin digits replaced by Persian equivalents.
347///
348/// # Examples
349///
350/// ```
351/// let result = jalali_rs::latin_digits_to_persian("1400-12-10");
352/// assert_eq!(result, "۱۴۰۰-۱۲-۱۰");
353/// ```
354pub fn latin_digits_to_persian(s: &str) -> String {
355    s.chars()
356        .map(|c| {
357            if c.is_ascii_digit() {
358                char::from_u32('۰' as u32 + (c as u32 - '0' as u32)).unwrap()
359            } else {
360                c
361            }
362        })
363        .collect()
364}
365
366/// Converts Persian or Arabic digits in a string to Latin digits.
367///
368/// Non-digit characters remain unchanged. Handles both Persian (U+06F0-U+06F9) and Arabic (U+0660-U+0669) digits.
369///
370/// # Arguments
371///
372/// * `s` - The input string.
373///
374/// # Returns
375///
376/// A new string with Persian/Arabic digits replaced by Latin equivalents.
377///
378/// # Examples
379///
380/// ```
381/// let result_persian = jalali_rs::persian_or_arabic_digits_to_latin("۱۴۰۰-۱۲-۱۰");
382/// assert_eq!(result_persian, "1400-12-10");
383///
384/// let result_arabic = jalali_rs::persian_or_arabic_digits_to_latin("١٤٠٠-١٢-١٠");
385/// assert_eq!(result_arabic, "1400-12-10");
386/// ```
387pub fn persian_or_arabic_digits_to_latin(s: &str) -> String {
388    s.chars()
389        .map(|c| {
390            let u = c as u32;
391            if (0x0660..=0x0669).contains(&u) {
392                char::from_u32('0' as u32 + (u - 0x0660)).unwrap()
393            } else if (0x06F0..=0x06F9).contains(&u) {
394                char::from_u32('0' as u32 + (u - 0x06F0)).unwrap()
395            } else {
396                c
397            }
398        })
399        .collect()
400}
401
402// Helper function to convert Julian Day Number (JDN) to Gregorian date.
403fn jdn_to_gregorian(jdn: i64) -> (i32, u32, u32) {
404    let a = jdn + 32044;
405    let b = (4 * a + 3) / 146097;
406    let c = a - (146097 * b) / 4;
407    let d = (4 * c + 3) / 1461;
408    let e = c - (1461 * d) / 4;
409    let m = (5 * e + 2) / 153;
410    let day = (e - (153 * m + 2) / 5 + 1) as u32;
411    let month = (m + 3 - 12 * (m / 10)) as u32;
412    let year = (100 * b + d - 4800 + (m / 10)) as i32;
413    (year, month, day)
414}
415
416// Helper function to convert Unix timestamp to Gregorian date.
417fn unix_to_gregorian(timestamp: i64) -> Option<(i32, u32, u32)> {
418    // Modify to handle negative timestamps
419    let days = timestamp / 86_400;
420    let jdn = 2_440_588 + days;
421    Some(jdn_to_gregorian(jdn))
422}
423
424// Helper function to convert Gregorian date to Unix timestamp.
425fn gregorian_to_unix(year: i32, month: u32, day: u32) -> Option<i64> {
426    let jdn = gregorian_to_jdn(year, month as i32, day as i32);
427    let days = jdn - 2_440_588;
428    Some(days * 86_400)
429}
430
431// Additional helper function for Julian Day Number conversion
432fn gregorian_to_jdn(year: i32, month: i32, day: i32) -> i64 {
433    let a = (14 - month) / 12;
434    let y = year + 4800 - a;
435    let m = month + 12 * a - 3;
436
437    let jdn = day as i64
438        + ((153 * m + 2) / 5) as i64
439        + 365 * y as i64
440        + y as i64 / 4
441        - y as i64 / 100
442        + y as i64 / 400
443        - 32045;
444
445    jdn
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451
452    #[test]
453    fn test_gregorian_to_jalali() {
454        let (jy, jm, jd) = gregorian_to_jalali(2023, 12, 27);
455        assert_eq!(jy, 1402);
456        assert_eq!(jm, 10);
457        assert_eq!(jd, 6);
458
459        let (jy, jm, jd) = gregorian_to_jalali(2025, 12, 27);
460        assert_eq!(jy, 1404);
461        assert_eq!(jm, 10);
462        assert_eq!(jd, 6);
463    }
464
465    #[test]
466    fn test_jalali_to_gregorian() {
467        let (gy, gm, gd) = jalali_to_gregorian(1402, 10, 6);
468        assert_eq!(gy, 2023);
469        assert_eq!(gm, 12);
470        assert_eq!(gd, 27);
471
472        let (gy, gm, gd) = jalali_to_gregorian(1404, 10, 6);
473        assert_eq!(gy, 2025);
474        assert_eq!(gm, 12);
475        assert_eq!(gd, 27);
476    }
477
478    #[test]
479    fn test_unix_to_jalali() {
480        let result = unix_to_jalali(0);
481        assert_eq!(result, Some((1348, 10, 11)));
482
483        let result2 = unix_to_jalali(-1234567890);
484        assert_eq!(result2, Some((1309, 8, 28)));
485    }
486
487    #[test]
488    fn test_jalali_to_unix() {
489        let result = jalali_to_unix(1348, 10, 11);
490        assert_eq!(result, Some(0));
491    }
492
493    #[test]
494    fn test_parse_gregorian_string_to_jalali_string() {
495        let result = parse_gregorian_string_to_jalali_string("2025-12-27", '-');
496        assert_eq!(result, Some("1404-10-06".to_string()));
497
498        let result_persian = parse_gregorian_string_to_jalali_string("۲۰۲۵-۱۲-۲۷", '-');
499        assert_eq!(result_persian, Some("1404-10-06".to_string()));
500
501        let invalid = parse_gregorian_string_to_jalali_string("invalid", '-');
502        assert_eq!(invalid, None);
503    }
504
505    #[test]
506    fn test_parse_jalali_string_to_gregorian_string() {
507        let result = parse_jalali_string_to_gregorian_string("1404-10-06", '-');
508        assert_eq!(result, Some("2025-12-27".to_string()));
509
510        let result_persian = parse_jalali_string_to_gregorian_string("۱۴۰۴-۱۰-۰۶", '-');
511        assert_eq!(result_persian, Some("2025-12-27".to_string()));
512
513        let invalid = parse_jalali_string_to_gregorian_string("invalid", '-');
514        assert_eq!(invalid, None);
515    }
516
517    #[test]
518    fn test_latin_digits_to_persian() {
519        let result = latin_digits_to_persian("1400-12-10");
520        assert_eq!(result, "۱۴۰۰-۱۲-۱۰");
521    }
522
523    #[test]
524    fn test_persian_or_arabic_digits_to_latin() {
525        let result_persian = persian_or_arabic_digits_to_latin("۱۴۰۰-۱۲-۱۰");
526        assert_eq!(result_persian, "1400-12-10");
527
528        let result_arabic = persian_or_arabic_digits_to_latin("١٤٠٠-١٢-١٠");
529        assert_eq!(result_arabic, "1400-12-10");
530
531        let mixed = persian_or_arabic_digits_to_latin("۴٤۵٥۶٦"); // Persian 4, Arabic 4,5,6
532        assert_eq!(mixed, "445566");
533    }
534}