1use crate::calendar_arithmetic::ArithmeticDate;
6use crate::calendar_arithmetic::DateFieldsResolver;
7use crate::error::{DateError, DateFromFieldsError, EcmaReferenceYearError, UnknownEraError};
8use crate::options::DateFromFieldsOptions;
9use crate::options::{DateAddOptions, DateDifferenceOptions};
10use crate::types::DateFields;
11use crate::{types, Calendar, Date, RangeError};
12use calendrical_calculations::helpers::I32CastError;
13use calendrical_calculations::rata_die::RataDie;
14use tinystr::tinystr;
15
16#[derive(Copy, Clone, Debug, Hash, Default, Eq, PartialEq, PartialOrd, Ord)]
74#[allow(clippy::exhaustive_structs)] pub struct Julian;
76
77#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
79pub struct JulianDateInner(pub(crate) ArithmeticDate<Julian>);
81
82impl DateFieldsResolver for Julian {
83 type YearInfo = i32;
84
85 fn days_in_provided_month(year: i32, month: u8) -> u8 {
86 match month {
87 4 | 6 | 9 | 11 => 30,
88 2 if calendrical_calculations::julian::is_leap_year(year) => 29,
89 2 => 28,
90 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
91 _ => 0,
92 }
93 }
94
95 fn months_in_provided_year(_: i32) -> u8 {
96 12
97 }
98
99 #[inline]
100 fn year_info_from_era(
101 &self,
102 era: &[u8],
103 era_year: i32,
104 ) -> Result<Self::YearInfo, UnknownEraError> {
105 match era {
106 b"ad" | b"ce" => Ok(era_year),
107 b"bc" | b"bce" => Ok(1 - era_year),
108 _ => Err(UnknownEraError),
109 }
110 }
111
112 #[inline]
113 fn year_info_from_extended(&self, extended_year: i32) -> Self::YearInfo {
114 extended_year
115 }
116
117 #[inline]
118 fn reference_year_from_month_day(
119 &self,
120 month_code: types::ValidMonthCode,
121 day: u8,
122 ) -> Result<Self::YearInfo, EcmaReferenceYearError> {
123 let (ordinal_month, false) = month_code.to_tuple() else {
124 return Err(EcmaReferenceYearError::MonthCodeNotInCalendar);
125 };
126 let julian_year = if ordinal_month < 12 || (ordinal_month == 12 && day <= 18) {
129 1972
130 } else {
131 1971
132 };
133 Ok(julian_year)
134 }
135}
136
137impl crate::cal::scaffold::UnstableSealed for Julian {}
138impl Calendar for Julian {
139 type DateInner = JulianDateInner;
140 type Year = types::EraYear;
141 type DifferenceError = core::convert::Infallible;
142
143 fn from_codes(
144 &self,
145 era: Option<&str>,
146 year: i32,
147 month_code: types::MonthCode,
148 day: u8,
149 ) -> Result<Self::DateInner, DateError> {
150 ArithmeticDate::from_codes(era, year, month_code, day, self).map(JulianDateInner)
151 }
152
153 #[cfg(feature = "unstable")]
154 fn from_fields(
155 &self,
156 fields: DateFields,
157 options: DateFromFieldsOptions,
158 ) -> Result<Self::DateInner, DateFromFieldsError> {
159 ArithmeticDate::from_fields(fields, options, self).map(JulianDateInner)
160 }
161
162 fn from_rata_die(&self, rd: RataDie) -> Self::DateInner {
163 JulianDateInner(
164 match calendrical_calculations::julian::julian_from_fixed(rd) {
165 Err(I32CastError::BelowMin) => ArithmeticDate::new_unchecked(i32::MIN, 1, 1),
166 Err(I32CastError::AboveMax) => ArithmeticDate::new_unchecked(i32::MAX, 12, 31),
167 Ok((year, month, day)) => ArithmeticDate::new_unchecked(year, month, day),
168 },
169 )
170 }
171
172 fn to_rata_die(&self, date: &Self::DateInner) -> RataDie {
173 calendrical_calculations::julian::fixed_from_julian(date.0.year, date.0.month, date.0.day)
174 }
175
176 fn has_cheap_iso_conversion(&self) -> bool {
177 false
178 }
179
180 fn months_in_year(&self, date: &Self::DateInner) -> u8 {
181 Self::months_in_provided_year(date.0.year)
182 }
183
184 fn days_in_year(&self, date: &Self::DateInner) -> u16 {
185 if self.is_in_leap_year(date) {
186 366
187 } else {
188 365
189 }
190 }
191
192 fn days_in_month(&self, date: &Self::DateInner) -> u8 {
193 Self::days_in_provided_month(date.0.year, date.0.month)
194 }
195
196 #[cfg(feature = "unstable")]
197 fn add(
198 &self,
199 date: &Self::DateInner,
200 duration: types::DateDuration,
201 options: DateAddOptions,
202 ) -> Result<Self::DateInner, DateError> {
203 date.0.added(duration, self, options).map(JulianDateInner)
204 }
205
206 #[cfg(feature = "unstable")]
207 fn until(
208 &self,
209 date1: &Self::DateInner,
210 date2: &Self::DateInner,
211 options: DateDifferenceOptions,
212 ) -> Result<types::DateDuration, Self::DifferenceError> {
213 Ok(date1.0.until(&date2.0, self, options))
214 }
215
216 fn year_info(&self, date: &Self::DateInner) -> Self::Year {
219 let extended_year = date.0.year;
220 if extended_year > 0 {
221 types::EraYear {
222 era: tinystr!(16, "ce"),
223 era_index: Some(1),
224 year: extended_year,
225 extended_year,
226 ambiguity: types::YearAmbiguity::CenturyRequired,
227 }
228 } else {
229 types::EraYear {
230 era: tinystr!(16, "bce"),
231 era_index: Some(0),
232 year: 1 - extended_year,
233 extended_year,
234 ambiguity: types::YearAmbiguity::EraAndCenturyRequired,
235 }
236 }
237 }
238
239 fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
240 calendrical_calculations::julian::is_leap_year(date.0.year)
241 }
242
243 fn month(&self, date: &Self::DateInner) -> types::MonthInfo {
245 types::MonthInfo::non_lunisolar(date.0.month)
246 }
247
248 fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
250 types::DayOfMonth(date.0.day)
251 }
252
253 fn day_of_year(&self, date: &Self::DateInner) -> types::DayOfYear {
254 types::DayOfYear(
255 (1..date.0.month)
256 .map(|m| Self::days_in_provided_month(date.0.year, m) as u16)
257 .sum::<u16>()
258 + date.0.day as u16,
259 )
260 }
261
262 fn debug_name(&self) -> &'static str {
263 "Julian"
264 }
265
266 fn calendar_algorithm(&self) -> Option<crate::preferences::CalendarAlgorithm> {
267 None
268 }
269}
270
271impl Julian {
272 pub fn new() -> Self {
274 Self
275 }
276
277 pub fn easter(year: i32) -> Date<Self> {
279 Date::from_rata_die(calendrical_calculations::julian::easter(year), Self)
280 }
281}
282
283impl Date<Julian> {
284 pub fn try_new_julian(year: i32, month: u8, day: u8) -> Result<Date<Julian>, RangeError> {
299 ArithmeticDate::try_from_ymd(year, month, day)
300 .map(JulianDateInner)
301 .map(|inner| Date::from_raw(inner, Julian))
302 }
303}
304
305#[cfg(test)]
306mod test {
307 use super::*;
308
309 #[test]
310 fn test_day_iso_to_julian() {
311 let iso_date = Date::try_new_iso(200, 3, 1).unwrap();
313 let julian_date = Date::new_from_iso(iso_date, Julian).inner;
314 assert_eq!(julian_date.0.year, 200);
315 assert_eq!(julian_date.0.month, 3);
316 assert_eq!(julian_date.0.day, 1);
317
318 let iso_date = Date::try_new_iso(200, 2, 28).unwrap();
320 let julian_date = Date::new_from_iso(iso_date, Julian).inner;
321 assert_eq!(julian_date.0.year, 200);
322 assert_eq!(julian_date.0.month, 2);
323 assert_eq!(julian_date.0.day, 29);
324
325 let iso_date = Date::try_new_iso(400, 3, 1).unwrap();
327 let julian_date = Date::new_from_iso(iso_date, Julian).inner;
328 assert_eq!(julian_date.0.year, 400);
329 assert_eq!(julian_date.0.month, 2);
330 assert_eq!(julian_date.0.day, 29);
331
332 let iso_date = Date::try_new_iso(2022, 1, 1).unwrap();
334 let julian_date = Date::new_from_iso(iso_date, Julian).inner;
335 assert_eq!(julian_date.0.year, 2021);
336 assert_eq!(julian_date.0.month, 12);
337 assert_eq!(julian_date.0.day, 19);
338 }
339
340 #[test]
341 fn test_day_julian_to_iso() {
342 let julian_date = Date::try_new_julian(200, 3, 1).unwrap();
344 let iso_date = julian_date.to_iso();
345 let iso_expected_date = Date::try_new_iso(200, 3, 1).unwrap();
346 assert_eq!(iso_date, iso_expected_date);
347
348 let julian_date = Date::try_new_julian(200, 2, 29).unwrap();
350 let iso_date = julian_date.to_iso();
351 let iso_expected_date = Date::try_new_iso(200, 2, 28).unwrap();
352 assert_eq!(iso_date, iso_expected_date);
353
354 let julian_date = Date::try_new_julian(400, 2, 29).unwrap();
356 let iso_date = julian_date.to_iso();
357 let iso_expected_date = Date::try_new_iso(400, 3, 1).unwrap();
358 assert_eq!(iso_date, iso_expected_date);
359
360 let julian_date = Date::try_new_julian(2021, 12, 19).unwrap();
362 let iso_date = julian_date.to_iso();
363 let iso_expected_date = Date::try_new_iso(2022, 1, 1).unwrap();
364 assert_eq!(iso_date, iso_expected_date);
365
366 let julian_date = Date::try_new_julian(2022, 2, 16).unwrap();
368 let iso_date = julian_date.to_iso();
369 let iso_expected_date = Date::try_new_iso(2022, 3, 1).unwrap();
370 assert_eq!(iso_date, iso_expected_date);
371 }
372
373 #[test]
374 fn test_roundtrip_negative() {
375 let iso_date = Date::try_new_iso(-1000, 3, 3).unwrap();
377 let julian = iso_date.to_calendar(Julian::new());
378 let recovered_iso = julian.to_iso();
379 assert_eq!(iso_date, recovered_iso);
380 }
381
382 #[test]
383 fn test_julian_near_era_change() {
384 #[derive(Debug)]
388 struct TestCase {
389 rd: i64,
390 iso_year: i32,
391 iso_month: u8,
392 iso_day: u8,
393 expected_year: i32,
394 expected_era: &'static str,
395 expected_month: u8,
396 expected_day: u8,
397 }
398
399 let cases = [
400 TestCase {
401 rd: 1,
402 iso_year: 1,
403 iso_month: 1,
404 iso_day: 1,
405 expected_year: 1,
406 expected_era: "ce",
407 expected_month: 1,
408 expected_day: 3,
409 },
410 TestCase {
411 rd: 0,
412 iso_year: 0,
413 iso_month: 12,
414 iso_day: 31,
415 expected_year: 1,
416 expected_era: "ce",
417 expected_month: 1,
418 expected_day: 2,
419 },
420 TestCase {
421 rd: -1,
422 iso_year: 0,
423 iso_month: 12,
424 iso_day: 30,
425 expected_year: 1,
426 expected_era: "ce",
427 expected_month: 1,
428 expected_day: 1,
429 },
430 TestCase {
431 rd: -2,
432 iso_year: 0,
433 iso_month: 12,
434 iso_day: 29,
435 expected_year: 1,
436 expected_era: "bce",
437 expected_month: 12,
438 expected_day: 31,
439 },
440 TestCase {
441 rd: -3,
442 iso_year: 0,
443 iso_month: 12,
444 iso_day: 28,
445 expected_year: 1,
446 expected_era: "bce",
447 expected_month: 12,
448 expected_day: 30,
449 },
450 TestCase {
451 rd: -367,
452 iso_year: -1,
453 iso_month: 12,
454 iso_day: 30,
455 expected_year: 1,
456 expected_era: "bce",
457 expected_month: 1,
458 expected_day: 1,
459 },
460 TestCase {
461 rd: -368,
462 iso_year: -1,
463 iso_month: 12,
464 iso_day: 29,
465 expected_year: 2,
466 expected_era: "bce",
467 expected_month: 12,
468 expected_day: 31,
469 },
470 TestCase {
471 rd: -1462,
472 iso_year: -4,
473 iso_month: 12,
474 iso_day: 30,
475 expected_year: 4,
476 expected_era: "bce",
477 expected_month: 1,
478 expected_day: 1,
479 },
480 TestCase {
481 rd: -1463,
482 iso_year: -4,
483 iso_month: 12,
484 iso_day: 29,
485 expected_year: 5,
486 expected_era: "bce",
487 expected_month: 12,
488 expected_day: 31,
489 },
490 ];
491
492 for case in cases {
493 let iso_from_rd = Date::from_rata_die(RataDie::new(case.rd), crate::Iso);
494 let julian_from_rd = Date::from_rata_die(RataDie::new(case.rd), Julian);
495 assert_eq!(julian_from_rd.era_year().year, case.expected_year,
496 "Failed year check from RD: {case:?}\nISO: {iso_from_rd:?}\nJulian: {julian_from_rd:?}");
497 assert_eq!(julian_from_rd.era_year().era, case.expected_era,
498 "Failed era check from RD: {case:?}\nISO: {iso_from_rd:?}\nJulian: {julian_from_rd:?}");
499 assert_eq!(julian_from_rd.month().ordinal, case.expected_month,
500 "Failed month check from RD: {case:?}\nISO: {iso_from_rd:?}\nJulian: {julian_from_rd:?}");
501 assert_eq!(julian_from_rd.day_of_month().0, case.expected_day,
502 "Failed day check from RD: {case:?}\nISO: {iso_from_rd:?}\nJulian: {julian_from_rd:?}");
503
504 let iso_date_man = Date::try_new_iso(case.iso_year, case.iso_month, case.iso_day)
505 .expect("Failed to initialize ISO date for {case:?}");
506 let julian_date_man = Date::new_from_iso(iso_date_man, Julian);
507 assert_eq!(iso_from_rd, iso_date_man,
508 "ISO from RD not equal to ISO generated from manually-input ymd\nCase: {case:?}\nRD: {iso_from_rd:?}\nMan: {iso_date_man:?}");
509 assert_eq!(julian_from_rd, julian_date_man,
510 "Julian from RD not equal to Julian generated from manually-input ymd\nCase: {case:?}\nRD: {julian_from_rd:?}\nMan: {julian_date_man:?}");
511 }
512 }
513
514 #[test]
515 fn test_julian_rd_date_conversion() {
516 for i in -10000..=10000 {
519 let rd = RataDie::new(i);
520 let julian = Date::from_rata_die(rd, Julian);
521 let new_rd = julian.to_rata_die();
522 assert_eq!(rd, new_rd);
523 }
524 }
525
526 #[test]
527 fn test_julian_directionality() {
528 for i in -100..=100 {
532 for j in -100..=100 {
533 let julian_i = Date::from_rata_die(RataDie::new(i), Julian);
534 let julian_j = Date::from_rata_die(RataDie::new(j), Julian);
535
536 assert_eq!(
537 i.cmp(&j),
538 julian_i.inner.0.cmp(&julian_j.inner.0),
539 "Julian directionality inconsistent with directionality for i: {i}, j: {j}"
540 );
541 }
542 }
543 }
544
545 #[test]
546 fn test_julian_leap_years() {
547 Date::try_new_julian(4, 2, 29).unwrap();
548 Date::try_new_julian(0, 2, 29).unwrap();
549 Date::try_new_julian(-4, 2, 29).unwrap();
550 Date::try_new_julian(2020, 2, 29).unwrap();
551 }
552}