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}