utc2k/lib.rs
1/*!
2# UTC2K
3
4[](https://docs.rs/utc2k/)
5[](https://github.com/Blobfolio/utc2k/blob/master/CHANGELOG.md)<br>
6[](https://crates.io/crates/utc2k)
7[](https://github.com/Blobfolio/utc2k/actions)
8[](https://deps.rs/crate/utc2k/)<br>
9[](https://en.wikipedia.org/wiki/WTFPL)
10[](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}