1use crate::calendar_arithmetic::{ArithmeticDate, DateFieldsResolver, ToExtendedYear};
6use crate::error::{
7 DateError, DateFromFieldsError, EcmaReferenceYearError, MonthCodeError, UnknownEraError,
8};
9use crate::options::{DateAddOptions, DateDifferenceOptions};
10use crate::options::{DateFromFieldsOptions, Overflow};
11use crate::types::{DateFields, MonthInfo, ValidMonthCode};
12use crate::RangeError;
13use crate::{types, Calendar, Date};
14use ::tinystr::tinystr;
15use calendrical_calculations::hebrew_keviyah::{Keviyah, YearInfo};
16use calendrical_calculations::rata_die::RataDie;
17
18#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord, Default)]
52#[allow(clippy::exhaustive_structs)] pub struct Hebrew;
54
55#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
57pub struct HebrewDateInner(ArithmeticDate<Hebrew>);
58
59impl Hebrew {
60 pub fn new() -> Self {
62 Hebrew
63 }
64}
65
66#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
67pub(crate) struct HebrewYearInfo {
68 keviyah: Keviyah,
69 value: i32,
70}
71
72impl ToExtendedYear for HebrewYearInfo {
73 fn to_extended_year(&self) -> i32 {
74 self.value
75 }
76}
77
78impl HebrewYearInfo {
79 #[inline]
82 fn compute(value: i32) -> Self {
83 Self {
84 keviyah: YearInfo::compute_for(value).keviyah,
85 value,
86 }
87 }
88}
89
90impl DateFieldsResolver for Hebrew {
91 type YearInfo = HebrewYearInfo;
92 fn days_in_provided_month(info: HebrewYearInfo, ordinal_month: u8) -> u8 {
93 info.keviyah.month_len(ordinal_month)
94 }
95
96 fn months_in_provided_year(info: HebrewYearInfo) -> u8 {
97 if info.keviyah.is_leap() {
98 13
99 } else {
100 12
101 }
102 }
103
104 #[inline]
105 fn year_info_from_era(
106 &self,
107 era: &[u8],
108 era_year: i32,
109 ) -> Result<Self::YearInfo, UnknownEraError> {
110 match era {
111 b"am" => Ok(HebrewYearInfo::compute(era_year)),
112 _ => Err(UnknownEraError),
113 }
114 }
115
116 #[inline]
117 fn year_info_from_extended(&self, extended_year: i32) -> Self::YearInfo {
118 HebrewYearInfo::compute(extended_year)
119 }
120
121 fn reference_year_from_month_day(
122 &self,
123 month_code: types::ValidMonthCode,
124 day: u8,
125 ) -> Result<Self::YearInfo, EcmaReferenceYearError> {
126 let hebrew_year = match month_code.to_tuple() {
128 (1, false) => 5733,
129 (2, false) => match day {
130 ..=29 => 5733,
132 _ => 5732,
135 },
136 (3, false) => match day {
137 ..=29 => 5733,
139 _ => 5732,
140 },
141 (4, false) => match day {
142 ..=26 => 5733,
143 _ => 5732,
144 },
145 (5..=12, false) => 5732,
146 (5, true) => 5730,
148 _ => {
149 return Err(EcmaReferenceYearError::MonthCodeNotInCalendar);
150 }
151 };
152 Ok(HebrewYearInfo::compute(hebrew_year))
153 }
154
155 fn ordinal_month_from_code(
156 &self,
157 year: &Self::YearInfo,
158 month_code: types::ValidMonthCode,
159 options: DateFromFieldsOptions,
160 ) -> Result<u8, MonthCodeError> {
161 let is_leap_year = year.keviyah.is_leap();
162 let ordinal_month = match month_code.to_tuple() {
163 (n @ 1..=12, false) => n + (n >= 6 && is_leap_year) as u8,
164 (5, true) => {
165 if is_leap_year {
166 6
167 } else if matches!(options.overflow, Some(Overflow::Constrain)) {
168 6
170 } else {
171 return Err(MonthCodeError::NotInYear);
172 }
173 }
174 _ => return Err(MonthCodeError::NotInCalendar),
175 };
176 Ok(ordinal_month)
177 }
178
179 fn month_code_from_ordinal(
180 &self,
181 year: &Self::YearInfo,
182 ordinal_month: u8,
183 ) -> types::ValidMonthCode {
184 let is_leap = year.keviyah.is_leap();
185 ValidMonthCode::new_unchecked(
186 ordinal_month - (is_leap && ordinal_month >= 6) as u8,
187 ordinal_month == 6 && is_leap,
188 )
189 }
190}
191
192impl crate::cal::scaffold::UnstableSealed for Hebrew {}
193impl Calendar for Hebrew {
194 type DateInner = HebrewDateInner;
195 type Year = types::EraYear;
196 type DifferenceError = core::convert::Infallible;
197
198 fn from_codes(
199 &self,
200 era: Option<&str>,
201 year: i32,
202 month_code: types::MonthCode,
203 day: u8,
204 ) -> Result<Self::DateInner, DateError> {
205 ArithmeticDate::from_codes(era, year, month_code, day, self).map(HebrewDateInner)
206 }
207
208 #[cfg(feature = "unstable")]
209 fn from_fields(
210 &self,
211 fields: DateFields,
212 options: DateFromFieldsOptions,
213 ) -> Result<Self::DateInner, DateFromFieldsError> {
214 ArithmeticDate::from_fields(fields, options, self).map(HebrewDateInner)
215 }
216
217 fn from_rata_die(&self, rd: RataDie) -> Self::DateInner {
218 let (year_info, year) = YearInfo::year_containing_rd(rd);
219 let keviyah = year_info.keviyah;
220
221 let day_in_year = u16::try_from(rd - year_info.new_year() + 1).unwrap_or(u16::MAX);
223 let (month, day) = keviyah.month_day_for(day_in_year);
224
225 HebrewDateInner(ArithmeticDate::new_unchecked(
226 HebrewYearInfo {
227 keviyah,
228 value: year,
229 },
230 month,
231 day,
232 ))
233 }
234
235 fn to_rata_die(&self, date: &Self::DateInner) -> RataDie {
236 let ny = date.0.year.keviyah.year_info(date.0.year.value).new_year();
237 let days_preceding = date.0.year.keviyah.days_preceding(date.0.month);
238
239 ny + i64::from(days_preceding) + i64::from(date.0.day) - 1
241 }
242
243 fn has_cheap_iso_conversion(&self) -> bool {
244 false
245 }
246
247 fn months_in_year(&self, date: &Self::DateInner) -> u8 {
248 Self::months_in_provided_year(date.0.year)
249 }
250
251 fn days_in_year(&self, date: &Self::DateInner) -> u16 {
252 date.0.year.keviyah.year_length()
253 }
254
255 fn days_in_month(&self, date: &Self::DateInner) -> u8 {
256 Self::days_in_provided_month(date.0.year, date.0.month)
257 }
258
259 #[cfg(feature = "unstable")]
260 fn add(
261 &self,
262 date: &Self::DateInner,
263 duration: types::DateDuration,
264 options: DateAddOptions,
265 ) -> Result<Self::DateInner, DateError> {
266 date.0.added(duration, self, options).map(HebrewDateInner)
267 }
268
269 #[cfg(feature = "unstable")]
270 fn until(
271 &self,
272 date1: &Self::DateInner,
273 date2: &Self::DateInner,
274 options: DateDifferenceOptions,
275 ) -> Result<types::DateDuration, Self::DifferenceError> {
276 Ok(date1.0.until(&date2.0, self, options))
277 }
278
279 fn debug_name(&self) -> &'static str {
280 "Hebrew"
281 }
282
283 fn year_info(&self, date: &Self::DateInner) -> Self::Year {
284 let extended_year = date.0.year.value;
285 types::EraYear {
286 era_index: Some(0),
287 era: tinystr!(16, "am"),
288 year: extended_year,
289 extended_year,
290 ambiguity: types::YearAmbiguity::CenturyRequired,
291 }
292 }
293
294 fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
295 date.0.year.keviyah.is_leap()
296 }
297
298 fn month(&self, date: &Self::DateInner) -> MonthInfo {
299 let valid_standard_code = self.month_code_from_ordinal(&date.0.year, date.0.month);
300
301 let valid_formatting_code = if valid_standard_code.number() == 6 && date.0.month == 7 {
302 ValidMonthCode::new_unchecked(6, true) } else {
304 valid_standard_code
305 };
306
307 types::MonthInfo {
308 ordinal: date.0.month,
309 standard_code: valid_standard_code.to_month_code(),
310 valid_standard_code,
311 formatting_code: valid_formatting_code.to_month_code(),
312 valid_formatting_code,
313 }
314 }
315
316 fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
317 types::DayOfMonth(date.0.day)
318 }
319
320 fn day_of_year(&self, date: &Self::DateInner) -> types::DayOfYear {
321 types::DayOfYear(date.0.year.keviyah.days_preceding(date.0.month) + date.0.day as u16)
322 }
323
324 fn calendar_algorithm(&self) -> Option<crate::preferences::CalendarAlgorithm> {
325 Some(crate::preferences::CalendarAlgorithm::Hebrew)
326 }
327}
328
329impl Date<Hebrew> {
330 #[deprecated(since = "2.1.0", note = "use `Date::try_new_from_codes`")]
334 pub fn try_new_hebrew(
335 year: i32,
336 ordinal_month: u8,
337 day: u8,
338 ) -> Result<Date<Hebrew>, RangeError> {
339 let year = HebrewYearInfo::compute(year);
340
341 ArithmeticDate::try_from_ymd(year, ordinal_month, day)
342 .map(HebrewDateInner)
343 .map(|inner| Date::from_raw(inner, Hebrew))
344 }
345}
346
347#[cfg(test)]
348mod tests {
349
350 use super::*;
351 use crate::types::MonthCode;
352
353 pub const TISHREI: ValidMonthCode = ValidMonthCode::new_unchecked(1, false);
354 pub const ḤESHVAN: ValidMonthCode = ValidMonthCode::new_unchecked(2, false);
355 pub const KISLEV: ValidMonthCode = ValidMonthCode::new_unchecked(3, false);
356 pub const TEVET: ValidMonthCode = ValidMonthCode::new_unchecked(4, false);
357 pub const SHEVAT: ValidMonthCode = ValidMonthCode::new_unchecked(5, false);
358 pub const ADARI: ValidMonthCode = ValidMonthCode::new_unchecked(5, true);
359 pub const ADAR: ValidMonthCode = ValidMonthCode::new_unchecked(6, false);
360 pub const NISAN: ValidMonthCode = ValidMonthCode::new_unchecked(7, false);
361 pub const IYYAR: ValidMonthCode = ValidMonthCode::new_unchecked(8, false);
362 pub const SIVAN: ValidMonthCode = ValidMonthCode::new_unchecked(9, false);
363 pub const TAMMUZ: ValidMonthCode = ValidMonthCode::new_unchecked(10, false);
364 pub const AV: ValidMonthCode = ValidMonthCode::new_unchecked(11, false);
365 pub const ELUL: ValidMonthCode = ValidMonthCode::new_unchecked(12, false);
366
367 const LEAP_YEARS_IN_TESTS: [i32; 1] = [5782];
369 #[expect(clippy::type_complexity)]
373 const ISO_HEBREW_DATE_PAIRS: [((i32, u8, u8), (i32, ValidMonthCode, u8)); 48] = [
374 ((2021, 1, 10), (5781, TEVET, 26)),
375 ((2021, 1, 25), (5781, SHEVAT, 12)),
376 ((2021, 2, 10), (5781, SHEVAT, 28)),
377 ((2021, 2, 25), (5781, ADAR, 13)),
378 ((2021, 3, 10), (5781, ADAR, 26)),
379 ((2021, 3, 25), (5781, NISAN, 12)),
380 ((2021, 4, 10), (5781, NISAN, 28)),
381 ((2021, 4, 25), (5781, IYYAR, 13)),
382 ((2021, 5, 10), (5781, IYYAR, 28)),
383 ((2021, 5, 25), (5781, SIVAN, 14)),
384 ((2021, 6, 10), (5781, SIVAN, 30)),
385 ((2021, 6, 25), (5781, TAMMUZ, 15)),
386 ((2021, 7, 10), (5781, AV, 1)),
387 ((2021, 7, 25), (5781, AV, 16)),
388 ((2021, 8, 10), (5781, ELUL, 2)),
389 ((2021, 8, 25), (5781, ELUL, 17)),
390 ((2021, 9, 10), (5782, TISHREI, 4)),
391 ((2021, 9, 25), (5782, TISHREI, 19)),
392 ((2021, 10, 10), (5782, ḤESHVAN, 4)),
393 ((2021, 10, 25), (5782, ḤESHVAN, 19)),
394 ((2021, 11, 10), (5782, KISLEV, 6)),
395 ((2021, 11, 25), (5782, KISLEV, 21)),
396 ((2021, 12, 10), (5782, TEVET, 6)),
397 ((2021, 12, 25), (5782, TEVET, 21)),
398 ((2022, 1, 10), (5782, SHEVAT, 8)),
399 ((2022, 1, 25), (5782, SHEVAT, 23)),
400 ((2022, 2, 10), (5782, ADARI, 9)),
401 ((2022, 2, 25), (5782, ADARI, 24)),
402 ((2022, 3, 10), (5782, ADAR, 7)),
403 ((2022, 3, 25), (5782, ADAR, 22)),
404 ((2022, 4, 10), (5782, NISAN, 9)),
405 ((2022, 4, 25), (5782, NISAN, 24)),
406 ((2022, 5, 10), (5782, IYYAR, 9)),
407 ((2022, 5, 25), (5782, IYYAR, 24)),
408 ((2022, 6, 10), (5782, SIVAN, 11)),
409 ((2022, 6, 25), (5782, SIVAN, 26)),
410 ((2022, 7, 10), (5782, TAMMUZ, 11)),
411 ((2022, 7, 25), (5782, TAMMUZ, 26)),
412 ((2022, 8, 10), (5782, AV, 13)),
413 ((2022, 8, 25), (5782, AV, 28)),
414 ((2022, 9, 10), (5782, ELUL, 14)),
415 ((2022, 9, 25), (5782, ELUL, 29)),
416 ((2022, 10, 10), (5783, TISHREI, 15)),
417 ((2022, 10, 25), (5783, TISHREI, 30)),
418 ((2022, 11, 10), (5783, ḤESHVAN, 16)),
419 ((2022, 11, 25), (5783, KISLEV, 1)),
420 ((2022, 12, 10), (5783, KISLEV, 16)),
421 ((2022, 12, 25), (5783, TEVET, 1)),
422 ];
423
424 #[test]
425 fn test_conversions() {
426 for ((iso_y, iso_m, iso_d), (y, m, d)) in ISO_HEBREW_DATE_PAIRS.into_iter() {
427 let iso_date = Date::try_new_iso(iso_y, iso_m, iso_d).unwrap();
428 let hebrew_date = Date::try_new_from_codes(Some("am"), y, m.to_month_code(), d, Hebrew)
429 .expect("Date should parse");
430
431 let iso_to_hebrew = iso_date.to_calendar(Hebrew);
432
433 let hebrew_to_iso = hebrew_date.to_iso();
434
435 assert_eq!(
436 hebrew_to_iso, iso_date,
437 "Failed comparing to-ISO value for {hebrew_date:?} => {iso_date:?}"
438 );
439 assert_eq!(
440 iso_to_hebrew, hebrew_date,
441 "Failed comparing to-hebrew value for {iso_date:?} => {hebrew_date:?}"
442 );
443
444 let ordinal_month = if (m == ADARI || m.number() >= ADAR.number())
445 && LEAP_YEARS_IN_TESTS.contains(&y)
446 {
447 m.number() + 1
448 } else {
449 assert!(m != ADARI);
450 m.number()
451 };
452
453 #[allow(deprecated)] let ordinal_hebrew_date = Date::try_new_hebrew(y, ordinal_month, d)
455 .expect("Construction of date must succeed");
456
457 assert_eq!(ordinal_hebrew_date, hebrew_date, "Hebrew date construction from codes and ordinals should work the same for {hebrew_date:?}");
458 }
459 }
460
461 #[test]
462 fn test_icu_bug_22441() {
463 let yi = YearInfo::compute_for(88369);
464 assert_eq!(yi.keviyah.year_length(), 383);
465 }
466
467 #[test]
468 fn test_negative_era_years() {
469 let greg_date = Date::try_new_gregorian(-5000, 1, 1).unwrap();
470 let greg_year = greg_date.era_year();
471 assert_eq!(greg_date.inner.0.year, -5000);
472 assert_eq!(greg_year.era, "bce");
473 assert_eq!(greg_year.year, 5001);
475 let hebr_date = greg_date.to_calendar(Hebrew);
476 let hebr_year = hebr_date.era_year();
477 assert_eq!(hebr_date.inner.0.year.value, -1240);
478 assert_eq!(hebr_year.era, "am");
479 assert_eq!(hebr_year.year, -1240);
481 }
482
483 #[test]
484 fn test_weekdays() {
485 let cal = Hebrew::new();
487 let era = "am";
488 let month_code = MonthCode::new_normal(1).unwrap();
489 let dt = Date::try_new_from_codes(Some(era), 3760, month_code, 1, cal).unwrap();
490
491 assert_eq!(6, dt.day_of_week() as usize);
494 }
495}