1use std::{
12 cmp::Ordering,
13 fmt::{Display, Formatter},
14 str::FromStr,
15 sync::OnceLock,
16};
17
18use crate::time::deltas::TimeDelta;
19use num::ToPrimitive;
20use thiserror::Error;
21
22use regex::Regex;
23
24use super::julian_dates::{Epoch, JulianDate, Unit};
25use crate::i64::consts::{SECONDS_PER_DAY, SECONDS_PER_HALF_DAY};
26
27fn iso_regex() -> &'static Regex {
28 static ISO: OnceLock<Regex> = OnceLock::new();
29 ISO.get_or_init(|| Regex::new(r"(?<year>-?\d{4,})-(?<month>\d{2})-(?<day>\d{2})").unwrap())
30}
31
32#[derive(Debug, Clone, Error, PartialEq, Eq, PartialOrd, Ord)]
34pub enum DateError {
35 #[error("invalid date `{0}-{1}-{2}`")]
37 InvalidDate(i64, u8, u8),
38 #[error("invalid ISO string `{0}`")]
40 InvalidIsoString(String),
41 #[error("day of year cannot be 366 for a non-leap year")]
43 NonLeapYear,
44}
45
46#[derive(Debug, Copy, Clone, PartialEq, Eq)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49pub enum Calendar {
50 ProlepticJulian,
52 Julian,
54 Gregorian,
56}
57
58#[derive(Debug, Copy, Clone, PartialEq, Eq)]
60#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
61pub struct Date {
62 calendar: Calendar,
63 year: i64,
64 month: u8,
65 day: u8,
66}
67
68impl Display for Date {
69 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
70 write!(f, "{}-{:02}-{:02}", self.year, self.month, self.day)
71 }
72}
73
74impl FromStr for Date {
75 type Err = DateError;
76
77 fn from_str(iso: &str) -> Result<Self, Self::Err> {
78 Self::from_iso(iso)
79 }
80}
81
82impl Default for Date {
83 fn default() -> Self {
85 Self {
86 calendar: Calendar::Gregorian,
87 year: 2000,
88 month: 1,
89 day: 1,
90 }
91 }
92}
93
94impl PartialOrd for Date {
95 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
96 Some(self.cmp(other))
97 }
98}
99
100impl Ord for Date {
101 fn cmp(&self, other: &Self) -> Ordering {
108 match self.year.cmp(&other.year) {
109 Ordering::Equal => match self.month.cmp(&other.month) {
110 Ordering::Equal => self.day.cmp(&other.day),
111 other => other,
112 },
113 other => other,
114 }
115 }
116}
117
118const LAST_PROLEPTIC_JULIAN_DAY_J2K: i64 = -730122;
119const LAST_JULIAN_DAY_J2K: i64 = -152384;
120
121impl Date {
122 pub fn calendar(&self) -> Calendar {
124 self.calendar
125 }
126
127 pub fn year(&self) -> i64 {
129 self.year
130 }
131
132 pub fn month(&self) -> u8 {
134 self.month
135 }
136
137 pub fn day(&self) -> u8 {
139 self.day
140 }
141
142 pub fn new(year: i64, month: u8, day: u8) -> Result<Self, DateError> {
149 if !(1..=12).contains(&month) {
150 Err(DateError::InvalidDate(year, month, day))
151 } else {
152 let calendar = calendar(year, month, day);
153 let check = Date::from_days_since_j2000(j2000_day_number(calendar, year, month, day));
154
155 if check.year() != year || check.month() != month || check.day() != day {
156 Err(DateError::InvalidDate(year, month, day))
157 } else {
158 Ok(Date {
159 calendar,
160 year,
161 month,
162 day,
163 })
164 }
165 }
166 }
167
168 pub const fn new_unchecked(year: i64, month: u8, day: u8) -> Self {
170 let calendar = calendar(year, month, day);
171 Date {
172 calendar,
173 year,
174 month,
175 day,
176 }
177 }
178
179 pub fn from_iso(iso: &str) -> Result<Self, DateError> {
186 let caps = iso_regex()
187 .captures(iso)
188 .ok_or(DateError::InvalidIsoString(iso.to_owned()))?;
189 let year: i64 = caps["year"]
190 .parse()
191 .map_err(|_| DateError::InvalidIsoString(iso.to_owned()))?;
192 let month = caps["month"]
193 .parse()
194 .map_err(|_| DateError::InvalidIsoString(iso.to_owned()))?;
195 let day = caps["day"]
196 .parse()
197 .map_err(|_| DateError::InvalidIsoString(iso.to_owned()))?;
198 Date::new(year, month, day)
199 }
200
201 pub fn from_days_since_j2000(days: i64) -> Self {
204 let calendar = if days < LAST_JULIAN_DAY_J2K {
205 if days > LAST_PROLEPTIC_JULIAN_DAY_J2K {
206 Calendar::Julian
207 } else {
208 Calendar::ProlepticJulian
209 }
210 } else {
211 Calendar::Gregorian
212 };
213
214 let year = find_year(calendar, days);
215 let leap = is_leap_year(calendar, year);
216 let day_of_year = (days - last_day_of_year_j2k(calendar, year - 1)) as u16;
217 let month = find_month(day_of_year, leap);
218 let day = find_day(day_of_year, month, leap).unwrap_or_else(|err| {
219 unreachable!("{} is not a valid day of the year: {}", day_of_year, err)
220 });
221
222 Date {
223 calendar,
224 year,
225 month,
226 day,
227 }
228 }
229
230 pub fn from_seconds_since_j2000(seconds: i64) -> Self {
233 let seconds = seconds + SECONDS_PER_HALF_DAY;
234 let mut time = seconds % SECONDS_PER_DAY;
235 if time < 0 {
236 time += SECONDS_PER_DAY;
237 }
238 let days = (seconds - time) / SECONDS_PER_DAY;
239 Self::from_days_since_j2000(days)
240 }
241
242 pub fn from_day_of_year(year: i64, day_of_year: u16) -> Result<Self, DateError> {
249 let calendar = calendar(year, 1, 1);
250 let leap = is_leap_year(calendar, year);
251 let month = find_month(day_of_year, leap);
252 let day = find_day(day_of_year, month, leap)?;
253
254 Ok(Date {
255 calendar,
256 year,
257 month,
258 day,
259 })
260 }
261
262 pub const fn j2000_day_number(&self) -> i64 {
264 j2000_day_number(self.calendar, self.year, self.month, self.day)
265 }
266
267 pub const fn to_delta(&self) -> TimeDelta {
269 let seconds = self.j2000_day_number() * SECONDS_PER_DAY - SECONDS_PER_HALF_DAY;
270 TimeDelta::from_seconds(seconds)
271 }
272}
273
274impl JulianDate for Date {
275 fn julian_date(&self, epoch: Epoch, unit: Unit) -> f64 {
276 self.to_delta().julian_date(epoch, unit)
277 }
278}
279
280fn find_year(calendar: Calendar, j2000day: i64) -> i64 {
281 match calendar {
282 Calendar::ProlepticJulian => -((-4 * j2000day - 2920488) / 1461),
283 Calendar::Julian => -((-4 * j2000day - 2921948) / 1461),
284 Calendar::Gregorian => {
285 let year = (400 * j2000day + 292194288) / 146097;
286 if j2000day <= last_day_of_year_j2k(Calendar::Gregorian, year - 1) {
287 year - 1
288 } else {
289 year
290 }
291 }
292 }
293}
294
295const fn last_day_of_year_j2k(calendar: Calendar, year: i64) -> i64 {
296 match calendar {
297 Calendar::ProlepticJulian => 365 * year + (year + 1) / 4 - 730123,
298 Calendar::Julian => 365 * year + year / 4 - 730122,
299 Calendar::Gregorian => 365 * year + year / 4 - year / 100 + year / 400 - 730120,
300 }
301}
302
303const fn is_leap_year(calendar: Calendar, year: i64) -> bool {
304 match calendar {
305 Calendar::ProlepticJulian | Calendar::Julian => year % 4 == 0,
306 Calendar::Gregorian => year % 4 == 0 && (year % 400 == 0 || year % 100 != 0),
307 }
308}
309
310const PREVIOUS_MONTH_END_DAY_LEAP: [u16; 12] =
311 [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335];
312
313const PREVIOUS_MONTH_END_DAY: [u16; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
314
315fn find_month(day_in_year: u16, is_leap: bool) -> u8 {
316 let offset = if is_leap { 313 } else { 323 };
317 let month = if day_in_year < 32 {
318 1
319 } else {
320 (10 * day_in_year + offset) / 306
321 };
322 month
323 .to_u8()
324 .unwrap_or_else(|| unreachable!("month could not be represented as u8: {}", month))
325}
326
327fn find_day(day_in_year: u16, month: u8, is_leap: bool) -> Result<u8, DateError> {
328 if !is_leap && day_in_year > 365 {
329 Err(DateError::NonLeapYear)
330 } else {
331 let previous_days = if is_leap {
332 PREVIOUS_MONTH_END_DAY_LEAP
333 } else {
334 PREVIOUS_MONTH_END_DAY
335 };
336 let day = day_in_year - previous_days[(month - 1) as usize];
337 Ok(day
338 .to_u8()
339 .unwrap_or_else(|| unreachable!("day could not be represented as u8: {}", day)))
340 }
341}
342
343const fn find_day_in_year(month: u8, day: u8, is_leap: bool) -> u16 {
344 let previous_days = if is_leap {
345 PREVIOUS_MONTH_END_DAY_LEAP
346 } else {
347 PREVIOUS_MONTH_END_DAY
348 };
349 day as u16 + previous_days[(month - 1) as usize]
350}
351
352const fn calendar(year: i64, month: u8, day: u8) -> Calendar {
353 if year < 1583 {
354 if year < 1 {
355 Calendar::ProlepticJulian
356 } else if year < 1582 || month < 10 || (month < 11 && day < 5) {
357 Calendar::Julian
358 } else {
359 Calendar::Gregorian
360 }
361 } else {
362 Calendar::Gregorian
363 }
364}
365
366const fn j2000_day_number(calendar: Calendar, year: i64, month: u8, day: u8) -> i64 {
367 let d1 = last_day_of_year_j2k(calendar, year - 1);
368 let d2 = find_day_in_year(month, day, is_leap_year(calendar, year));
369 d1 + d2 as i64
370}
371
372pub trait CalendarDate {
374 fn date(&self) -> Date;
376
377 fn year(&self) -> i64 {
379 self.date().year()
380 }
381
382 fn month(&self) -> u8 {
384 self.date().month()
385 }
386
387 fn day(&self) -> u8 {
389 self.date().day()
390 }
391
392 fn day_of_year(&self) -> u16 {
394 let date = self.date();
395 let leap = is_leap_year(date.calendar(), date.year());
396 find_day_in_year(date.month(), date.day(), leap)
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use crate::f64::consts::{DAYS_PER_JULIAN_CENTURY, SECONDS_PER_JULIAN_CENTURY};
403 use rstest::rstest;
404
405 use super::*;
406
407 #[rstest]
408 #[case::equal_same_calendar(Date { calendar: Calendar::Gregorian, year: 2000, month: 1, day: 1}, Date { calendar: Calendar::Gregorian, year: 2000, month: 1, day: 1}, Ordering::Equal)]
409 #[case::equal_different_calendar(Date { calendar: Calendar::Gregorian, year: 2000, month: 1, day: 1}, Date { calendar: Calendar::Julian, year: 2000, month: 1, day: 1}, Ordering::Equal)]
410 #[case::less_than_year(Date { calendar: Calendar::Gregorian, year: 1999, month: 1, day: 1}, Date { calendar: Calendar::Gregorian, year: 2000, month: 1, day: 1}, Ordering::Less)]
411 #[case::less_than_month(Date { calendar: Calendar::Gregorian, year: 2000, month: 1, day: 1}, Date { calendar: Calendar::Gregorian, year: 2000, month: 2, day: 1}, Ordering::Less)]
412 #[case::less_than_day(Date { calendar: Calendar::Gregorian, year: 2000, month: 1, day: 1}, Date { calendar: Calendar::Gregorian, year: 2000, month: 1, day: 2}, Ordering::Less)]
413 #[case::greater_than_year(Date { calendar: Calendar::Gregorian, year: 2001, month: 1, day: 1}, Date { calendar: Calendar::Gregorian, year: 2000, month: 1, day: 1}, Ordering::Greater)]
414 #[case::greater_than_month(Date { calendar: Calendar::Gregorian, year: 2000, month: 2, day: 1}, Date { calendar: Calendar::Gregorian, year: 2000, month: 1, day: 1}, Ordering::Greater)]
415 #[case::greater_than_day(Date { calendar: Calendar::Gregorian, year: 2000, month: 1, day: 2}, Date { calendar: Calendar::Gregorian, year: 2000, month: 1, day: 1}, Ordering::Greater)]
416 fn test_date_ord(#[case] lhs: Date, #[case] rhs: Date, #[case] expected: Ordering) {
417 assert_eq!(expected, lhs.cmp(&rhs));
418 }
419
420 #[rstest]
421 #[case::j2000("2000-01-01", Date { calendar: Calendar::Gregorian, year: 2000, month: 1, day: 1})]
422 #[case::j2000("0000-01-01", Date { calendar: Calendar::ProlepticJulian, year: 0, month: 1, day: 1})]
423 fn test_date_iso(#[case] str: &str, #[case] expected: Date) {
424 let actual = Date::from_iso(str).expect("date should parse");
425 assert_eq!(actual, expected);
426 }
427
428 #[test]
429 fn test_date_unchecked() {
430 let date = Date::new_unchecked(2026, 2, 11);
431 assert_eq!(date.calendar, Calendar::Gregorian);
432 assert_eq!(date.year, 2026);
433 assert_eq!(date.month, 2);
434 assert_eq!(date.day, 11);
435 }
436
437 #[test]
438 fn test_date_from_day_of_year() {
439 let date = Date::from_day_of_year(2000, 366).unwrap();
440 assert_eq!(date.year(), 2000);
441 assert_eq!(date.month(), 12);
442 assert_eq!(date.day(), 31);
443 }
444
445 #[test]
446 fn test_date_from_invalid_day_of_year() {
447 let actual = Date::from_day_of_year(2001, 366);
448 let expected = Err(DateError::NonLeapYear);
449 assert_eq!(actual, expected);
450 }
451
452 #[test]
453 fn test_date_jd_epoch() {
454 let date = Date::default();
455 assert_eq!(date.days_since_julian_epoch(), 2451544.5);
456 }
457
458 #[test]
459 fn test_date_julian_date() {
460 let date = Date::default();
461 assert_eq!(date.days_since_julian_epoch(), 2451544.5);
462
463 let date = Date::new(2100, 1, 1).unwrap();
464 assert_eq!(
465 date.seconds_since_j2000(),
466 SECONDS_PER_JULIAN_CENTURY - SECONDS_PER_HALF_DAY as f64
467 );
468 assert_eq!(date.days_since_j2000(), DAYS_PER_JULIAN_CENTURY - 0.5);
469 assert_eq!(
470 date.centuries_since_j2000(),
471 1.0 - 0.5 / DAYS_PER_JULIAN_CENTURY
472 );
473 assert_eq!(
474 date.centuries_since_j1950(),
475 1.5 - 0.5 / DAYS_PER_JULIAN_CENTURY
476 );
477 assert_eq!(
478 date.centuries_since_modified_julian_epoch(),
479 2.411211498973306 - 0.5 / DAYS_PER_JULIAN_CENTURY
480 );
481 assert_eq!(
482 date.centuries_since_julian_epoch(),
483 68.11964407939767 - 0.5 / DAYS_PER_JULIAN_CENTURY
484 );
485 }
486}