1use crate::cal::iso::{Iso, IsoDateInner};
20use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic};
21use crate::error::{year_check, DateError};
22use crate::{types, Calendar, Date, DateDuration, DateDurationUnit, RangeError};
23use calendrical_calculations::helpers::I32CastError;
24use calendrical_calculations::rata_die::RataDie;
25use tinystr::tinystr;
26
27#[derive(Copy, Clone, Debug, Hash, Default, Eq, PartialEq, PartialOrd, Ord)]
43#[allow(clippy::exhaustive_structs)] pub struct Julian;
45
46#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
48pub struct JulianDateInner(pub(crate) ArithmeticDate<Julian>);
50
51impl CalendarArithmetic for Julian {
52 type YearInfo = i32;
53
54 fn days_in_provided_month(year: i32, month: u8) -> u8 {
55 match month {
56 4 | 6 | 9 | 11 => 30,
57 2 if Self::provided_year_is_leap(year) => 29,
58 2 => 28,
59 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
60 _ => 0,
61 }
62 }
63
64 fn months_in_provided_year(_: i32) -> u8 {
65 12
66 }
67
68 fn provided_year_is_leap(year: i32) -> bool {
69 calendrical_calculations::julian::is_leap_year(year)
70 }
71
72 fn last_month_day_in_provided_year(_year: i32) -> (u8, u8) {
73 (12, 31)
74 }
75
76 fn days_in_provided_year(year: i32) -> u16 {
77 if Self::provided_year_is_leap(year) {
78 366
79 } else {
80 365
81 }
82 }
83}
84
85impl crate::cal::scaffold::UnstableSealed for Julian {}
86impl Calendar for Julian {
87 type DateInner = JulianDateInner;
88 type Year = types::EraYear;
89
90 fn from_codes(
91 &self,
92 era: Option<&str>,
93 year: i32,
94 month_code: types::MonthCode,
95 day: u8,
96 ) -> Result<Self::DateInner, DateError> {
97 let year = match era {
98 Some("ce" | "ad") | None => year_check(year, 1..)?,
99 Some("bce" | "bc") => 1 - year_check(year, 1..)?,
100 Some(_) => return Err(DateError::UnknownEra),
101 };
102
103 ArithmeticDate::new_from_codes(self, year, month_code, day).map(JulianDateInner)
104 }
105
106 fn from_rata_die(&self, rd: RataDie) -> Self::DateInner {
107 JulianDateInner(
108 match calendrical_calculations::julian::julian_from_fixed(rd) {
109 Err(I32CastError::BelowMin) => ArithmeticDate::min_date(),
110 Err(I32CastError::AboveMax) => ArithmeticDate::max_date(),
111 Ok((year, month, day)) => ArithmeticDate::new_unchecked(year, month, day),
112 },
113 )
114 }
115
116 fn to_rata_die(&self, date: &Self::DateInner) -> RataDie {
117 calendrical_calculations::julian::fixed_from_julian(date.0.year, date.0.month, date.0.day)
118 }
119
120 fn from_iso(&self, iso: IsoDateInner) -> JulianDateInner {
121 self.from_rata_die(Iso.to_rata_die(&iso))
122 }
123
124 fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner {
125 Iso.from_rata_die(self.to_rata_die(date))
126 }
127
128 fn months_in_year(&self, date: &Self::DateInner) -> u8 {
129 date.0.months_in_year()
130 }
131
132 fn days_in_year(&self, date: &Self::DateInner) -> u16 {
133 date.0.days_in_year()
134 }
135
136 fn days_in_month(&self, date: &Self::DateInner) -> u8 {
137 date.0.days_in_month()
138 }
139
140 fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration<Self>) {
141 date.0.offset_date(offset, &());
142 }
143
144 #[allow(clippy::field_reassign_with_default)]
145 fn until(
146 &self,
147 date1: &Self::DateInner,
148 date2: &Self::DateInner,
149 _calendar2: &Self,
150 _largest_unit: DateDurationUnit,
151 _smallest_unit: DateDurationUnit,
152 ) -> DateDuration<Self> {
153 date1.0.until(date2.0, _largest_unit, _smallest_unit)
154 }
155
156 fn year_info(&self, date: &Self::DateInner) -> Self::Year {
159 let extended_year = self.extended_year(date);
160 if extended_year > 0 {
161 types::EraYear {
162 era: tinystr!(16, "ce"),
163 era_index: Some(1),
164 year: extended_year,
165 ambiguity: types::YearAmbiguity::CenturyRequired,
166 }
167 } else {
168 types::EraYear {
169 era: tinystr!(16, "bce"),
170 era_index: Some(0),
171 year: 1_i32.saturating_sub(extended_year),
172 ambiguity: types::YearAmbiguity::EraAndCenturyRequired,
173 }
174 }
175 }
176
177 fn extended_year(&self, date: &Self::DateInner) -> i32 {
178 date.0.extended_year()
179 }
180
181 fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
182 Self::provided_year_is_leap(date.0.year)
183 }
184
185 fn month(&self, date: &Self::DateInner) -> types::MonthInfo {
187 date.0.month()
188 }
189
190 fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
192 date.0.day_of_month()
193 }
194
195 fn day_of_year(&self, date: &Self::DateInner) -> types::DayOfYear {
196 date.0.day_of_year()
197 }
198
199 fn debug_name(&self) -> &'static str {
200 "Julian"
201 }
202
203 fn calendar_algorithm(&self) -> Option<crate::preferences::CalendarAlgorithm> {
204 None
205 }
206}
207
208impl Julian {
209 pub fn new() -> Self {
211 Self
212 }
213}
214
215impl Date<Julian> {
216 pub fn try_new_julian(year: i32, month: u8, day: u8) -> Result<Date<Julian>, RangeError> {
231 ArithmeticDate::new_from_ordinals(year, month, day)
232 .map(JulianDateInner)
233 .map(|inner| Date::from_raw(inner, Julian))
234 }
235}
236
237#[cfg(test)]
238mod test {
239 use super::*;
240
241 #[test]
242 fn test_day_iso_to_julian() {
243 let iso_date = Date::try_new_iso(200, 3, 1).unwrap();
245 let julian_date = Date::new_from_iso(iso_date, Julian).inner;
246 assert_eq!(julian_date.0.year, 200);
247 assert_eq!(julian_date.0.month, 3);
248 assert_eq!(julian_date.0.day, 1);
249
250 let iso_date = Date::try_new_iso(200, 2, 28).unwrap();
252 let julian_date = Date::new_from_iso(iso_date, Julian).inner;
253 assert_eq!(julian_date.0.year, 200);
254 assert_eq!(julian_date.0.month, 2);
255 assert_eq!(julian_date.0.day, 29);
256
257 let iso_date = Date::try_new_iso(400, 3, 1).unwrap();
259 let julian_date = Date::new_from_iso(iso_date, Julian).inner;
260 assert_eq!(julian_date.0.year, 400);
261 assert_eq!(julian_date.0.month, 2);
262 assert_eq!(julian_date.0.day, 29);
263
264 let iso_date = Date::try_new_iso(2022, 1, 1).unwrap();
266 let julian_date = Date::new_from_iso(iso_date, Julian).inner;
267 assert_eq!(julian_date.0.year, 2021);
268 assert_eq!(julian_date.0.month, 12);
269 assert_eq!(julian_date.0.day, 19);
270 }
271
272 #[test]
273 fn test_day_julian_to_iso() {
274 let julian_date = Date::try_new_julian(200, 3, 1).unwrap();
276 let iso_date = julian_date.to_iso();
277 let iso_expected_date = Date::try_new_iso(200, 3, 1).unwrap();
278 assert_eq!(iso_date, iso_expected_date);
279
280 let julian_date = Date::try_new_julian(200, 2, 29).unwrap();
282 let iso_date = julian_date.to_iso();
283 let iso_expected_date = Date::try_new_iso(200, 2, 28).unwrap();
284 assert_eq!(iso_date, iso_expected_date);
285
286 let julian_date = Date::try_new_julian(400, 2, 29).unwrap();
288 let iso_date = julian_date.to_iso();
289 let iso_expected_date = Date::try_new_iso(400, 3, 1).unwrap();
290 assert_eq!(iso_date, iso_expected_date);
291
292 let julian_date = Date::try_new_julian(2021, 12, 19).unwrap();
294 let iso_date = julian_date.to_iso();
295 let iso_expected_date = Date::try_new_iso(2022, 1, 1).unwrap();
296 assert_eq!(iso_date, iso_expected_date);
297
298 let julian_date = Date::try_new_julian(2022, 2, 16).unwrap();
300 let iso_date = julian_date.to_iso();
301 let iso_expected_date = Date::try_new_iso(2022, 3, 1).unwrap();
302 assert_eq!(iso_date, iso_expected_date);
303 }
304
305 #[test]
306 fn test_roundtrip_negative() {
307 let iso_date = Date::try_new_iso(-1000, 3, 3).unwrap();
309 let julian = iso_date.to_calendar(Julian::new());
310 let recovered_iso = julian.to_iso();
311 assert_eq!(iso_date, recovered_iso);
312 }
313
314 #[test]
315 fn test_julian_near_era_change() {
316 #[derive(Debug)]
320 struct TestCase {
321 rd: i64,
322 iso_year: i32,
323 iso_month: u8,
324 iso_day: u8,
325 expected_year: i32,
326 expected_era: &'static str,
327 expected_month: u8,
328 expected_day: u8,
329 }
330
331 let cases = [
332 TestCase {
333 rd: 1,
334 iso_year: 1,
335 iso_month: 1,
336 iso_day: 1,
337 expected_year: 1,
338 expected_era: "ce",
339 expected_month: 1,
340 expected_day: 3,
341 },
342 TestCase {
343 rd: 0,
344 iso_year: 0,
345 iso_month: 12,
346 iso_day: 31,
347 expected_year: 1,
348 expected_era: "ce",
349 expected_month: 1,
350 expected_day: 2,
351 },
352 TestCase {
353 rd: -1,
354 iso_year: 0,
355 iso_month: 12,
356 iso_day: 30,
357 expected_year: 1,
358 expected_era: "ce",
359 expected_month: 1,
360 expected_day: 1,
361 },
362 TestCase {
363 rd: -2,
364 iso_year: 0,
365 iso_month: 12,
366 iso_day: 29,
367 expected_year: 1,
368 expected_era: "bce",
369 expected_month: 12,
370 expected_day: 31,
371 },
372 TestCase {
373 rd: -3,
374 iso_year: 0,
375 iso_month: 12,
376 iso_day: 28,
377 expected_year: 1,
378 expected_era: "bce",
379 expected_month: 12,
380 expected_day: 30,
381 },
382 TestCase {
383 rd: -367,
384 iso_year: -1,
385 iso_month: 12,
386 iso_day: 30,
387 expected_year: 1,
388 expected_era: "bce",
389 expected_month: 1,
390 expected_day: 1,
391 },
392 TestCase {
393 rd: -368,
394 iso_year: -1,
395 iso_month: 12,
396 iso_day: 29,
397 expected_year: 2,
398 expected_era: "bce",
399 expected_month: 12,
400 expected_day: 31,
401 },
402 TestCase {
403 rd: -1462,
404 iso_year: -4,
405 iso_month: 12,
406 iso_day: 30,
407 expected_year: 4,
408 expected_era: "bce",
409 expected_month: 1,
410 expected_day: 1,
411 },
412 TestCase {
413 rd: -1463,
414 iso_year: -4,
415 iso_month: 12,
416 iso_day: 29,
417 expected_year: 5,
418 expected_era: "bce",
419 expected_month: 12,
420 expected_day: 31,
421 },
422 ];
423
424 for case in cases {
425 let iso_from_rd = Date::from_rata_die(RataDie::new(case.rd), crate::Iso);
426 let julian_from_rd = Date::from_rata_die(RataDie::new(case.rd), Julian);
427 assert_eq!(julian_from_rd.era_year().year, case.expected_year,
428 "Failed year check from RD: {case:?}\nISO: {iso_from_rd:?}\nJulian: {julian_from_rd:?}");
429 assert_eq!(julian_from_rd.era_year().era, case.expected_era,
430 "Failed era check from RD: {case:?}\nISO: {iso_from_rd:?}\nJulian: {julian_from_rd:?}");
431 assert_eq!(julian_from_rd.month().ordinal, case.expected_month,
432 "Failed month check from RD: {case:?}\nISO: {iso_from_rd:?}\nJulian: {julian_from_rd:?}");
433 assert_eq!(julian_from_rd.day_of_month().0, case.expected_day,
434 "Failed day check from RD: {case:?}\nISO: {iso_from_rd:?}\nJulian: {julian_from_rd:?}");
435
436 let iso_date_man = Date::try_new_iso(case.iso_year, case.iso_month, case.iso_day)
437 .expect("Failed to initialize ISO date for {case:?}");
438 let julian_date_man = Date::new_from_iso(iso_date_man, Julian);
439 assert_eq!(iso_from_rd, iso_date_man,
440 "ISO from RD not equal to ISO generated from manually-input ymd\nCase: {case:?}\nRD: {iso_from_rd:?}\nMan: {iso_date_man:?}");
441 assert_eq!(julian_from_rd, julian_date_man,
442 "Julian from RD not equal to Julian generated from manually-input ymd\nCase: {case:?}\nRD: {julian_from_rd:?}\nMan: {julian_date_man:?}");
443 }
444 }
445
446 #[test]
447 fn test_julian_rd_date_conversion() {
448 for i in -10000..=10000 {
451 let rd = RataDie::new(i);
452 let julian = Date::from_rata_die(rd, Julian);
453 let new_rd = julian.to_rata_die();
454 assert_eq!(rd, new_rd);
455 }
456 }
457
458 #[test]
459 fn test_julian_directionality() {
460 for i in -100..=100 {
464 for j in -100..=100 {
465 let julian_i = Date::from_rata_die(RataDie::new(i), Julian);
466 let julian_j = Date::from_rata_die(RataDie::new(j), Julian);
467
468 assert_eq!(
469 i.cmp(&j),
470 julian_i.inner.0.cmp(&julian_j.inner.0),
471 "Julian directionality inconsistent with directionality for i: {i}, j: {j}"
472 );
473 }
474 }
475 }
476
477 #[test]
478 fn test_hebrew_epoch() {
479 assert_eq!(
480 calendrical_calculations::julian::fixed_from_julian_book_version(-3761, 10, 7),
481 RataDie::new(-1373427)
482 );
483 }
484
485 #[test]
486 fn test_julian_leap_years() {
487 assert!(Julian::provided_year_is_leap(4));
488 assert!(Julian::provided_year_is_leap(0));
489 assert!(Julian::provided_year_is_leap(-4));
490
491 Date::try_new_julian(2020, 2, 29).unwrap();
492 }
493}