heca_lib/convert/
mod.rs

1use chrono::{DateTime, Duration, Utc};
2
3use crate::prelude::*;
4use serde::ser::SerializeStruct;
5use serde::Serialize;
6use std::num::NonZeroI8;
7
8mod year;
9#[doc(inline)]
10pub use year::*;
11
12#[derive(Debug, Copy, Clone)]
13/// HebrewDate holds a specific Hebrew Date. It can be constructed individually or through HebrewYear.
14pub struct HebrewDate {
15    day: NonZeroI8,
16    month: HebrewMonth,
17    year: HebrewYear,
18}
19impl Serialize for HebrewDate {
20    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
21    where
22        S: serde::ser::Serializer,
23    {
24        let mut state = serializer.serialize_struct("HebrewDate", 3)?;
25        state.serialize_field("day", &self.day.get())?;
26        state.serialize_field("month", &self.month)?;
27        state.serialize_field("year", &self.year())?;
28        state.end()
29    }
30}
31impl Eq for HebrewDate {}
32impl PartialEq for HebrewDate {
33    fn eq(&self, other: &HebrewDate) -> bool {
34        self.day == other.day && self.month == other.month && self.year() == other.year()
35    }
36}
37
38use std::cmp::Ordering;
39impl Ord for HebrewDate {
40    fn cmp(&self, other: &HebrewDate) -> Ordering {
41        if self.year() < other.year() {
42            Ordering::Less
43        } else if self.year() > other.year() {
44            Ordering::Greater
45        } else if (self.month as i32) < (other.month as i32) {
46            Ordering::Less
47        } else if (self.month as i32) > (other.month as i32) {
48            Ordering::Greater
49        } else if self.day < other.day {
50            Ordering::Less
51        } else if self.day > other.day {
52            Ordering::Greater
53        } else {
54            Ordering::Equal
55        }
56    }
57}
58
59impl PartialOrd for HebrewDate {
60    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
61        Some(self.cmp(other))
62    }
63}
64
65impl HebrewDate {
66    /// Returns a HebrewDate on success or a ConversionError on failure.
67    ///
68    /// # Arguments
69    /// * `year` - The Hebrew year since creation.
70    /// * `month` - The Hebrew month.
71    /// * `day` - The Hebrew day of month.
72    ///
73    /// # Error Values
74    /// * `YearTooSmall` - This algorithm won't work if the year is before 3764.
75    /// * `IsLeapYear` - I treat Adar, Adar 1 and Adar 2 as three seperate months, so if you want to
76    /// convert a day in Adar 1 or Adar 2 of a leap year, specify which one.
77    ///  * `IsNotLeapYear` - I treat Adar, Adar 1 and Adar 2 as three seperate months, so it won't
78    ///  make sense to get the English date of the first of Adar 1 or Adar 2 if the year isn't a
79    ///  leap year.
80    ///  * `TooManyDaysInMonth` - There are either 29 or 30 days in a month, so it doesn't make sense
81    ///  to find the 50th day of Nissan.
82    ///
83    /// # Notes:
84    ///
85    /// Day must be above zero. If it's below zero, the function returns TooManyDaysInMonth. In a future release, day will be a NonZeroU8 so that it will be impossible to supply a negative number.
86    pub fn from_ymd(
87        year: u64,
88        month: HebrewMonth,
89        day: NonZeroI8,
90    ) -> Result<HebrewDate, ConversionError> {
91        HebrewYear::new(year)?.get_hebrew_date(month, day)
92    }
93
94    pub(crate) fn from_ymd_internal(
95        month: HebrewMonth,
96        day: NonZeroI8, //TODO: Make NonZeroU8
97        hebrew_year: HebrewYear,
98    ) -> Result<HebrewDate, ConversionError> {
99        if day.get() < 0 {
100            return Err(ConversionError::TooManyDaysInMonth(
101                hebrew_year.sched[month as usize],
102            )); //TODO: Remove when day is NonZeroU8
103        }
104        //Get a HebrewDate object from the Hebrew Year, Month, and Day. Can fail if the year is too
105        //small or the day is less than one.
106        if !hebrew_year.is_leap_year()
107            && (month == HebrewMonth::Adar1 || month == HebrewMonth::Adar2)
108        {
109            return Err(ConversionError::IsNotLeapYear);
110        }
111
112        if hebrew_year.is_leap_year() && month == HebrewMonth::Adar {
113            return Err(ConversionError::IsLeapYear);
114        }
115
116        if day.get() as u8 > hebrew_year.sched[month as usize] {
117            return Err(ConversionError::TooManyDaysInMonth(
118                hebrew_year.sched[month as usize],
119            ));
120        }
121
122        Ok(HebrewDate {
123            month,
124            day,
125            year: hebrew_year,
126        })
127    }
128
129    fn from_gregorian(date: DateTime<Utc>) -> Result<HebrewDate, ConversionError> {
130        if date < *crate::convert::year::backend::FIRST_RH + Duration::days(2 + 365) {
131            return Err(ConversionError::YearTooSmall);
132        }
133        let days_since_first_rh =
134            ((date - *crate::convert::year::backend::FIRST_RH).num_days() + 2) as u64;
135
136        let hebrew_year = HebrewYear::new(crate::convert::year::backend::day_of_last_rh(
137            days_since_first_rh,
138        ))
139        .unwrap();
140        Ok(hebrew_year.get_hebrewdate_from_days_after_rh(days_since_first_rh))
141    }
142
143    pub(crate) fn to_gregorian(&self) -> DateTime<Utc> {
144        let amnt_days_between_rh_and_epoch = self.year.days_since_epoch;
145        let sched = self.year.sched;
146        let mut amnt_days_in_month: u16 = 0;
147        if self.month != HebrewMonth::Tishrei {
148            for item in sched.iter().take(self.month as usize) {
149                amnt_days_in_month += u16::from(*item);
150            }
151        }
152
153        let amnt_days =
154            amnt_days_between_rh_and_epoch + u64::from(amnt_days_in_month) + self.day.get() as u64
155                - 1;
156        *crate::convert::year::backend::EPOCH + Duration::days(amnt_days as i64)
157    }
158    ///Get the Hebrew day of month.
159    #[inline]
160    pub fn day(&self) -> NonZeroI8 {
161        self.day
162    }
163
164    ///Get the Hebrew month of year
165    #[inline]
166    pub fn month(&self) -> HebrewMonth {
167        self.month
168    }
169
170    ///Get the Hebrew year.
171
172    #[inline]
173    pub fn year(&self) -> u64 {
174        self.year.year
175    }
176}
177
178mod tests {
179    #[test]
180    fn get_year() {
181        use super::*;
182        use chrono::prelude::*;
183        for j in 0..100 {
184            let mut original_day = Utc.ymd(16 + j, 10, 4).and_hms(18, 0, 0);
185            for _i in 1..366 {
186                let h_day = HebrewDate::from_gregorian(original_day).unwrap();
187                let ne_day = h_day.to_gregorian();
188                assert_eq!(original_day, ne_day);
189                original_day = original_day + Duration::days(1);
190            }
191        }
192    }
193
194    #[test]
195    fn from_ymd_negative() {
196        use crate::prelude::*;
197        use crate::HebrewYear;
198        use std::num::NonZeroI8;
199        let a: ConversionError = HebrewYear::new(5779)
200            .unwrap()
201            .get_hebrew_date(HebrewMonth::Shvat, NonZeroI8::new(-1).unwrap())
202            .unwrap_err();
203        if let ConversionError::TooManyDaysInMonth(_) = a {
204        } else {
205            panic!("A is not a TooManyDaysInMonth");
206        }
207    }
208}