pcal/
persian.rs

1//! Persian calendar data: month names, weekday names, and numeral conversion.
2//!
3//! Provides both Persian script and romanized forms for all calendar labels,
4//! plus a parser that accepts either form as input.
5
6/// Month names in Persian script (Farvardin through Esfand).
7///
8/// Prefixed with U+200E (LTR mark) to prevent terminal `BiDi` reordering
9/// when displayed alongside numbers.
10pub const MONTH_NAMES: [&str; 12] = [
11    "\u{200e}فروردین",
12    "\u{200e}اردیبهشت",
13    "\u{200e}خرداد",
14    "\u{200e}تیر",
15    "\u{200e}مرداد",
16    "\u{200e}شهریور",
17    "\u{200e}مهر",
18    "\u{200e}آبان",
19    "\u{200e}آذر",
20    "\u{200e}دی",
21    "\u{200e}بهمن",
22    "\u{200e}اسفند",
23];
24
25/// Month names in romanized form (lowercase), used as default display
26/// and for parsing user input.
27pub const MONTH_NAMES_ROMANIZED: [&str; 12] = [
28    "farvardin",
29    "ordibehesht",
30    "khordad",
31    "tir",
32    "mordad",
33    "shahrivar",
34    "mehr",
35    "aban",
36    "azar",
37    "dey",
38    "bahman",
39    "esfand",
40];
41
42/// Short weekday names in English (Saturday to Friday)
43pub const WEEKDAY_NAMES_EN: [&str; 7] = ["Sa", "Su", "Mo", "Tu", "We", "Th", "Fr"];
44
45/// Short weekday names in Persian (Saturday to Friday).
46/// Prefixed with U+200E (LTR mark) to prevent terminal `BiDi` reordering.
47pub const WEEKDAY_NAMES_FA: [&str; 7] = [
48    "\u{200e}ش",
49    "\u{200e}ی",
50    "\u{200e}د",
51    "\u{200e}س",
52    "\u{200e}چ",
53    "\u{200e}پ",
54    "\u{200e}ج",
55];
56
57/// Number of days in each month (index 0 = Farvardin).
58/// Months 1–6 have 31 days, months 7–11 have 30 days, month 12 has 29
59/// (or 30 in leap years, handled by `shahanshahi::days_in_month`).
60pub const MONTH_DAYS: [u32; 12] = [31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29];
61
62/// Persian (Eastern Arabic) digit characters, indexed 0–9.
63const PERSIAN_DIGITS: [char; 10] = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'];
64
65/// Replace all ASCII digits (0–9) in a string with their Persian equivalents (۰–۹).
66#[must_use]
67pub fn to_persian_digits(s: &str) -> String {
68    s.chars()
69        .map(|c| {
70            if c.is_ascii_digit() {
71                PERSIAN_DIGITS[(c as u8 - b'0') as usize]
72            } else {
73                c
74            }
75        })
76        .collect()
77}
78
79/// Parse a month string into a 1-based month index (1–12).
80///
81/// Accepts numeric ("5"), romanized ("mordad", case-insensitive),
82/// or Persian script ("مرداد") input. Returns `None` for invalid input.
83#[must_use]
84pub fn parse_month(input: &str) -> Option<u32> {
85    // Try numeric first
86    if let Ok(n) = input.parse::<u32>() {
87        if (1..=12).contains(&n) {
88            return Some(n);
89        }
90        return None;
91    }
92
93    let lower = input.to_lowercase();
94
95    // Try romanized names
96    for (i, name) in MONTH_NAMES_ROMANIZED.iter().enumerate() {
97        if *name == lower {
98            return Some(i as u32 + 1);
99        }
100    }
101
102    // Try Persian names (strip LTR mark prefix from MONTH_NAMES)
103    for (i, name) in MONTH_NAMES.iter().enumerate() {
104        let clean_name = name.trim_start_matches('\u{200e}');
105        if clean_name == input {
106            return Some(i as u32 + 1);
107        }
108    }
109
110    None
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_to_persian_digits() {
119        assert_eq!(to_persian_digits("1404"), "۱۴۰۴");
120        assert_eq!(to_persian_digits("31"), "۳۱");
121        assert_eq!(to_persian_digits("abc"), "abc");
122    }
123
124    #[test]
125    fn test_parse_month_numeric() {
126        assert_eq!(parse_month("1"), Some(1));
127        assert_eq!(parse_month("12"), Some(12));
128        assert_eq!(parse_month("0"), None);
129        assert_eq!(parse_month("13"), None);
130    }
131
132    #[test]
133    fn test_parse_month_romanized() {
134        assert_eq!(parse_month("mordad"), Some(5));
135        assert_eq!(parse_month("Mordad"), Some(5));
136        assert_eq!(parse_month("esfand"), Some(12));
137    }
138
139    #[test]
140    fn test_parse_month_persian() {
141        assert_eq!(parse_month("فروردین"), Some(1));
142        assert_eq!(parse_month("مرداد"), Some(5));
143        assert_eq!(parse_month("اسفند"), Some(12));
144    }
145}