nichi/
date.rs

1//---------------------------------------------------------------------------------------------------- Use
2use crate::weekday::Weekday;
3use crate::year::Year;
4use crate::month::Month;
5use crate::day::Day;
6use once_cell::sync::Lazy;
7use regex::Regex;
8
9//---------------------------------------------------------------------------------------------------- Date
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
12#[derive(Copy,Clone,Debug,Default,PartialEq,PartialOrd,Eq,Ord,Hash)]
13/// Calendar date
14pub struct Date {
15	year: Year,
16	month: Month,
17	day: Day,
18}
19
20//---------------------------------------------------------------------------------------------------- Impl
21impl Date {
22	#[inline]
23	/// Create a new [`Date`] from numbers
24	///
25	/// ## Panics
26	/// This function panics if:
27	/// - `month == 0`
28	/// - `month > 12`
29	/// - `day == 0`
30	/// - `day < 32`
31	///
32	/// ```rust,should_panic
33	/// # use nichi::*;
34	/// Date::new(2000, 0, 0);
35	/// ```
36	/// ```rust,should_panic
37	/// # use nichi::*;
38	/// Date::new(2000, 1, 32);
39	/// ```
40	/// ```rust,should_panic
41	/// # use nichi::*;
42	/// Date::new(2000, 13, 31);
43	/// ```
44	pub const fn new(year: i16, month: u8, day: u8) -> Self {
45		assert!(month != 0, "month was 0");
46		assert!(month < 13, "month was greater than 13");
47		assert!(day != 0, "day was 0");
48		assert!(day < 32, "day was greater than 31");
49
50		Self { year: Year(year), month: Month::new(month), day: Day::new(day) }
51	}
52
53	#[inline]
54	/// ```rust
55	/// # use nichi::*;
56	/// let date = Date::new_saturating(2000, 0, 0);
57	/// assert_eq!(date.inner(), (2000, 1, 1));
58	///
59	/// let date = Date::new_saturating(2000, 13, 32);
60	/// assert_eq!(date.inner(), (2000, 12, 31));
61	/// ```
62	pub const fn new_saturating(year: i16, month: u8, day: u8) -> Self {
63		Self { year: Year(year), month: Month::new_saturating(month), day: Day::new_saturating(day) }
64	}
65
66	#[inline]
67	/// ```rust
68	/// # use nichi::*;
69	/// // Year does not wrap.
70	///
71	/// let date = Date::new_wrapping(2000, 0, 0);
72	/// assert_eq!(date.inner(), (2000, 12, 31));
73	///
74	/// let date = Date::new_wrapping(2000, 13, 32);
75	/// assert_eq!(date.inner(), (2000, 1, 1));
76	/// ```
77	pub const fn new_wrapping(year: i16, month: u8, day: u8) -> Self {
78		Self { year: Year(year), month: Month::new_wrapping(month), day: Day::new_wrapping(day) }
79	}
80
81	#[inline]
82	/// Create a new [`Date`] from typed [`Year`], [`Month`], and [`Day`]
83	///
84	/// ```rust
85	/// # use nichi::*;
86	/// let date = Date::new_typed(
87	/// 	Year(2000),
88	/// 	Month::December,
89	/// 	Day::TwentyFifth
90	/// );
91	///
92	/// // Christmas in the year 2000 was on a Monday.
93	/// assert_eq!(date.weekday(), Weekday::Monday);
94	/// ```
95	pub const fn new_typed(year: Year, month: Month, day: Day) -> Self {
96		Self { year, month, day }
97	}
98
99	#[inline]
100	/// Receive the corresponding [`Weekday`] of this [`Date`].
101	///
102	/// It is accurate for any [`Date`].
103	///
104	/// ```rust
105	/// # use nichi::{Date,Weekday};
106	/// // US Independence day was on a Thursday.
107	/// assert_eq!(Date::new(1776, 7, 4).weekday(), Weekday::Thursday);
108	///
109	/// // Nintendo Switch was released on a Friday.
110	/// assert_eq!(Date::new(2017, 3, 3).weekday(), Weekday::Friday);
111	///
112	/// // Christmas in 1999 was on a Saturday.
113	/// assert_eq!(Date::new(1999, 12, 25).weekday(), Weekday::Saturday);
114	///
115	/// // A good album was released on a Wednesday.
116	/// assert_eq!(Date::new(2018, 4, 25).weekday(), Weekday::Wednesday);
117	/// ```
118	///
119	/// ## Algorithm
120	/// This uses [Tomohiko Sakamoto's](https://en.wikipedia.org/wiki/Determination_of_the_day_of_the_week#Sakamoto's_methods) algorithm.
121	pub const fn weekday(self) -> Weekday {
122		Self::weekday_raw(self.year.inner(), self.month.inner(), self.day.inner())
123	}
124
125	#[inline]
126	/// Same as [`Date::weekday`] but with raw number primitives
127	pub const fn weekday_raw(year: i16, month: u8, day: u8) -> Weekday {
128		let month: isize = month as isize - 1;
129		debug_assert!(month >= 0);
130		debug_assert!(month < 12);
131
132		let year = if month < 2 {
133			year.saturating_sub(1)
134		} else {
135			year
136		};
137
138		const LUT: [i16; 12] = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
139
140		// SAFETY: indexing is not const, so we must
141		// "index" by using pointer offsetting.
142		let lut: i16 = unsafe { std::ptr::read(LUT.as_ptr().offset(month)) };
143		debug_assert!(lut < 12);
144
145		let weekday: i16 = (year + year/4 - year/100 + year/400 + lut + day as i16) % 7;
146		debug_assert!(weekday < 7);
147
148		// SAFETY: indexing is not const, so we must
149		// "index" by using pointer offsetting.
150		unsafe { std::ptr::read(Weekday::ALL.as_ptr().offset(weekday as isize)) }
151	}
152
153	/// ```rust
154	/// # use nichi::*;
155	/// let date = Date::new(2000, 12, 25);
156	/// let ((year, month, day)) = date.inner();
157	///
158	/// assert_eq!((year, month, day), (2000, 12, 25));
159	/// ```
160	pub const fn inner(self) -> (i16, u8, u8) {
161		(self.year.inner(), self.month.inner(), self.day.inner())
162	}
163
164	/// ```rust
165	/// # use nichi::*;
166	/// let date = Date::new(2000, 12, 25);
167	/// assert_eq!(date.inner_typed(), (Year(2000), Month::new(12), Day::new(25)));
168	/// ```
169	pub const fn inner_typed(self) -> (Year, Month, Day) {
170		(self.year, self.month, self.day)
171	}
172
173	/// ```rust
174	/// # use nichi::*;
175	/// let date = Date::new(2000, 12, 25);
176	/// assert_eq!(date.year(), 2000);
177	/// ```
178	pub const fn year(self) -> Year {
179		self.year
180	}
181
182	/// ```rust
183	/// # use nichi::*;
184	/// let date = Date::new(2000, 12, 25);
185	/// assert_eq!(date.month(), 12);
186	/// ```
187	pub const fn month(self) -> Month {
188		self.month
189	}
190
191	/// ```rust
192	/// # use nichi::*;
193	/// let date = Date::new(2000, 12, 25);
194	/// assert_eq!(date.day(), 25);
195	/// ```
196	pub const fn day(self) -> Day {
197		self.day
198	}
199
200	#[inline]
201	/// Create [`Date`] from a string
202	///
203	/// ## Invariants
204	/// - The year must be `1000..=9999`
205	/// - The month must be at least the first 3 letters of the month in english (`oct`, `Dec`, `SEP`, etc)
206	/// - The day must be a number, either optionally with a leading `0` or suffixed by `th`, `rd`, `nd`, `st` (but not both, e.g, `3rd` is OK, `03` is OK, `03rd` is INVALID)
207	///
208	/// The order of the `year`, `month`, and `day` do not matter:
209	/// ```rust
210	/// # use nichi::*;
211	/// let december_25th_2010 = Date::new(2010, 12, 25);
212	/// assert_eq!(Date::from_str("dec 25 2010").unwrap(), december_25th_2010);
213	/// assert_eq!(Date::from_str("2010 dec 25").unwrap(), december_25th_2010);
214	/// assert_eq!(Date::from_str("2010 25th Dec").unwrap(), december_25th_2010);
215	/// assert_eq!(Date::from_str("25TH 2010 DEC").unwrap(), december_25th_2010);
216	/// ```
217	///
218	/// Infinite amount of separator characters are allowed:
219	/// ```rust
220	/// # use nichi::*;
221	/// let december_25th_2010 = Date::new(2010, 12, 25);
222	/// assert_eq!(Date::from_str("dec-25 ...       2010").unwrap(), december_25th_2010);
223	/// ```
224	///
225	/// This function is extremely lenient, as long as some resemblance of a
226	/// calendar date is in the input string, it will parse it out:
227	/// ```rust
228	/// # use nichi::*;
229	/// //                                            Year 2010
230	/// //                                  25th day      |
231	/// //                         December     |         |
232	/// //                            |         |         |
233	/// assert_eq!( //                v         v         v
234	/// 	Date::from_str("----fasdf decBR wef 25 a - >.a2010a...aa").unwrap(),
235	/// 	Date::new(2010, 12, 25),
236	/// );
237	/// ```
238	///
239	/// ## ISO 8601 (like)
240	/// This function also parses `ISO 8601`-like dates.
241	///
242	/// The `year`, `month`, and `day` must be available in that order.
243	///
244	/// A single separator character must exist, although it does not need to be `-`.
245	///
246	/// ```rust
247	/// # use nichi::*;
248	/// assert_eq!(Date::from_str("2010-12-25").unwrap(), Date::new(2010, 12, 25));
249	/// assert_eq!(Date::from_str("2010.02.02").unwrap(), Date::new(2010, 2, 2));
250	/// assert_eq!(Date::from_str("2010/2/2").unwrap(),   Date::new(2010, 2, 2));
251	/// assert_eq!(Date::from_str("2010_02_2").unwrap(),  Date::new(2010, 2, 2));
252	/// assert_eq!(Date::from_str("2010 2 02").unwrap(),  Date::new(2010, 2, 2));
253	/// ```
254	///
255	/// ## Examples
256	/// ```rust
257	/// # use nichi::*;
258	/// let december_25th_2010 = Date::new(2010, 12, 25);
259	///
260	/// assert_eq!(Date::from_str("dec, 25, 2010").unwrap(),        december_25th_2010);
261	/// assert_eq!(Date::from_str("dec 25 2010").unwrap(),          december_25th_2010);
262	/// assert_eq!(Date::from_str("Dec 25th 2010").unwrap(),        december_25th_2010);
263	/// assert_eq!(Date::from_str("DEC 25TH 2010").unwrap(),        december_25th_2010);
264	/// assert_eq!(Date::from_str("DEC-25th-2010").unwrap(),        december_25th_2010);
265	/// assert_eq!(Date::from_str("2010.dec.25").unwrap(),          december_25th_2010);
266	/// assert_eq!(Date::from_str("2010, 25th, Dec").unwrap(),      december_25th_2010);
267	/// assert_eq!(Date::from_str("2010 december 25th").unwrap(),   december_25th_2010);
268	/// assert_eq!(Date::from_str("2010, DECEMBER, 25th").unwrap(), december_25th_2010);
269	/// assert_eq!(Date::from_str("DECEMBER 25th 2010").unwrap(),   december_25th_2010);
270	/// assert_eq!(Date::from_str("December 25th, 2010").unwrap(),  december_25th_2010);
271	///
272	/// let april_3rd_1000 = Date::new(1000, 4, 3);
273	/// assert_eq!(Date::from_str("apr, 3, 1000").unwrap(),     april_3rd_1000);
274	/// assert_eq!(Date::from_str("apr 03 1000").unwrap(),      april_3rd_1000);
275	/// assert_eq!(Date::from_str("Apr 3rd 1000").unwrap(),    april_3rd_1000);
276	/// assert_eq!(Date::from_str("APR 3RD 1000").unwrap(),     april_3rd_1000);
277	/// assert_eq!(Date::from_str("APR-3RD-1000").unwrap(),    april_3rd_1000);
278	/// assert_eq!(Date::from_str("1000.apr.03").unwrap(),      april_3rd_1000);
279	/// assert_eq!(Date::from_str("1000, 3rd, Apr").unwrap(),   april_3rd_1000);
280	/// assert_eq!(Date::from_str("1000 april 3rd").unwrap(),  april_3rd_1000);
281	/// assert_eq!(Date::from_str("1000, APRIL, 3RD").unwrap(), april_3rd_1000);
282	/// assert_eq!(Date::from_str("APRIL 3rd 1000").unwrap(),   april_3rd_1000);
283	/// assert_eq!(Date::from_str("April 3rd, 1000").unwrap(), april_3rd_1000);
284	/// ```
285	pub fn from_str(s: &str) -> Option<Self> {
286		// Debug.
287		// println!("{s}");
288
289		// ISO 8601
290		static ISO: Lazy<Regex> = Lazy::new(|| Regex::new(r"[1-9]\d{3}.\d{1,2}.\d{1,2}").unwrap());
291		static ISO_1: Lazy<Regex> = Lazy::new(|| Regex::new(r"[1-9]\d{3}.(0[1-9]|1[012]).(0[1-9]|[12][0-9]|30|31)").unwrap());
292		static ISO_2: Lazy<Regex> = Lazy::new(|| Regex::new(r"[1-9]\d{3}.[1-9].(0[1-9]|[12][0-9]|30|31)").unwrap());
293		static ISO_3: Lazy<Regex> = Lazy::new(|| Regex::new(r"[1-9]\d{3}.(0[1-9]|1[012]).[1-9]").unwrap());
294		static ISO_4: Lazy<Regex> = Lazy::new(|| Regex::new(r"[1-9]\d{3}.[1-9].[1-9]").unwrap());
295
296		// If ISO matches, attempt that first.
297		if ISO.is_match(s) {
298			// Debug
299			// println!("iso {s}");
300
301			// xxxx-xx-xx
302			if let Some(m) = ISO_1.find(s) {
303				// println!("iso2 {m:?}");
304				let s = m.as_str();
305				let b = s.as_bytes();
306				let year  = s[0..4].parse::<i16>().unwrap();
307				let month = Month::from_bytes(&b[5..7]).unwrap();
308				let day   = Day::from_bytes(&b[8..10]).unwrap();
309				return Some(Self { year: Year(year), month, day })
310			// xxxx-x-xx
311			} else if let Some(m) = ISO_2.find(s) {
312				// println!("iso2 {m:?}");
313				let s = m.as_str();
314				let b = s.as_bytes();
315				let year  = s[0..4].parse::<i16>().unwrap();
316				let month = Month::from_bytes(&b[5..6]).unwrap();
317				let day   = Day::from_bytes(&b[7..9]).unwrap();
318				return Some(Self { year: Year(year), month, day })
319			// xxxx-xx-x
320			} else if let Some(m) = ISO_3.find(s) {
321				// println!("iso3 {m:?}");
322				let s = m.as_str();
323				let b = s.as_bytes();
324				let year  = s[0..4].parse::<i16>().unwrap();
325				let month = Month::from_bytes(&b[5..7]).unwrap();
326				let day   = Day::from_bytes(&b[8..9]).unwrap();
327				return Some(Self { year: Year(year), month, day })
328			// xxxx-x-x
329			} else if let Some(m) = ISO_4.find(s) {
330				// println!("iso4 {m:?}");
331				let s = m.as_str();
332				let b = s.as_bytes();
333				let year  = s[0..4].parse::<i16>().unwrap();
334				let month = Month::from_bytes(&b[5..6]).unwrap();
335				let day   = Day::from_bytes(&b[7..8]).unwrap();
336				return Some(Self { year: Year(year), month, day })
337			}
338		}
339
340		// Year.
341		static YEAR: Lazy<Regex> = Lazy::new(|| Regex::new(r"\d{4}").unwrap());
342		// Month.
343		static MONTH: Lazy<Regex> = Lazy::new(|| Regex::new(
344r"Jan|jan|JAN|Feb|feb|FEB|Mar|mar|MAR|Apr|apr|APR|Jun|jun|JUN|Jul|jul|JUL|Aug|aug|AUG|Sep|sep|SEP|Oct|oct|OCT|Nov|nov|NOV|Dec|dec|DEC"
345		).unwrap());
346		// Day.
347		// 2 numbers followed by 2 [A-Za-z] OR 2 numbers
348		static DAY: Lazy<Regex> = Lazy::new(|| Regex::new(r"(\d{2}|\d{1})[A-Za-z]{2}|\b\d{2}\b|\b\d{1}\b").unwrap());
349
350		// Attempt year.
351		let Some(year) = YEAR.find(s) else {
352			return None;
353		};
354		let Some(year) = Year::from_str(year.as_str()) else {
355			return None;
356		};
357
358		// Attempt month.
359		let Some(month) = MONTH.find(s) else {
360			return None;
361		};
362		let Some(month) = Month::from_str(month.as_str()) else {
363			return None;
364		};
365
366		// Attempt day.
367		let Some(day) = DAY.find(s) else {
368			return None;
369		};
370		let Some(day) = Day::from_str(day.as_str()) else {
371			return None;
372		};
373
374		Some(Self { year, month, day })
375	}
376
377	#[inline]
378	/// Convert a UNIX timestamp into a [`Date`]
379	///
380	/// This converts a UNIX timestamp into a calendar day, assuming UTC.
381	///
382	/// It is the reverse of [`Date::as_unix`].
383	///
384	/// The input is relative to the `UNIX_EPOCH`, for example:
385	/// ```rust
386	/// # use nichi::*;
387	/// // 0 is the UNIX_EPOCH
388	/// assert_eq!(Date::from_unix(0),     Date::new(1970, 1, 1));
389	/// assert_eq!(Date::from_unix(86399), Date::new(1970, 1, 1));
390	///
391	/// // 1 day after.
392	/// assert_eq!(Date::from_unix(86400), Date::new(1970, 1, 2));
393	/// ```
394	///
395	/// A negative input will return dates before the `UNIX_EPOCH`:
396	/// ```rust
397	/// # use nichi::*;
398	/// assert_eq!(Date::from_unix(-1),     Date::new(1969, 12, 31));
399	/// assert_eq!(Date::from_unix(-86400), Date::new(1969, 12, 30));
400	/// ```
401	///
402	/// ## Example
403	/// ```rust
404	/// # use nichi::*;
405	/// let date = Date::from_unix(1697760000);
406	/// assert_eq!(date, Date::new(2023, 10, 20));
407	///
408	/// assert_eq!(date.as_unix(), 1697760000);
409	/// ```
410	///
411	/// ## Algorithm
412	/// <https://howardhinnant.github.io/date_algorithms.html#civil_from_days>
413	pub const fn from_unix(seconds_relative_to_unix_epoch: i128) -> Self {
414		let s = if seconds_relative_to_unix_epoch.is_negative() {
415			seconds_relative_to_unix_epoch - 86400
416		} else {
417			seconds_relative_to_unix_epoch
418		};
419
420		let z:   i64 = ((s / 86400) + 719468) as i64;
421		let era: i64 = if z >= 0 { z } else { z - 146096 } / 146097;
422		let doe: u64 = (z - era * 146097) as u64;
423		let yoe: u64 = (doe - doe/1460 + doe/36524 - doe/146096) / 365;
424		let y:   i64 = (yoe as i64) + era * 400;
425		let doy: u64 = doe - (365*yoe + yoe/4 - yoe/100);
426		let mp:  u64 = (5*doy + 2)/153;
427		let d:   u8  = (doy - (153*mp+2)/5 + 1) as u8;
428		let m:   u8  = (if mp < 10 { mp + 3 } else { mp - 9 }) as u8;
429
430		debug_assert!(m != 0);
431		debug_assert!(m < 13);
432		debug_assert!(d != 0);
433		debug_assert!(d < 32);
434
435		let y = if m <= 2 {
436			y + 1
437		} else {
438			y
439		};
440
441		let y = if y > i16::MAX as i64 {
442			Year::MAX
443		} else if y < i16::MIN as i64 {
444			Year::MIN
445		} else {
446			Year(y as i16)
447		};
448
449		unsafe { Self {
450			year: y,
451			month: Month::new_unchecked(m),
452			day: Day::new_unchecked(d)
453		}}
454	}
455
456	#[inline]
457	/// Convert a [`Date`] to a UNIX timestamp
458	///
459	/// This converts a calendar day into a UNIX timestamp, assuming UTC.
460	///
461	/// It is the reverse of [`Date::from_unix`].
462	///
463	/// A negative `year` represents BCE years, e.g, `-1` is `1 BCE`.
464	///
465	/// Values before the `UNIX_EPOCH` (before `January 1st, 1970`) will return negative values.
466	///
467	/// ```rust
468	/// # use nichi::*;
469	/// let date = Date::new(2023, 10, 20);
470	/// assert_eq!(date.as_unix(), 1697760000);
471	///
472	/// assert_eq!(date, Date::from_unix(date.as_unix()));
473	/// ```
474	///
475	/// ## Algorithm
476	/// <https://howardhinnant.github.io/date_algorithms.html#days_from_civil>
477	pub const fn as_unix(self) -> i128 {
478		let (year, month, day) = self.inner();
479
480		let year  = year as i64;
481		let month = month as u64;
482		let day   = day as u64;
483
484		let year = if month <= 2 {
485			year - 1
486		} else {
487			year
488		};
489
490		let era: i64 = if year >= 0 { year } else { year - 399 } / 400;
491		let yoe: u64 = (year - era * 400) as u64;
492		let doy: u64 = (153 * if month > 2 { month - 3 } else { month + 9 } + 2) / 5 + day - 1;
493		let doe: u64 = yoe * 365 + yoe/4 - yoe/100 + doy;
494
495		((era as i128) * 146097 + (doe as i128) - 719468) * 86400
496	}
497}
498
499//---------------------------------------------------------------------------------------------------- Impl