khorshid_common/
lib.rs

1use chrono::Datelike;
2use parsidate::{ParsiDate, ParsiDateTime, DateError};
3
4#[derive(Debug)]
5pub enum Script {
6    Latin,
7    Persian,
8    English,
9}
10
11const PERSIAN_DAY_NAMES : [&str; 7] = ["شنبه", "یک‌شنبه", "دوشنبه", "سه‌شنبه", "چهارشنبه", "پنج‌شنبه", "جمعه"];
12const LATIN_DAY_NAMES : [&str; 7] = ["Shanbeh", "Yek-Shanbeh", "Do-Shanbeh", "Seh-Shanbeh", "Chahar-Shanbeh", "Panj-Shanbeh", "Jomeh"];
13const ENGLISH_DAY_NAMES : [&str; 7] = ["Saturday", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday"];
14const PERSIAN_MONTH_NAMES : [&str; 12] = ["فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور", "مهر", "آبان", "آذر", "دی", "بهمن", "اسفند"];
15const LATIN_MONTH_NAMES : [&str; 12] = ["Farvardin", "Ordibehesht", "Khordad", "Tir", "Mordad", "Shahrivar", "Mehr", "Aban", "Azar", "Dei", "Bahman", "Esfand"];
16
17pub trait DateTimeFormat {
18    fn format_datetime(&self, pattern: &str, script: &Script) -> String;
19}
20
21pub trait DateHelper {
22    fn day_name_in(&self, script: &Script) -> Result<&'static str, DateError>;
23    fn month_name_in(&self, script: &Script) -> &'static str;
24    fn month_first_day(&self) -> Result<usize, DateError>;
25    fn weekday_from_saturday(&self) -> Result<usize, DateError>;
26    fn next_month(&self) -> Result<ParsiDate, DateError>;
27    fn prev_month(&self) -> Result<ParsiDate, DateError>;
28}
29
30impl DateHelper for ParsiDate {
31    fn day_name_in(&self, script: &Script) -> Result<&'static str, DateError> {
32        let day_index = self.weekday_from_saturday()?;
33
34        Ok(match script {
35            Script::Latin => LATIN_DAY_NAMES[day_index],
36            Script::English => ENGLISH_DAY_NAMES[day_index],
37            Script::Persian => PERSIAN_DAY_NAMES[day_index],
38        })
39    }
40
41    fn month_name_in(&self, script: &Script) -> &'static str {
42        let month_index = self.month() as usize - 1;
43
44        match script {
45            Script::Latin | Script::English => LATIN_MONTH_NAMES[month_index],
46            Script::Persian => PERSIAN_MONTH_NAMES[month_index],
47        }
48    }
49
50    fn month_first_day(&self) -> Result<usize, DateError> {
51        self.first_day_of_month().weekday_from_saturday()
52    }
53
54    fn weekday_from_saturday(&self) -> Result<usize, DateError> {
55        let weekday_from_monday = self.to_gregorian()?.weekday() as usize;
56
57        Ok((weekday_from_monday + 2) % 7)
58    }
59
60    fn next_month(&self) -> Result<ParsiDate, DateError> {
61        self.add_months(1)
62    }
63
64    fn prev_month(&self) -> Result<ParsiDate, DateError> {
65        self.sub_months(1)
66    }
67}
68
69impl DateTimeFormat for ParsiDateTime {
70    fn format_datetime(&self, pattern: &str, script: &Script) -> String {
71        let modified_pattern = pattern
72            .replace("%R", "%H:%M")
73            .replace("%X", "%H:%M:%S")
74            .replace("%F", "%Y-%m-%d")
75            .replace("%x", "%Y/%m/%d")
76            .replace("%t", "\t")
77            .replace("%n", "\n")
78            .replace("%e", &format!("{:2}", self.day()))
79            .replace("%A", self.date().day_name_in(script).unwrap_or("%A"))
80            .replace("%B", self.date().month_name_in(script));
81
82        self.format(&modified_pattern)
83    }
84}
85
86pub fn replace_with_persian_numbers(text: String) -> String {
87    text
88        .replace('0', "۰")
89        .replace('1', "۱")
90        .replace('2', "۲")
91        .replace('3', "۳")
92        .replace('4', "۴")
93        .replace('5', "۵")
94        .replace('6', "۶")
95        .replace('7', "۷")
96        .replace('8', "۸")
97        .replace('9', "۹")
98}
99
100pub fn abbreviated_day_names(script: &Script) -> Vec<&'static str> {
101    let (day_names, bytes) = match script {
102        Script::Latin => (LATIN_DAY_NAMES, 2),
103        Script::English => (ENGLISH_DAY_NAMES, 2),
104        Script::Persian => (PERSIAN_DAY_NAMES, 2 * 2),
105    };
106
107    day_names.iter().map(|day| &day[..bytes]).collect::<Vec<_>>()
108}
109
110#[cfg(not(fake_date))]
111pub fn date_now() -> Result<ParsiDateTime, DateError> {
112    ParsiDateTime::now()
113}
114#[cfg(fake_date)]
115pub fn date_now() -> Result<ParsiDateTime, DateError> {
116    // UTC: 2025-04-05 19:25:30 IRST: 1404-01-16 22:55:30
117    ParsiDateTime::new(1404, 1, 16, 22, 55, 30)
118}
119
120#[cfg(not(fake_date))]
121pub fn date_today() -> Result<ParsiDate, DateError> {
122    ParsiDate::today()
123}
124#[cfg(fake_date)]
125pub fn date_today() -> Result<ParsiDate, DateError> {
126    // UTC: 2025-04-05: 1404-01-16
127    ParsiDate::new(1404, 1, 16)
128}
129
130#[cfg(test)]
131mod tests {
132    #[allow(unused)]
133    use super::*;
134
135    #[cfg(not(fake_date))]
136    #[test]
137    fn test_date_format() -> Result<(), Box<dyn std::error::Error>> {
138        use predicates::prelude::{predicate, Predicate};
139
140        let date = date_now();
141        let script = Script::Latin;
142
143        let predicate = predicate::str::is_match(r"\d{2}:\d{2}")?;
144        assert_eq!(true, predicate.eval(&date?.format_datetime("%R", &script)));
145
146        let predicate = predicate::str::is_match(r"\d{2}:\d{2}:\d{2}")?;
147        assert_eq!(true, predicate.eval(&date?.format_datetime("%T", &script)));
148        assert_eq!(true, predicate.eval(&date?.format_datetime("%X", &script)));
149        assert_eq!(true, predicate.eval(&date?.format_datetime("%H:%M:%S", &script)));
150
151        let predicate = predicate::str::is_match(r"\d{4}-\d{2}-\d{2}")?;
152        assert_eq!(true, predicate.eval(&date?.format_datetime("%F", &script)));
153        assert_eq!(true, predicate.eval(&date?.format_datetime("%Y-%m-%d", &script)));
154
155        let predicate = predicate::str::is_match(r"\d{4}/\d{2}/\d{2}")?;
156        assert_eq!(true, predicate.eval(&date?.format_datetime("%x", &script)));
157        assert_eq!(true, predicate.eval(&date?.format_datetime("%Y/%m/%d", &script)));
158
159        assert_eq!(date?.format_datetime("%t%n", &script), "\t\n");
160
161        Ok(())
162    }
163
164    #[test]
165    fn test_replace_with_persian_numbers() {
166        assert_eq!(replace_with_persian_numbers("1403-09-26 18:37".to_string()), "۱۴۰۳-۰۹-۲۶ ۱۸:۳۷")
167    }
168
169    #[test]
170    fn test_abbreviated_day_names() {
171        assert_eq!(abbreviated_day_names(&Script::Latin)[0], "Sh");
172        assert_eq!(abbreviated_day_names(&Script::English)[1], "Su");
173        assert_eq!(abbreviated_day_names(&Script::Persian)[2], "دو");
174    }
175
176    #[cfg(fake_date)]
177    #[test]
178    fn test_jalali_date_now() -> Result<(), DateError> {
179        let date = date_now()?;
180
181        assert_eq!(date.year(), 1404);
182        assert_eq!(date.month(), 1);
183        assert_eq!(date.day(), 16);
184        assert_eq!(date.hour(), 22);
185        assert_eq!(date.minute(), 55);
186        assert_eq!(date.second(), 30);
187
188        Ok(())
189    }
190
191    #[cfg(fake_date)]
192    #[test]
193    fn test_date_format() -> Result<(), DateError> {
194        let date = date_now()?;
195        let script = Script::Latin;
196
197        assert_eq!(date.format_datetime("%R", &script), "22:55");
198        assert_eq!(date.format_datetime("%T", &script), "22:55:30");
199        assert_eq!(date.format_datetime("%X", &script), "22:55:30");
200        assert_eq!(date.format_datetime("%H:%M:%S", &script), "22:55:30");
201        assert_eq!(date.format_datetime("%F", &script), "1404-01-16");
202        assert_eq!(date.format_datetime("%e", &script), "16");
203        assert_eq!(date.format_datetime("%t%n", &script), "\t\n");
204        assert_eq!(date.format_datetime("%Y-%m-%d", &script), "1404-01-16");
205        assert_eq!(date.format_datetime("%x", &script), "1404/01/16");
206        assert_eq!(date.format_datetime("%A", &script), "Shanbeh");
207        assert_eq!(date.format_datetime("%B", &script), "Farvardin");
208
209        Ok(())
210    }
211
212    #[cfg(fake_date)]
213    #[test]
214    fn test_date_format_english() -> Result<(), DateError> {
215        let date = date_now()?;
216        let script = Script::English;
217
218        assert_eq!(date.format_datetime("%A", &script), "Saturday");
219        assert_eq!(date.format_datetime("%B", &script), "Farvardin");
220
221        Ok(())
222    }
223
224    #[cfg(fake_date)]
225    #[test]
226    fn test_date_format_persian() -> Result<(), DateError> {
227        let date = date_now()?;
228        let script = Script::Persian;
229
230        assert_eq!(date.format_datetime("%H:%M:%S", &script), "22:55:30");
231        assert_eq!(date.format_datetime("%Y-%m-%d", &script), "1404-01-16");
232        assert_eq!(date.format_datetime("%x", &script), "1404/01/16");
233        assert_eq!(date.format_datetime("%A", &script), "شنبه");
234        assert_eq!(date.format_datetime("%B", &script), "فروردین");
235
236        Ok(())
237    }
238
239    #[cfg(fake_date)]
240    #[test]
241    fn test_date_helper() -> Result<(), DateError> {
242        let date = date_today()?;
243        let script = Script::Latin;
244
245        assert_eq!(date.day_name_in(&script)?, "Shanbeh");
246
247        assert_eq!(date.weekday_from_saturday()?, 0);
248
249        assert_eq!(date.month_name_in(&script), "Farvardin");
250
251        assert_eq!(date.month_first_day()?, 6); // First day of Farvardin 1404: Jomeh
252
253        let next_month = date.next_month()?;
254        assert_eq!(next_month.month_name_in(&script), "Ordibehesht");
255        assert_eq!(next_month.year(), 1404);
256
257        let prev_month = date.prev_month()?;
258        assert_eq!(prev_month.month_name_in(&script), "Esfand");
259        assert_eq!(prev_month.year(), 1403);
260
261        assert_eq!(date.with_day(31)?.prev_month()?.day(), 30); // Esfand 1403 has 30 days
262
263        Ok(())
264    }
265}