utc2k/
lib.rs

1/*!
2# UTC2K
3
4[![docs.rs](https://img.shields.io/docsrs/utc2k.svg?style=flat-square&label=docs.rs)](https://docs.rs/utc2k/)
5[![changelog](https://img.shields.io/crates/v/utc2k.svg?style=flat-square&label=changelog&color=9b59b6)](https://github.com/Blobfolio/utc2k/blob/master/CHANGELOG.md)<br>
6[![crates.io](https://img.shields.io/crates/v/utc2k.svg?style=flat-square&label=crates.io)](https://crates.io/crates/utc2k)
7[![ci](https://img.shields.io/github/actions/workflow/status/Blobfolio/utc2k/ci.yaml?style=flat-square&label=ci)](https://github.com/Blobfolio/utc2k/actions)
8[![deps.rs](https://deps.rs/crate/utc2k/latest/status.svg?style=flat-square&label=deps.rs)](https://deps.rs/crate/utc2k/)<br>
9[![license](https://img.shields.io/badge/license-wtfpl-ff1493?style=flat-square)](https://en.wikipedia.org/wiki/WTFPL)
10[![contributions welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square&label=contributions)](https://github.com/Blobfolio/utc2k/issues)
11
12UTC2K is a heavily-optimized — and extremely niche — date/time library that **only supports UTC happenings in _this century_**.
13
14For the moments between `2000-01-01 00:00:00..=2099-12-31 23:59:59`, it can run circles around crates like [`chrono`](https://crates.io/crates/chrono) and [`time`](https://crates.io/crates/time), while still being able to:
15
16* Determine "now", at least until the final seconds of 2099;
17* Convert to/from Unix timestamps;
18* Convert to/from all sorts of different date/time strings;
19* Perform checked and saturating addition/subtraction;
20* Calculate ordinals, weekdays, leap years, etc.;
21
22
23
24## Examples
25
26The library's main export is [`Utc2k`], a `Copy`-friendly struct representing a specific UTC datetime.
27
28```
29use utc2k::{Utc2k, Weekday};
30
31// Instantiation, four ways:
32let date = Utc2k::now();                             // The current system time.
33let date = Utc2k::new(2020, 1, 2, 12, 30, 30);       // From parts.
34let date = Utc2k::from_unixtime(4_102_444_799);      // From a timestamp.
35let date = Utc2k::from_ascii(b"2024-10-31 00:00:00") // From a datetime string.
36               .unwrap();
37
38// What day was Halloween 2024, anyway?
39assert_eq!(
40    date.weekday(),
41    Weekday::Thursday,
42);
43
44// Ordinals are a kind of bird, right?
45assert_eq!(
46    date.ordinal(),
47    305,
48);
49
50// Boss wants an RFC2822 for some reason?
51assert_eq!(
52    date.to_rfc2822(),
53    "Thu, 31 Oct 2024 00:00:00 +0000",
54);
55```
56
57
58
59## Optional Crate Features
60
61* `local`: Enables the [`Local2k`]/[`FmtLocal2k`] structs. Refer to the documentation for important caveats and limitations.
62* `serde`: Enables serialization/deserialization support.
63* `sqlx-mysql`: Enables [`sqlx`](https://crates.io/crates/sqlx) (mysql) support for [`Utc2k`].
64*/
65
66#![deny(
67	clippy::allow_attributes_without_reason,
68	clippy::correctness,
69	unreachable_pub,
70	unsafe_code,
71)]
72
73#![warn(
74	clippy::complexity,
75	clippy::nursery,
76	clippy::pedantic,
77	clippy::perf,
78	clippy::style,
79
80	clippy::allow_attributes,
81	clippy::clone_on_ref_ptr,
82	clippy::create_dir,
83	clippy::filetype_is_file,
84	clippy::format_push_string,
85	clippy::get_unwrap,
86	clippy::impl_trait_in_params,
87	clippy::implicit_clone,
88	clippy::lossy_float_literal,
89	clippy::missing_assert_message,
90	clippy::missing_docs_in_private_items,
91	clippy::needless_raw_strings,
92	clippy::panic_in_result_fn,
93	clippy::pub_without_shorthand,
94	clippy::rest_pat_in_fully_bound_structs,
95	clippy::semicolon_inside_block,
96	clippy::str_to_string,
97	clippy::todo,
98	clippy::undocumented_unsafe_blocks,
99	clippy::unneeded_field_pattern,
100	clippy::unseparated_literal_suffix,
101	clippy::unwrap_in_result,
102
103	macro_use_extern_crate,
104	missing_copy_implementations,
105	missing_docs,
106	non_ascii_idents,
107	trivial_casts,
108	trivial_numeric_casts,
109	unused_crate_dependencies,
110	unused_extern_crates,
111	unused_import_braces,
112)]
113
114#![expect(clippy::redundant_pub_crate, reason = "Unresolvable.")]
115
116#![cfg_attr(docsrs, feature(doc_cfg))]
117
118
119
120mod chr;
121mod date;
122mod error;
123mod month;
124mod period;
125mod weekday;
126mod year;
127
128mod macros;
129
130#[cfg(any(test, feature = "serde"))]
131#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
132mod serde;
133
134#[cfg(feature = "sqlx-mysql")]
135#[cfg_attr(docsrs, doc(cfg(feature = "sqlx-mysql")))]
136mod sqlx;
137
138
139
140use chr::DateChar;
141pub use date::{
142	FmtUtc2k,
143	Utc2k,
144};
145pub use error::{
146	Utc2kError,
147	Utc2kFormatError,
148};
149pub use month::Month;
150pub use period::Period;
151pub use weekday::Weekday;
152use year::Year;
153
154#[cfg(feature = "local")]
155#[cfg_attr(docsrs, doc(cfg(feature = "local")))]
156pub use date::local::{
157	FmtLocal2k,
158	Local2k,
159};
160
161#[cfg(test)] use brunch as _;
162
163
164/// # Seconds per Minute.
165pub const MINUTE_IN_SECONDS: u32 = 60;
166
167/// # Seconds per Hour.
168pub const HOUR_IN_SECONDS: u32 = 3600;
169
170/// # Seconds per Day.
171pub const DAY_IN_SECONDS: u32 = 86_400;
172
173/// # Seconds per Week.
174pub const WEEK_IN_SECONDS: u32 = 604_800;
175
176/// # Seconds per (Normal) Year.
177pub const YEAR_IN_SECONDS: u32 = 31_536_000;
178
179/// # ASCII Lower Mask.
180///
181/// This mask is used to unconditionally lowercase the last three bytes of a
182/// (LE) `u32` so we can case-insensitively match (alphabetic-only) month,
183/// weekday, and offset abbreviations.
184const ASCII_LOWER: u32 = 0x2020_2000;
185
186/// # Julian Day Offset.
187///
188/// The offset in days between JD0 and 1 March 1BC, necessary since _someone_
189/// forgot to invent 0AD. Haha.
190///
191/// (Only used when calendarizing timestamps.)
192const JULIAN_OFFSET: u32 = 2_440_588 - 1_721_119;
193
194/// # Days per Year (Rounded to Two Decimals).
195///
196/// The average number of days per year, rounded to two decimal places (and
197/// multiplied by 100).
198///
199/// (Only used when calendarizing timestamps.)
200const YEAR_IN_DAYS_P2: u32 = 36_525; // 365.25
201
202/// # Days per Year (Rounded to Four Decimals).
203///
204/// The average number of days per year, rounded to four decimal places (and
205/// multiplied by 10,000).
206///
207/// (Only used when calendarizing timestamps.)
208const YEAR_IN_DAYS_P4: u32 = 3_652_425; // 365.2425
209
210
211
212#[expect(
213	clippy::cast_lossless,
214	clippy::cast_possible_truncation,
215	reason = "False positive.",
216)]
217#[must_use]
218/// # Now (Current Unixtime).
219///
220/// This returns the current unix timestamp as a `u32`.
221///
222/// Rather than panic on out-of-range values — in the event the system clock is
223/// broken or an archaeologist is running this in the distant future — the
224/// timetsamp will be saturated to [`Utc2k::MIN_UNIXTIME`] or
225/// [`Utc2k::MAX_UNIXTIME`].
226pub fn unixtime() -> u32 {
227	use std::time::SystemTime;
228
229	SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).map_or(
230		Utc2k::MIN_UNIXTIME,
231		|n| n.as_secs().clamp(Utc2k::MIN_UNIXTIME as u64, Utc2k::MAX_UNIXTIME as u64) as u32
232	)
233}
234
235#[expect(clippy::cast_possible_truncation, reason = "False positive.")]
236#[must_use]
237/// # Now (Current Year).
238///
239/// This returns the current year as a `u16`.
240///
241/// See [`unixtime`] for notes about system clock error recovery.
242///
243/// ## Examples
244///
245/// ```
246/// assert_eq!(
247///     utc2k::Utc2k::now().year(),
248///     utc2k::year(),
249/// );
250/// ```
251pub fn year() -> u16 {
252	// Same as Utc2k::now().year(), but stripped to the essentials.
253	let z = unixtime().wrapping_div(DAY_IN_SECONDS) + JULIAN_OFFSET;
254	let h: u32 = 100 * z - 25;
255	let mut a: u32 = h.wrapping_div(YEAR_IN_DAYS_P4);
256	a -= a.wrapping_div(4);
257	let year: u32 = (100 * a + h).wrapping_div(YEAR_IN_DAYS_P2);
258	a = a + z - 365 * year - year.wrapping_div(4);
259	let month = (5 * a + 456).wrapping_div(153);
260
261	year as u16 + u16::from(12 < month)
262}
263
264
265
266#[expect(clippy::inline_always, reason = "Foundational.")]
267#[inline(always)]
268#[must_use]
269/// # Case-Insensitive Needle.
270///
271/// This method lower cases three (presumed letters) into a single `u32` for
272/// lightweight comparison.
273///
274/// This is used for matching [`Month`] and [`Weekday`] abbreviations, and
275/// `"UTC"`/`"GMT"` offset markers.
276const fn needle3(a: u8, b: u8, c: u8) -> u32 {
277	u32::from_le_bytes([0, a, b, c]) | ASCII_LOWER
278}
279
280
281
282#[cfg(test)]
283mod test {
284	use super::*;
285	use std::time::SystemTime;
286
287	#[test]
288	fn t_needle3() {
289		// The ASCII lower bit mask is meant to apply to the last three bytes
290		// (LE).
291		assert_eq!(
292			ASCII_LOWER.to_le_bytes(),
293			[0, 0b0010_0000, 0b0010_0000, 0b0010_0000],
294		);
295
296		// We lowercase month/weekday abbreviation search needles
297		// unconditionally — non-letters won't match regardless — so just need
298		// to make sure it works for upper/lower letters.
299		assert_eq!(
300			needle3(b'J', b'E', b'B'),
301			u32::from_le_bytes([0, b'j', b'e', b'b']),
302		);
303		assert_eq!(
304			needle3(b'j', b'e', b'b'),
305			u32::from_le_bytes([0, b'j', b'e', b'b']),
306		);
307	}
308
309	#[test]
310	fn t_unixtime() {
311		// Our method.
312		let our_secs = unixtime();
313
314		// Manual construction via SystemTime.
315		let secs: u32 = SystemTime::now()
316			.duration_since(SystemTime::UNIX_EPOCH)
317			.expect("The system time is set to the deep past!")
318			.as_secs()
319			.try_into()
320			.expect("The system clock is set to the distant future!");
321
322		// The SystemTime version should fall within our range.
323		assert!(
324			(Utc2k::MIN_UNIXTIME..=Utc2k::MAX_UNIXTIME).contains(&secs),
325			"Bug: the system clock is completely wrong!",
326		);
327
328		// It should also match the `unixtime` output, but let's allow a tiny
329		// ten-second cushion in case the runner is _really_ slow.
330		assert!(
331			our_secs.abs_diff(secs) <= 10,
332			"SystemTime and unixtime are more different than expected!",
333		);
334	}
335}