ptime/
lib.rs

1// In the name of Allah
2
3//! Provides functionality for conversion among Persian (Solar Hijri) and Gregorian calendars.
4//! A Julian calendar is used as an interface for all conversions.
5//! The crate name is ptime and it is compatible with the crate [time](https://crates.io/crates/time).
6//! This source code is licensed under MIT license that can be found in the LICENSE file.
7//!
8//! # Example
9//! ```
10//! extern crate ptime;
11//!
12//! fn main() {
13//!     let p_tm = ptime::from_gregorian_date(2016, 2, 21).unwrap();
14//!
15//!     assert_eq!(p_tm.tm_year, 1395);
16//!     assert_eq!(p_tm.tm_mon, 0);
17//!     assert_eq!(p_tm.tm_mday, 2);
18//! }
19//! ```
20
21extern crate time;
22
23use std::cmp::Ordering;
24use std::ops::{Add, Sub};
25use std::fmt;
26
27/// Represents the components of a moment in time in Persian Calendar.
28#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
29#[cfg_attr(feature = "rustc-serialize", derive(RustcEncodable, RustcDecodable))]
30pub struct Tm {
31    /// The same as `tm_sec` of `time::Tm`
32    pub tm_sec: i32,
33
34    /// The same as `tm_min` of `time::Tm`
35    pub tm_min: i32,
36
37    /// The same as `tm_hour` of `time::Tm`
38    pub tm_hour: i32,
39
40    /// MonthDay - [1, 31]
41    pub tm_mday: i32,
42
43    /// Month since Farvardin - [0, 11]
44    pub tm_mon: i32,
45
46    /// Year
47    pub tm_year: i32,
48
49    /// Weekday since Shanbe - [0, 6]. 0 = Shanbeh, ..., 6 = Jomeh.
50    pub tm_wday: i32,
51
52    /// YearDay since Farvardin 1 - [0, 365]
53    pub tm_yday: i32,
54
55    /// The same as `tm_isdst` of `time::Tm`
56    pub tm_isdst: i32,
57
58    /// The same as `tm_utcoff` of `time::Tm`
59    pub tm_utcoff: i32,
60
61    /// The same as `tm_nsec` of `time::Tm`
62    pub tm_nsec: i32,
63}
64
65impl fmt::Display for Tm {
66    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
67        write!(f, "{}", self.to_string("yyyy-MM-ddTHH:mm:ss.ns"))
68    }
69}
70
71impl Add<time::Duration> for Tm {
72    type Output = Tm;
73
74    // FIXME: The timezone of `self` is different from resulting time
75    fn add(self, other: time::Duration) -> Tm {
76        at_utc(self.to_timespec() + other)
77    }
78}
79
80impl Sub<time::Duration> for Tm {
81    type Output = Tm;
82
83    // FIXME: The timezone of `self` is different from resulting time
84    fn sub(self, other: time::Duration) -> Tm {
85        at_utc(self.to_timespec() - other)
86    }
87}
88
89impl Sub<Tm> for Tm {
90    type Output = time::Duration;
91
92    fn sub(self, other: Tm) -> time::Duration {
93        self.to_timespec() - other.to_timespec()
94    }
95}
96
97impl Sub<time::Tm> for Tm {
98    type Output = time::Duration;
99
100    fn sub(self, other: time::Tm) -> time::Duration {
101        self.to_timespec() - other.to_timespec()
102    }
103}
104
105impl PartialOrd for Tm {
106    fn partial_cmp(&self, other: &Tm) -> Option<Ordering> {
107        self.to_timespec().partial_cmp(&other.to_timespec())
108    }
109}
110
111impl Ord for Tm {
112    fn cmp(&self, other: &Tm) -> Ordering {
113        self.to_timespec().cmp(&other.to_timespec())
114    }
115}
116
117impl Tm {
118    /// Converts Persian calendar to Gregorian calendar
119    pub fn to_gregorian(&self) -> time::Tm {
120        let year: i32;
121        let month: i32;
122        let day: i32;
123
124        let jdn = get_jdn(self.tm_year, self.tm_mon + 1, self.tm_mday);
125
126        if jdn > 2299160 {
127            let mut l = jdn + 68569;
128            let n = 4 * l / 146097;
129            l = l - (146097 * n + 3) / 4;
130            let i = 4000 * (l + 1) / 1461001;
131            l = l - 1461 * i / 4 + 31;
132            let j = 80 * l / 2447;
133            day = l - 2447 * j / 80;
134            l = j / 11;
135            month = j + 2 - 12 * l;
136            year = 100 * (n - 49) + i + l;
137        } else {
138            let mut j = jdn + 1402;
139            let k = (j - 1) / 1461;
140            let l = j - 1461 * k;
141            let n = (l - 1) / 365 - l / 1461;
142            let mut i = l - 365 * n + 30;
143            j = 80 * i / 2447;
144            day = i - 2447 * j / 80;
145            i = j / 11;
146            month = j + 2 - 12 * i;
147            year = 4 * k + n + i - 4716;
148        }
149
150        time::Tm {
151            tm_sec: self.tm_sec,
152            tm_min: self.tm_min,
153            tm_hour: self.tm_hour,
154            tm_mday: day,
155            tm_mon: month - 1,
156            tm_year: year - 1900,
157            tm_wday: get_gregorian_weekday(self.tm_wday),
158            tm_yday: get_gregorian_yday(year, month - 1, day),
159            tm_isdst: self.tm_isdst,
160            tm_utcoff: self.tm_utcoff,
161            tm_nsec: self.tm_nsec,
162        }
163    }
164
165    /// Returns the number of seconds since January 1, 1970 UTC
166    pub fn to_timespec(&self) -> time::Timespec {
167        self.to_gregorian().to_timespec()
168    }
169
170    /// Returns true if the year is a leap year
171    pub fn is_leap(&self) -> bool {
172        is_persian_leap(self.tm_year)
173    }
174
175    /// Convert time to the local timezone
176    pub fn to_local(&self) -> Tm {
177        match self.tm_utcoff {
178            0 => at(self.to_timespec()),
179            _ => *self
180        }
181    }
182
183    /// Convert time to the UTC
184    pub fn to_utc(&self) -> Tm {
185        match self.tm_utcoff {
186            0 => *self,
187            _ => at_utc(self.to_timespec())
188        }
189    }
190
191    /// Returns the formatted representation of time
192    ///     yyyy, yyy, y     year (e.g. 1394)
193    ///     yy               2-digits representation of year (e.g. 94)
194    ///     MMM              the Persian name of month (e.g. فروردین)
195    ///     MM               2-digits representation of month (e.g. 01)
196    ///     M                month (e.g. 1)
197    ///     DD               day of year (starting from 1)
198    ///     D                day of year (starting from 0)
199    ///     dd               2-digits representation of day (e.g. 01)
200    ///     d                day (e.g. 1)
201    ///     E                the Persian name of weekday (e.g. شنبه)
202    ///     e                the Persian short name of weekday (e.g. ش)
203    ///     A                the Persian name of 12-Hour marker (e.g. قبل از ظهر)
204    ///     a                the Persian short name of 12-Hour marker (e.g. ق.ظ)
205    ///     HH               2-digits representation of hour [00-23]
206    ///     H                hour [0-23]
207    ///     kk               2-digits representation of hour [01-24]
208    ///     k                hour [1-24]
209    ///     hh               2-digits representation of hour [01-12]
210    ///     h                hour [1-12]
211    ///     KK               2-digits representation of hour [00-11]
212    ///     K                hour [0-11]
213    ///     mm               2-digits representation of minute [00-59]
214    ///     m                minute [0-59]
215    ///     ss               2-digits representation of seconds [00-59]
216    ///     s                seconds [0-59]
217    ///     ns               nanoseconds
218    pub fn to_string<'a>(&'a self, format: &'a str) -> String {
219        format
220            .replace("yyyy", &self.tm_year.to_string())
221            .replace("yyy", &self.tm_year.to_string())
222            .replace("yy", &self.tm_year.to_string()[2..])
223            .replace("y", &self.tm_year.to_string())
224            .replace("MMM", match self.tm_mon {
225                                0 => "فروردین",
226                                1 => "اردیبهشت",
227                                2 => "خرداد",
228                                3 => "تیر",
229                                4 => "مرداد",
230                                5 => "شهریور",
231                                6 => "مهر",
232                                7 => "آبان",
233                                8 => "آذر",
234                                9 => "دی",
235                                10 => "بهمن",
236                                11 => "اسفند",
237                                _ => panic!("invalid month value of {}", self.tm_mon),
238                            })
239            .replace("MM", &format!("{:02}", self.tm_mon + 1))
240            .replace("M", &format!("{}", self.tm_mon + 1))
241            .replace("DD", &format!("{}", self.tm_yday + 1))
242            .replace("D", &self.tm_yday.to_string())
243            .replace("dd", &format!("{:02}", self.tm_mday))
244            .replace("d", &self.tm_mday.to_string())
245            .replace("E", match self.tm_wday {
246                              0 => "شنبه",
247                              1 => "یک‌شنبه",
248                              2 => "دوشنبه",
249                              3 => "سه‌شنبه",
250                              4 => "چهارشنبه",
251                              5 => "پنج‌شنبه",
252                              6 => "جمعه",
253                              _ => panic!("invalid weekday value of {}", self.tm_wday),
254                          })
255            .replace("e", match self.tm_wday {
256                              0 => "ش",
257                              1 => "ی",
258                              2 => "د",
259                              3 => "س",
260                              4 => "چ",
261                              5 => "پ",
262                              6 => "ج",
263                              _ => panic!("invalid weekday value of {}", self.tm_wday),
264                          })
265            .replace("A", if self.tm_hour < 12 {
266                              "قبل از ظهر"
267                          } else {
268                              "بعد از ظهر"
269                          })
270            .replace("a", if self.tm_hour < 12 {
271                              "ق.ظ"
272                          } else {
273                              "ب.ظ"
274                          })
275            .replace("HH", &format!("{:02}", self.tm_hour))
276            .replace("H", &self.tm_hour.to_string())
277            .replace("kk", &format!("{:02}", self.tm_hour + 1))
278            .replace("k", &format!("{}", self.tm_hour + 1))
279            .replace("hh", &format!("{:02}", if self.tm_hour > 11 {
280                                                 self.tm_hour - 12
281                                             } else {
282                                                 self.tm_hour
283                                             } + 1))
284            .replace("h", &format!("{}", if self.tm_hour > 11 {
285                                             self.tm_hour - 12
286                                         } else {
287                                             self.tm_hour
288                                         } + 1))
289            .replace("KK", &format!("{:02}", if self.tm_hour > 11 {
290                                                 self.tm_hour - 12
291                                             } else {
292                                                 self.tm_hour
293                                             }))
294            .replace("K", &format!("{}", if self.tm_hour > 11 {
295                                             self.tm_hour - 12
296                                         } else {
297                                             self.tm_hour
298                                         }))
299            .replace("mm", &format!("{:02}", self.tm_min))
300            .replace("m", &self.tm_min.to_string())
301            .replace("ns", &self.tm_nsec.to_string())
302            .replace("ss", &format!("{:02}", self.tm_sec))
303            .replace("s", &self.tm_sec.to_string())
304    }
305}
306
307/// Creates an empty `ptime::Tm`
308pub fn empty_tm() -> Tm {
309    Tm {
310        tm_sec: 0,
311        tm_min: 0,
312        tm_hour: 0,
313        tm_mday: 0,
314        tm_mon: 0,
315        tm_year: 0,
316        tm_wday: 0,
317        tm_yday: 0,
318        tm_isdst: 0,
319        tm_utcoff: 0,
320        tm_nsec: 0,
321    }
322}
323
324/// Converts Gregorian calendar to Persian calendar
325pub fn from_gregorian(gregorian_tm:time::Tm) -> Tm {
326    let mut year: i32;
327    let gy = gregorian_tm.tm_year + 1900;
328    let gm = gregorian_tm.tm_mon + 1;
329    let gd = gregorian_tm.tm_mday;
330
331    let jdn: i32 = if gy > 1582 || (gy == 1582 && gm > 10) || (gy == 1582 && gm == 10 && gd > 14) {
332        ((1461 * (gy + 4800 + ((gm - 14) / 12))) / 4) + ((367 * (gm - 2 - 12*((gm-14)/12))) / 12) - ((3 * ((gy + 4900 + ((gm - 14) / 12)) / 100)) / 4) + gd - 32075
333    } else {
334        367 * gy - ((7 * (gy + 5001 + ((gm - 9) / 7))) / 4) + ((275 * gm) / 9) + gd + 1729777
335    };
336
337    let dep = jdn - get_jdn(475, 1, 1);
338    let cyc = dep / 1029983;
339    let rem = dep % 1029983;
340    let ycyc = if rem == 1029982 {
341        2820
342    } else {
343        let a = rem / 366;
344        (2134 * a + 2816 * (rem % 366) + 2815) / 1028522 + a + 1
345    };
346
347    year = ycyc + 2820 * cyc + 474;
348    if year <= 0 {
349        year -= 1;
350    }
351
352    let dy: f64 = (jdn - get_jdn(year, 1, 1) + 1) as f64;
353    let month: i32 = if dy <= 186f64 {
354        let mod_dy: f64 = dy / 31f64;
355        mod_dy.ceil() as i32
356    } else {
357        let mod_dy: f64 = (dy - 6f64) / 30f64;
358        mod_dy.ceil() as i32
359    } - 1;
360    let day = jdn - get_jdn(year, month + 1, 1) + 1;
361
362    Tm {
363        tm_sec: gregorian_tm.tm_sec,
364        tm_min: gregorian_tm.tm_min,
365        tm_hour: gregorian_tm.tm_hour,
366        tm_mday: day,
367        tm_mon: month,
368        tm_year: year,
369        tm_wday: get_persian_weekday(gregorian_tm.tm_wday),
370        tm_yday: get_persian_yday(month, day),
371        tm_isdst: gregorian_tm.tm_isdst,
372        tm_utcoff: gregorian_tm.tm_utcoff,
373        tm_nsec: gregorian_tm.tm_nsec,
374    }
375}
376
377/// Creates a new instance of Persian time from Gregorian date
378pub fn from_gregorian_date(g_year: i32, g_month: i32, g_day: i32) -> Option<Tm> {
379    from_gregorian_components(g_year, g_month, g_day, 0, 0, 0, 0)
380}
381
382/// Creates a new instance of Persian time from Persian date
383pub fn from_persian_date(p_year: i32, p_month: i32, p_day: i32) -> Option<Tm> {
384    from_persian_components(p_year, p_month, p_day, 0, 0, 0, 0)
385}
386
387/// Creates a new instance of Persian time from Gregorian date components
388pub fn from_gregorian_components(g_year: i32, g_month: i32, g_day: i32, hour: i32, minute: i32, second: i32, nanosecond: i32) -> Option<Tm> {
389    if is_time_valid(hour, minute, second, nanosecond) && is_gregorian_date_valid(g_year, g_month, g_day) {
390        let tm = time::Tm{
391            tm_sec: second,
392            tm_min: minute,
393            tm_hour: hour,
394            tm_mday: g_day,
395            tm_mon: g_month,
396            tm_year: g_year - 1900,
397            tm_wday: 0,
398            tm_yday: 0,
399            tm_isdst: 0,
400            tm_utcoff: 0,
401            tm_nsec: nanosecond,
402        };
403        return Some(at_utc(tm.to_timespec()))
404    }
405    None
406}
407
408/// Creates a new instance of Persian time from Persian date components
409// FIXME: Calculate the weekday without converting to Gregorian calendar
410pub fn from_persian_components(p_year: i32, p_month: i32, p_day: i32, hour: i32, minute: i32, second: i32, nanosecond: i32) -> Option<Tm> {
411    if is_time_valid(hour, minute, second, nanosecond) && is_persian_date_valid(p_year, p_month, p_day) {
412        let mut tm = Tm{
413            tm_sec: second,
414            tm_min: minute,
415            tm_hour: hour,
416            tm_mday: p_day,
417            tm_mon: p_month,
418            tm_year: p_year,
419            tm_wday: 0,
420            tm_yday: get_persian_yday(p_month, p_day),
421            tm_isdst: 0,
422            tm_utcoff: 0,
423            tm_nsec: nanosecond,
424        };
425        tm.tm_wday = get_persian_weekday(time::at_utc(tm.to_timespec()).tm_wday);
426        return Some(tm)
427    }
428    None
429}
430
431/// Creates a new instance of Persian time from the number of seconds since January 1, 1970 in UTC
432pub fn at_utc(clock: time::Timespec) -> Tm {
433    from_gregorian(time::at_utc(clock))
434}
435
436/// Creates a new instance of Persian time from the number of seconds since January 1, 1970 in the local timezone
437pub fn at(clock: time::Timespec) -> Tm {
438    from_gregorian(time::at(clock))
439}
440
441/// Creates a new instance of Persian time corresponding to the current time in UTC
442pub fn now_utc() -> Tm {
443    from_gregorian(time::now_utc())
444}
445
446/// Creates a new instance of Persian time corresponding to the current time in the local timezone
447pub fn now() -> Tm {
448    from_gregorian(time::now())
449}
450
451fn divider(num: i32, den: i32) -> i32 {
452    if num > 0 {
453        num % den
454    } else {
455        num - ((((num + 1) / den) - 1) * den)
456    }
457}
458
459fn get_jdn(year: i32, month: i32, day: i32) -> i32 {
460    let base = if year >= 0 {
461        year - 474
462    } else {
463        year - 473
464    };
465
466    let epy = 474 + (base % 2820);
467
468    let md = if month <= 7 {
469        (month - 1) * 31
470    } else {
471        (month - 1) * 30 + 6
472    };
473
474    day + md + (epy * 682 - 110) / 2816 + (epy - 1) * 365 + base / 2820 * 1029983 + 1948320
475}
476
477fn get_persian_weekday(wd: i32) -> i32 {
478    match wd {
479        0 => 1,
480        1 => 2,
481        2 => 3,
482        3 => 4,
483        4 => 5,
484        5 => 6,
485        6 => 0,
486        _ => panic!("invalid weekday value of {}", wd),
487    }
488}
489
490fn get_gregorian_weekday(wd: i32) -> i32 {
491    match wd {
492        0 => 6,
493        1 => 0,
494        2 => 1,
495        3 => 2,
496        4 => 3,
497        5 => 4,
498        6 => 5,
499        _ => panic!("invalid weekday value of {}", wd),
500    }
501}
502
503fn get_persian_yday(month: i32, day: i32) -> i32 {
504    [
505        0,   // Farvardin
506        31,  // Ordibehesht
507        62,  // Khordad
508        93,  // Tir
509        124, // Mordad
510        155, // Shahrivar
511        186, // Mehr
512        216, // Aban
513        246, // Azar
514        276, // Dey
515        306, // Bahman
516        336, // Esfand
517    ][month as usize] + day - 1
518}
519
520fn get_gregorian_yday(year: i32, month: i32, day: i32) -> i32 {
521    [
522        [0, 0],
523        [31, 31],
524        [59, 60],
525        [90, 91],
526        [120, 121],
527        [151, 152],
528        [181, 182],
529        [212, 213],
530        [243, 244],
531        [273, 274],
532        [304, 305],
533        [334, 335],
534    ][month as usize][is_gregorian_leap(year) as usize] + day - 1
535}
536
537fn is_persian_leap(year: i32) -> bool {
538    divider(25 * year + 11, 33) < 8
539}
540
541fn is_gregorian_leap(year: i32) -> bool {
542    year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
543}
544
545fn is_persian_date_valid(year: i32, month: i32, day: i32) -> bool {
546    if month < 0 || month > 11 {
547        return false
548    }
549
550    [
551        [31, 31],
552        [31, 31],
553        [31, 31],
554        [31, 31],
555        [31, 31],
556        [31, 31],
557        [30, 30],
558        [30, 30],
559        [30, 30],
560        [30, 30],
561        [30, 30],
562        [29, 30],
563    ][month as usize][is_persian_leap(year) as usize] >= day
564}
565
566fn is_gregorian_date_valid(year: i32, month: i32, day: i32) -> bool {
567    if month < 0 || month > 11 {
568        return false
569    }
570
571    [
572        [31, 31],
573        [28, 29],
574        [31, 31],
575        [30, 30],
576        [31, 31],
577        [30, 30],
578        [31, 31],
579        [31, 31],
580        [30, 30],
581        [31, 31],
582        [30, 30],
583        [31, 31],
584    ][month as usize][is_gregorian_leap(year) as usize] >= day
585}
586
587fn is_time_valid(hour: i32, minute: i32, second: i32, nanosecond: i32) -> bool {
588    !(hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59 || nanosecond < 0 || nanosecond > 999999999)
589}