1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286
use chrono::prelude::*; use time::Duration; use crate::prelude::*; use serde::ser::SerializeStruct; use serde::Serialize; mod year; #[doc(inline)] pub use year::*; #[derive(Debug, Copy, Clone)] pub struct HebrewDate { day: u8, month: HebrewMonth, year: HebrewYear, } impl Serialize for HebrewDate { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::ser::Serializer, { let mut state = serializer.serialize_struct("HebrewDate", 3)?; state.serialize_field("day", &self.day)?; state.serialize_field("month", &self.month)?; state.serialize_field("year", &self.year())?; state.end() } } impl Eq for HebrewDate {} impl PartialEq for HebrewDate { fn eq(&self, other: &HebrewDate) -> bool { self.day == other.day && self.month == other.month && self.year() == other.year() } } use std::cmp::Ordering; impl Ord for HebrewDate { fn cmp(&self, other: &HebrewDate) -> Ordering { if self.year() < other.year() { Ordering::Less } else if self.year() > other.year() { Ordering::Greater } else if (self.month as i32) < (other.month as i32) { Ordering::Less } else if (self.month as i32) > (other.month as i32) { Ordering::Greater } else if self.day < other.day { Ordering::Less } else if self.day > other.day { Ordering::Greater } else { Ordering::Equal } } } impl PartialOrd for HebrewDate { fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) } } impl HebrewDate { /// Returns a HebrewDate on success, or a ConversionError on failure. /// /// # Arguments /// * `year` - The Hebrew year since creation. /// * `month` - The Hebrew month. /// * `day` - The Hebrew day of month. /// /// # Error Values /// * `YearTooSmall` - This algorithm won't work if the year is before 3764. /// * `DayIsZero` - Months start with day 1, not zero. So 0 Adar won't work. /// * `IsLeapYear` - I treat Adar, Adar 1 and Adar 2 as three seperate months, so if you want to /// convert a day in Adar 1 or Adar 2 of a leap year, specify which one. /// * `IsNotLeapYear` - I treat Adar, Adar 1 and Adar 2 as three seperate months, so it won't /// make sense to get the English date of the first of Adar 1 or Adar 2 if the year isn't a /// leap year. /// * `TooManyDaysInMonth` - There are either 29 or 30 days in a month, so it doesn't make sense /// to find the 50th day of Nissan. pub fn from_ymd(year: u64, month: HebrewMonth, day: u8) -> Result<HebrewDate, ConversionError> { HebrewYear::new(year)?.get_hebrew_date(month, day) } pub fn from_ymd_unchecked(year: u64, month: HebrewMonth, day: u8) -> HebrewDate { HebrewYear::new(year) .unwrap() .get_hebrew_date(month, day) .unwrap() } pub(crate) fn from_ymd_internal( month: HebrewMonth, day: u8, hebrew_year: HebrewYear, ) -> Result<HebrewDate, ConversionError> { //Get a HebrewDate object from the Hebrew Year, Month, and Day. Can fail if the year is too //small or the day is less than one. if day == 0 { return Err(ConversionError::DayIsZero); } if !hebrew_year.is_leap_year() && (month == HebrewMonth::Adar1 || month == HebrewMonth::Adar2) { return Err(ConversionError::IsNotLeapYear); } if hebrew_year.is_leap_year() && month == HebrewMonth::Adar { return Err(ConversionError::IsLeapYear); } if day > hebrew_year.sched[month as usize] { return Err(ConversionError::TooManyDaysInMonth( hebrew_year.sched[month as usize], )); } Ok(HebrewDate { month, day: day as u8, year: hebrew_year, }) } /// Returns a HebrewDate on success, or a ConversionError on failure. /// /// # Arguments /// * `date` - The Gregorian date. /// /// # Notes: /// Hebrew days start at sundown, not midnight, so there isn't a full 1:1 mapping between /// Gregorian days and Hebrew. So when you look up the date of Rosh Hashana 5779, you'll get "Monday, 10th of September 2018", while Rosh Hashana really started at sundown on the 9th of September. /// /// I'm trying to be a _bit_ more precise, so I made the date cutoff at 6:00 PM. So for /// example: /// ``` /// extern crate heca_lib; /// /// use chrono::Utc; /// use chrono::offset::TimeZone; /// use heca_lib::prelude::*; /// use heca_lib::HebrewDate; /// /// assert_eq!(HebrewDate::from_gregorian(Utc.ymd(2018,9,10).and_hms(17,59,59)).unwrap(),HebrewDate::from_ymd(5779,HebrewMonth::Tishrei,1).unwrap()); /// ``` /// /// while /// /// ``` /// extern crate heca_lib; /// /// use chrono::Utc; /// use chrono::offset::TimeZone; /// use heca_lib::prelude::*; /// use heca_lib::HebrewDate; /// /// assert_eq!(HebrewDate::from_gregorian(Utc.ymd(2018,9,10).and_hms(18,0,0)).unwrap(),HebrewDate::from_ymd(5779,HebrewMonth::Tishrei,2).unwrap()); /// ``` /// # Error Values: /// * YearTooSmall - This algorithm won't work if the year is before year 4. /// pub fn from_gregorian(date: DateTime<Utc>) -> Result<HebrewDate, ConversionError> { if date < *crate::convert::year::backend::FIRST_RH + Duration::days(2 + 365) { return Err(ConversionError::YearTooSmall); } let days_since_first_rh = ((date - *crate::convert::year::backend::FIRST_RH).num_days() + 2) as u64; let hebrew_year = HebrewYear::new(crate::convert::year::backend::day_of_last_rh( days_since_first_rh, )) .unwrap(); Ok(hebrew_year.get_hebrewdate_from_days_after_rh(days_since_first_rh)) } /// Gets the Gregorian date for the current Hebrew date. /// /// # Notes /// /// This function returns the DateTime of the given HebrewDate at nightfall. /// /// For example, Yom Kippur 5779 started at sunset of September 18, 2018. So /// ``` /// extern crate heca_lib; /// /// use chrono::Utc; /// use chrono::offset::TimeZone; /// use heca_lib::prelude::*; /// use heca_lib::HebrewDate; /// /// assert_eq!(HebrewDate::from_ymd(5779,HebrewMonth::Tishrei,10).unwrap().to_gregorian(),Utc.ymd(2018, 9,18).and_hms(18,00,00)); /// ``` /// ## Algorithm: /// The conversion is done (at the moment) according to the calculation of the Rambam (Maimonidies), as is documented in [Hilchos Kiddush Ha'chodesh](https://www.sefaria.org/Mishneh_Torah%2C_Sanctification_of_the_New_Month.6.1?lang=bi&with=all&lang2=en). /// /// The algorithm is as follows: /// /// 1. There are exactly 1080 Chalakim (parts) in an hour. /// 2. There are exactly (well, not really. But it's close enough that we use that number as exact.) 29 days, 12 hours, and 793 Chalakim between new moons. /// /// So that's the basic numbers. Regarding the calendar itself: /// /// 3. All months are either 29 or 30 days long. /// 4. There are either 12 or 13 months in the Hebrew calendar, depending if it's a leap year. When it's a leap year, Adar (which generally is in the late winter or early spring) is doubled into a "first Adar" (Adar1) and a "second Adar" (Adar2). /// 5. There is a 19 year cycle of leap years. So the first two years of the cycle are regular years, the one after that's a leap year. Then another two are regular, then a leap year. Then it's regular, leap, regular, regular, leap, regular, regular, leap. /// 6. Year 3763 was the first year of its 19 year cycle. /// 7. Now you can calculate when's the New Moon before a given Rosh Hashana. /// /// So how to calculate Rosh Hashana: /// /// 8. If the New Moon is in the afternoon, Rosh Hashana is postponed to the next day. /// 9. If Rosh Hashana's starting on a Sunday (Saturday night), Wednesday (Tuesday night), or Friday (Thursday night) - postpone it by a day. /// /// If any of the above two conditions were fulfilled. Good. You just found Rosh Hashana. If not: /// /// 10. If the New Moon is on a Tuesday after 3am+204 Chalakim and the coming year is not a leap year, Rosh Hashana is postponed to that upcoming Thursday instead. /// 11. If the New Moon is on a Monday after 9am+589 Chalakim, and the previous year was a leap year, then Rosh Hashana is postponed to Tuesday. /// /// /// Now you have all the Rosh Hashanas. /// /// 12. In general, months alternate between “Full” (30 days long) and “Empty” (29 days long) months. So Tishrei is full, Teves is empty, Shvat is full, Adar is empty, Nissan is full. /// 13. When the year is a leap year, Adar 1 is full and Adar 2 is empty. (So a full Shvat is followed by a full Adar1). /// /// Knowing this, you can calculate any other date of the year. /// /// But wait! We're playing with the date when Rosh Hashana will start, so not every year will be the same length! How do we make up these days? /// /// So there's a last little bit: /// /// 14. Cheshvan and Kislev are variable length months – some years both are full, some years both are empty, and some years Cheshvan is full and Kislev is empty - depending on the day Rosh Hashana starts (and the day _the next Rosh Hashana starts_) and how many days are in the year. pub fn to_gregorian(&self) -> chrono::DateTime<Utc> { let amnt_days_between_rh_and_epoch = self.year.days_since_epoch; let sched = self.year.sched; let mut amnt_days_in_month: u16 = 0; if self.month != HebrewMonth::Tishrei { for item in sched.iter().take(self.month as usize) { amnt_days_in_month += u16::from(*item); } } let amnt_days = amnt_days_between_rh_and_epoch + u64::from(amnt_days_in_month) + u64::from(self.day) - 1; *crate::convert::year::backend::EPOCH + Duration::days(amnt_days as i64) } ///Get the Hebrew day of month. #[inline] pub fn day(&self) -> u8 { self.day } ///Get the Hebrew month of year #[inline] pub fn month(&self) -> HebrewMonth { self.month } ///Get the Hebrew year. #[inline] pub fn year(&self) -> u64 { self.year.year } } mod tests { #[test] fn get_year() { use super::*; for j in 0..100 { let mut original_day = Utc.ymd(16 + j, 10, 4).and_hms(18, 0, 0); for _i in 1..366 { let h_day = HebrewDate::from_gregorian(original_day).unwrap(); let ne_day = h_day.to_gregorian(); assert_eq!(original_day, ne_day); original_day = original_day + Duration::days(1); } } } }