utc2k/date/
local.rs

1/*!
2# UTC2K: Local Dates!
3*/
4
5use crate::{
6	DateChar,
7	DAY_IN_SECONDS,
8	FmtUtc2k,
9	HOUR_IN_SECONDS,
10	macros,
11	MINUTE_IN_SECONDS,
12	Month,
13	Utc2k,
14	Weekday,
15};
16use std::{
17	borrow::Cow,
18	cmp::Ordering,
19	fmt,
20	hash,
21	num::NonZeroI32,
22	sync::OnceLock,
23};
24use super::Abacus;
25use tz::timezone::TimeZone;
26
27
28
29/// # Parsed Timezone Details.
30static TZ: OnceLock<Option<TimeZone>> = OnceLock::new();
31
32
33
34#[derive(Debug, Clone, Copy)]
35/// # Formatted Local ~~UTC~~2K.
36///
37/// This is the formatted companion to [`Local2k`]. You can use it to obtain a
38/// string version of the date, print it, etc.
39///
40/// While this acts essentially as a glorified `String`, it is sized exactly
41/// and therefore requires less memory to represent. It also implements `Copy`.
42///
43/// It follows the simple Unix date format of `YYYY-MM-DD hh:mm:ss`.
44///
45/// Speaking of, you can obtain an `&str` using `AsRef<str>`,
46/// `Borrow<str>`, or [`FmtLocal2k::as_str`].
47///
48/// If you only want the date or time half, call [`FmtLocal2k::date`] or
49/// [`FmtLocal2k::time`] respectively.
50///
51/// See [`Local2k`] for limitations and gotchas.
52pub struct FmtLocal2k {
53	/// # Date/Time (w/ `offset`)
54	inner: FmtUtc2k,
55
56	/// # Local Offset (Seconds).
57	offset: Option<NonZeroI32>,
58}
59
60impl AsRef<[u8]> for FmtLocal2k {
61	#[inline]
62	fn as_ref(&self) -> &[u8] { self.as_bytes() }
63}
64
65macros::as_ref_borrow_cast!(FmtLocal2k: as_str str);
66
67impl fmt::Display for FmtLocal2k {
68	#[inline]
69	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70		<FmtUtc2k as fmt::Display>::fmt(&self.inner, f)
71	}
72}
73
74impl Eq for FmtLocal2k {}
75
76impl From<Local2k> for FmtLocal2k {
77	#[inline]
78	fn from(src: Local2k) -> Self { Self::from_local2k(src) }
79}
80
81impl From<FmtLocal2k> for String {
82	#[inline]
83	fn from(src: FmtLocal2k) -> Self { src.as_str().to_owned() }
84}
85
86impl hash::Hash for FmtLocal2k {
87	#[inline]
88	fn hash<H: hash::Hasher>(&self, state: &mut H) {
89		<Local2k as hash::Hash>::hash(&Local2k::from_fmtlocal2k(*self), state);
90	}
91}
92
93impl Ord for FmtLocal2k {
94	#[inline]
95	fn cmp(&self, other: &Self) -> Ordering {
96		if self.offset == other.offset { self.inner.cmp(&other.inner) }
97		else {
98			Local2k::from_fmtlocal2k(*self).cmp(&Local2k::from_fmtlocal2k(*other))
99		}
100	}
101}
102
103impl PartialEq for FmtLocal2k {
104	#[inline]
105	fn eq(&self, other: &Self) -> bool {
106		if self.offset == other.offset { self.inner == other.inner }
107		else {
108			Local2k::from_fmtlocal2k(*self) == Local2k::from_fmtlocal2k(*other)
109		}
110	}
111}
112
113impl PartialEq<str> for FmtLocal2k {
114	#[inline]
115	fn eq(&self, other: &str) -> bool { self.as_str() == other }
116}
117impl PartialEq<FmtLocal2k> for str {
118	#[inline]
119	fn eq(&self, other: &FmtLocal2k) -> bool { <FmtLocal2k as PartialEq<Self>>::eq(other, self) }
120}
121
122/// # Helper: Reciprocal `PartialEq`.
123macro_rules! fmt_eq {
124	($($ty:ty)+) => ($(
125		impl PartialEq<$ty> for FmtLocal2k {
126			#[inline]
127			fn eq(&self, other: &$ty) -> bool { <Self as PartialEq<str>>::eq(self, other) }
128		}
129		impl PartialEq<FmtLocal2k> for $ty {
130			#[inline]
131			fn eq(&self, other: &FmtLocal2k) -> bool { <FmtLocal2k as PartialEq<str>>::eq(other, self) }
132		}
133	)+);
134}
135fmt_eq! { &str &String String &Cow<'_, str> Cow<'_, str> &Box<str> Box<str> }
136
137impl PartialOrd for FmtLocal2k {
138	#[inline]
139	fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
140}
141
142/// ## Instantiation.
143impl FmtLocal2k {
144	#[inline]
145	#[must_use]
146	/// # Now.
147	///
148	/// Create a new instance representing the current local time.
149	///
150	/// ```
151	/// use utc2k::{FmtLocal2k, Local2k};
152	///
153	/// // Equivalent.
154	/// assert_eq!(
155	///     FmtLocal2k::now(),
156	///     FmtLocal2k::from(Local2k::now()),
157	/// );
158	/// ```
159	pub fn now() -> Self { Self::from_local2k(Local2k::now()) }
160}
161
162/// ## Getters.
163impl FmtLocal2k {
164	#[inline]
165	#[must_use]
166	/// # As Bytes.
167	///
168	/// Return a byte string slice in `YYYY-MM-DD hh:mm:ss` format.
169	///
170	/// A byte slice can also be obtained using [`FmtLocal2k::as_ref`].
171	///
172	/// ## Examples
173	///
174	/// ```
175	/// use utc2k::{Local2k, Utc2k};
176	///
177	/// let fmt = Local2k::from(Utc2k::MAX).formatted();
178	/// # let fmt = Local2k::fixed_from_utc2k(Utc2k::MAX, -28800).formatted();
179	/// assert_eq!(
180	///     fmt.as_bytes(),
181	///     b"2099-12-31 15:59:59", // e.g. California.
182	/// );
183	/// ```
184	pub const fn as_bytes(&self) -> &[u8] { self.inner.as_bytes() }
185
186	#[inline]
187	#[must_use]
188	/// # As Str.
189	///
190	/// Return a string slice in `YYYY-MM-DD hh:mm:ss` format.
191	///
192	/// ## Examples
193	///
194	/// ```
195	/// use utc2k::{Local2k, Utc2k};
196	///
197	/// let fmt = Local2k::from(Utc2k::MAX).formatted();
198	/// # let fmt = Local2k::fixed_from_utc2k(Utc2k::MAX, -28800).formatted();
199	/// assert_eq!(
200	///     fmt.as_str(),
201	///     "2099-12-31 15:59:59", // e.g. California.
202	/// );
203	/// ```
204	pub const fn as_str(&self) -> &str { self.inner.as_str() }
205
206	#[inline]
207	#[must_use]
208	/// # Just the Date Bits.
209	///
210	/// This returns the date as a string slice in `YYYY-MM-DD` format.
211	///
212	/// ## Examples
213	///
214	/// ```
215	/// use utc2k::{Local2k, Utc2k};
216	///
217	/// let utc = Utc2k::new(2025, 6, 19, 18, 57, 12);
218	/// let fmt = Local2k::from(utc).formatted();
219	/// # let fmt = Local2k::fixed_from_utc2k(utc, -25200).formatted();
220	/// assert_eq!(
221	///     fmt.as_str(),
222	///     "2025-06-19 11:57:12", // e.g. California.
223	/// );
224	/// assert_eq!(fmt.date(), "2025-06-19");
225	/// ```
226	pub const fn date(&self) -> &str { self.inner.date() }
227
228	#[inline]
229	#[must_use]
230	/// # Just the Year Bit.
231	///
232	/// This returns the year as a string slice.
233	///
234	/// ## Examples
235	///
236	/// ```
237	/// use utc2k::{Local2k, Utc2k};
238	///
239	/// let utc = Utc2k::new(2025, 6, 19, 18, 57, 12);
240	/// let fmt = Local2k::from(utc).formatted();
241	/// # let fmt = Local2k::fixed_from_utc2k(utc, -25200).formatted();
242	/// assert_eq!(
243	///     fmt.as_str(),
244	///     "2025-06-19 11:57:12", // e.g. California.
245	/// );
246	/// assert_eq!(fmt.year(), "2025");
247	/// ```
248	pub const fn year(&self) -> &str { self.inner.year() }
249
250	#[inline]
251	#[must_use]
252	/// # Just the Time Bits.
253	///
254	/// This returns the time as a string slice in `hh:mm:ss` format.
255	///
256	/// ## Examples
257	///
258	/// ```
259	/// use utc2k::{Local2k, Utc2k};
260	///
261	/// let utc = Utc2k::new(2025, 6, 19, 18, 57, 12);
262	/// let fmt = Local2k::from(utc).formatted();
263	/// # let fmt = Local2k::fixed_from_utc2k(utc, -25200).formatted();
264	/// assert_eq!(
265	///     fmt.as_str(),
266	///     "2025-06-19 11:57:12", // e.g. California.
267	/// );
268	/// assert_eq!(fmt.time(), "11:57:12");
269	/// ```
270	pub const fn time(&self) -> &str { self.inner.time() }
271}
272
273/// ## Conversion.
274impl FmtLocal2k {
275	#[must_use]
276	/// # To RFC2822.
277	///
278	/// Return a string formatted according to [RFC2822](https://datatracker.ietf.org/doc/html/rfc2822).
279	///
280	/// ## Examples
281	///
282	/// ```
283	/// use utc2k::{Local2k, Utc2k};
284	///
285	/// // A proper UTC date in RFC2822.
286	/// let utc = Utc2k::new(2021, 12, 13, 04, 56, 1);
287	/// assert_eq!(
288	///     utc.to_rfc2822(),
289	///     "Mon, 13 Dec 2021 04:56:01 +0000",
290	/// );
291	///
292	/// // The same date localized to, say, California.
293	/// let local = Local2k::from(utc).formatted();
294	/// # let local = Local2k::fixed_from_utc2k(utc, -28800).formatted();
295	/// assert_eq!(
296	///     local.to_rfc2822(),
297	///     "Sun, 12 Dec 2021 20:56:01 -0800",
298	/// );
299	/// ```
300	///
301	/// The RFC2822 date/time format is portable, whether local or UTC.
302	///
303	/// ```
304	/// # use utc2k::{Local2k, Utc2k};
305	/// # let utc = Utc2k::new(2003, 7, 1, 10, 52, 37);
306	/// # let local = Local2k::from(utc).formatted();
307	/// let utc_2822 = utc.to_rfc2822();
308	/// let local_2822 = local.to_rfc2822();
309	///
310	/// // The RFC2822 representations will vary if there's an offset, but
311	/// // if parsed back into a Utc2k, that'll get sorted and they'll match!
312	/// assert_eq!(
313	///     Utc2k::from_rfc2822(utc_2822.as_bytes()),
314	///     Some(utc),
315	/// );
316	/// assert_eq!(
317	///     Utc2k::from_rfc2822(local_2822.as_bytes()),
318	///     Some(utc),
319	/// );
320	/// ```
321	pub fn to_rfc2822(&self) -> String {
322		let local = Local2k::from_fmtlocal2k(*self);
323
324		let mut out = String::with_capacity(31);
325		out.push_str(local.weekday().abbreviation());
326		out.push_str(", ");
327		out.push(self.inner.0[8].as_char());
328		out.push(self.inner.0[9].as_char());
329		out.push(' ');
330		out.push_str(local.month().abbreviation());
331		out.push(' ');
332		out.push_str(self.year());
333		out.push(' ');
334		out.push_str(self.time());
335		if let Some(offset) = offset_suffix(self.offset) {
336			out.push(' ');
337			out.push_str(DateChar::as_str(offset.as_slice()));
338		}
339		else { out.push_str(" +0000"); }
340
341		out
342	}
343
344	#[must_use]
345	/// # To RFC3339.
346	///
347	/// Return a string formatted according to [RFC3339](https://datatracker.ietf.org/doc/html/rfc3339).
348	///
349	/// ## Examples
350	///
351	/// ```
352	/// use utc2k::{Local2k, Utc2k};
353	///
354	/// // A proper UTC date in RFC3339.
355	/// let utc = Utc2k::new(2021, 12, 13, 11, 56, 1);
356	/// assert_eq!(utc.to_rfc3339(), "2021-12-13T11:56:01Z");
357	///
358	/// // The same date localized to, say, California.
359	/// let local = Local2k::from(utc).formatted();
360	/// # let local = Local2k::fixed_from_utc2k(utc, -28800).formatted();
361	/// assert_eq!(local.to_rfc3339(), "2021-12-13T03:56:01-0800");
362	/// ```
363	///
364	/// The RFC3339 date/time format is portable, whether local or UTC.
365	///
366	/// ```
367	/// # use utc2k::{Local2k, Utc2k};
368	/// # let utc = Utc2k::new(2021, 12, 13, 11, 56, 1);
369	/// # let local = Local2k::from(utc).formatted();
370	/// let utc_3339 = utc.to_rfc3339();
371	/// let local_3339 = local.to_rfc3339();
372	///
373	/// // The RFC3339 representations will vary if there's an offset, but
374	/// // if parsed back into a Utc2k, that'll get sorted and they'll match!
375	/// assert_eq!(
376	///     Utc2k::from_ascii(utc_3339.as_bytes()),
377	///     Some(utc),
378	/// );
379	/// assert_eq!(
380	///     Utc2k::from_ascii(local_3339.as_bytes()),
381	///     Some(utc),
382	/// );
383	/// ```
384	pub fn to_rfc3339(&self) -> String {
385		let mut out = String::with_capacity(if self.offset.is_some() { 24 } else { 20 });
386		out.push_str(self.date());
387		out.push('T');
388		out.push_str(self.time());
389		if let Some(offset) = offset_suffix(self.offset) {
390			out.push_str(DateChar::as_str(offset.as_slice()));
391		}
392		else { out.push('Z'); }
393		out
394	}
395}
396
397/// ## Internal.
398impl FmtLocal2k {
399	#[must_use]
400	/// # From [`Local2k`].
401	const fn from_local2k(src: Local2k) -> Self {
402		Self {
403			inner: FmtUtc2k::from_utc2k(src.inner),
404			offset: src.offset,
405		}
406	}
407}
408
409
410
411#[derive(Debug, Clone, Copy)]
412/// # Local ~~UTC~~2K.
413///
414/// This struct brings barebones locale awareness to [`Utc2k`], allowing
415/// date/time digits to be carved up according to the user's local time zone
416/// instead of the usual UTC.
417///
418/// Time zone detection is automatic, but only supported on unix platforms.
419/// If the lookup fails or the user is running something weird like Windows,
420/// it'll stick with UTC.
421///
422/// UTC is also used in cases where the local offset would cause the date/time
423/// to be clamped to the `2000..=2099` range. (This is only applicable to the
424/// first and final hours of the century, so shouldn't come up very often!)
425///
426/// To keep things simple, `Local2k` is effectively read-only, requiring
427/// [`Utc2k`] as a go-between for both [instantiation](Local2k::from_utc2k)
428/// and [modification](Local2k::to_utc2k), except for a few convenience methods
429/// like [`Local2k::now`], [`Local2k::tomorrow`], and [`Local2k::yesterday`].
430///
431/// Note that offsets, or the lack thereof, have no effect on date/time
432/// equality, hashing, or ordering. `Local2k` objects can be freely compared
433/// with one another and/or [`Utc2k`] date/times.
434pub struct Local2k {
435	/// # Date/Time (w/ `offset`)
436	inner: Utc2k,
437
438	/// # Local Offset (Seconds).
439	offset: Option<NonZeroI32>,
440}
441
442impl fmt::Display for Local2k {
443	#[inline]
444	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
445		<FmtLocal2k as fmt::Display>::fmt(&FmtLocal2k::from_local2k(*self), f)
446	}
447}
448
449impl Eq for Local2k {}
450
451impl From<&FmtLocal2k> for Local2k {
452	#[inline]
453	fn from(src: &FmtLocal2k) -> Self { Self::from_fmtlocal2k(*src) }
454}
455
456impl From<FmtLocal2k> for Local2k {
457	#[inline]
458	fn from(src: FmtLocal2k) -> Self { Self::from_fmtlocal2k(src) }
459}
460
461impl From<&Utc2k> for Local2k {
462	#[inline]
463	fn from(src: &Utc2k) -> Self { Self::from_utc2k(*src) }
464}
465
466impl From<Utc2k> for Local2k {
467	#[inline]
468	fn from(src: Utc2k) -> Self { Self::from_utc2k(src) }
469}
470
471impl From<Local2k> for String {
472	#[inline]
473	fn from(src: Local2k) -> Self { Self::from(FmtLocal2k::from_local2k(src)) }
474}
475
476impl From<&Local2k> for Utc2k {
477	#[inline]
478	fn from(src: &Local2k) -> Self { src.to_utc2k() }
479}
480
481impl From<Local2k> for Utc2k {
482	#[inline]
483	fn from(src: Local2k) -> Self { src.to_utc2k() }
484}
485
486impl hash::Hash for Local2k {
487	#[inline]
488	fn hash<H: hash::Hasher>(&self, state: &mut H) {
489		<Utc2k as hash::Hash>::hash(&self.to_utc2k(), state);
490	}
491}
492
493impl Ord for Local2k {
494	#[inline]
495	fn cmp(&self, other: &Self) -> Ordering {
496		if self.offset == other.offset { self.inner.cmp(&other.inner) }
497		else { self.unixtime().cmp(&other.unixtime()) }
498	}
499}
500
501impl PartialEq for Local2k {
502	#[inline]
503	/// # Equality.
504	///
505	/// ```
506	/// use utc2k::{Local2k, Utc2k};
507	///
508	/// let utc = Utc2k::new(2001, 1, 15, 0, 0, 0);
509	/// let local = Local2k::from(utc);
510	///
511	/// // Offsets don't affect equality.
512	/// assert_eq!(utc, local);
513	/// assert_eq!(local, local);
514	/// ```
515	fn eq(&self, other: &Self) -> bool {
516		if self.offset == other.offset { self.inner == other.inner }
517		else { self.unixtime() == other.unixtime() }
518	}
519}
520
521impl PartialEq<Utc2k> for Local2k {
522	#[inline]
523	/// # Cross-Offset Equality.
524	///
525	/// Local and UTC dates are compared as unix timestamps, so should always
526	/// match up.
527	///
528	/// ## Examples
529	///
530	/// ```
531	/// use utc2k::{Local2k, Utc2k};
532	///
533	/// let utc = Utc2k::new(2025, 1, 1, 0, 0, 0);
534	/// let local = Local2k::from(utc);
535	/// assert_eq!(utc, local);
536	///
537	/// // String representations, however, will only be equal if there's
538	/// // no offset.
539	/// assert_eq!(
540	///     utc.to_string() == local.to_string(),
541	///     local.offset().is_none(),
542	/// );
543	/// ```
544	fn eq(&self, other: &Utc2k) -> bool { self.unixtime() == other.unixtime() }
545}
546impl PartialEq<Local2k> for Utc2k {
547	#[inline]
548	fn eq(&self, other: &Local2k) -> bool { <Local2k as PartialEq<Self>>::eq(other, self) }
549}
550
551impl PartialOrd for Local2k {
552	#[inline]
553	fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
554}
555
556/// # Instantiation.
557impl Local2k {
558	#[must_use]
559	/// # From UTC.
560	///
561	/// Convert a UTC date/time into a local one.
562	///
563	/// Refer to the main [`Local2k`] for limitations and gotchas.
564	pub fn from_utc2k(src: Utc2k) -> Self {
565		// If we have an offset, we need to do some things.
566		let unixtime = src.unixtime();
567
568		// Is there an offset?
569		if let Some(offset) = unixtime_offset(unixtime) {
570			let localtime = unixtime.saturating_add_signed(offset.get());
571			if (Utc2k::MIN_UNIXTIME..=Utc2k::MAX_UNIXTIME).contains(&localtime) {
572				return Self {
573					inner: Utc2k::from_unixtime(localtime),
574					offset: Some(offset),
575				};
576			}
577		}
578
579		// Keep it UTC.
580		Self { inner: src, offset: None }
581	}
582
583	#[doc(hidden)]
584	#[must_use]
585	/// # From UTC w/ Fixed Offset.
586	///
587	/// Same as [`Local2k::from_utc2k`], but localized with a fixed offset
588	/// instead of the system one.
589	///
590	/// Offsets can be positive or negative, but must break down evenly into
591	/// hours and/or minutes, and must be (absolutely) less than one day.
592	///
593	/// Note: this method is used internally for debugging/testing and is not
594	/// intended for broader use.
595	///
596	/// ```
597	/// use std::num::NonZeroI32;
598	/// use utc2k::{Local2k, Utc2k};
599	///
600	/// let one = Local2k::now();
601	/// let two = Local2k::fixed_from_utc2k(
602	///     one.to_utc2k(),
603	///     one.offset().map_or(0, NonZeroI32::get),
604	/// );
605	///
606	/// assert_eq!(one, two);
607	/// ```
608	pub fn fixed_from_utc2k(src: Utc2k, offset: i32) -> Self {
609		// If we have an offset, we need to do some things.
610		let unixtime = src.unixtime();
611
612		// Is there an offset?
613		if let Some(offset) = nonzero_offset(offset) {
614			let localtime = unixtime.saturating_add_signed(offset.get());
615			if (Utc2k::MIN_UNIXTIME..=Utc2k::MAX_UNIXTIME).contains(&localtime) {
616				return Self {
617					inner: Utc2k::from_unixtime(localtime),
618					offset: Some(offset),
619				};
620			}
621		}
622
623		// Keep it UTC.
624		Self { inner: src, offset: None }
625	}
626
627	#[inline]
628	#[must_use]
629	/// # Now.
630	///
631	/// Create a new instance representing the current local time.
632	///
633	/// ```
634	/// use utc2k::{Local2k, Utc2k};
635	///
636	/// // Equivalent.
637	/// assert_eq!(
638	///     Local2k::now(),
639	///     Local2k::from(Utc2k::now()),
640	/// );
641	/// ```
642	pub fn now() -> Self { Self::from_utc2k(Utc2k::now()) }
643
644	#[inline]
645	#[must_use]
646	/// # Tomorrow.
647	///
648	/// Create a new instance representing one day from now (present time).
649	///
650	/// ## Examples
651	///
652	/// ```
653	/// use utc2k::{Local2k, Utc2k};
654	///
655	/// // Equivalent.
656	/// assert_eq!(
657	///     Local2k::tomorrow(),
658	///     Local2k::from(Utc2k::tomorrow()),
659	/// );
660	/// ```
661	pub fn tomorrow() -> Self { Self::from_utc2k(Utc2k::tomorrow()) }
662
663	#[inline]
664	#[must_use]
665	/// # Yesterday.
666	///
667	/// Create a new instance representing one day ago (present time).
668	///
669	/// ## Examples
670	///
671	/// ```
672	/// use utc2k::{Local2k, Utc2k};
673	///
674	/// // Equivalent.
675	/// assert_eq!(
676	///     Local2k::yesterday(),
677	///     Local2k::from(Utc2k::yesterday()),
678	/// );
679	/// ```
680	pub fn yesterday() -> Self { Self::from_utc2k(Utc2k::yesterday()) }
681}
682
683/// # Conversion.
684impl Local2k {
685	#[inline]
686	#[must_use]
687	/// # Formatted.
688	///
689	/// This returns a [`FmtLocal2k`] and is equivalent to calling
690	/// `FmtLocal2k::from(self)`.
691	///
692	/// ## Examples
693	///
694	/// ```
695	/// use utc2k::{FmtLocal2k, Local2k, Utc2k};
696	///
697	/// let utc = Utc2k::new(2010, 5, 15, 16, 30, 1);
698	/// let local = Local2k::from(utc);
699	/// assert_eq!(local.formatted(), FmtLocal2k::from(local));
700	/// ```
701	pub const fn formatted(self) -> FmtLocal2k { FmtLocal2k::from_local2k(self) }
702
703	#[must_use]
704	/// # To RFC2822.
705	///
706	/// Return a string formatted according to [RFC2822](https://datatracker.ietf.org/doc/html/rfc2822).
707	///
708	/// ## Examples
709	///
710	/// ```
711	/// use utc2k::{Local2k, Utc2k};
712	///
713	/// // A proper UTC date in RFC2822.
714	/// let utc = Utc2k::new(2021, 12, 13, 04, 56, 1);
715	/// assert_eq!(
716	///     utc.to_rfc2822(),
717	///     "Mon, 13 Dec 2021 04:56:01 +0000",
718	/// );
719	///
720	/// // The same date localized to, say, California.
721	/// let local = Local2k::from(utc);
722	/// # let local = Local2k::fixed_from_utc2k(utc, -28800);
723	/// assert_eq!(
724	///     local.to_rfc2822(),
725	///     "Sun, 12 Dec 2021 20:56:01 -0800",
726	/// );
727	/// ```
728	///
729	/// The RFC2822 date/time format is portable, whether local or UTC.
730	///
731	/// ```
732	/// # use utc2k::{Local2k, Utc2k};
733	/// # let utc = Utc2k::new(2003, 7, 1, 10, 52, 37);
734	/// # let local = Local2k::from(utc);
735	/// let utc_2822 = utc.to_rfc2822();
736	/// let local_2822 = local.to_rfc2822();
737	///
738	/// // The RFC2822 representations will vary if there's an offset, but
739	/// // if parsed back into a Utc2k, that'll get sorted and they'll match!
740	/// assert_eq!(
741	///     Utc2k::from_rfc2822(utc_2822.as_bytes()),
742	///     Some(utc),
743	/// );
744	/// assert_eq!(
745	///     Utc2k::from_rfc2822(local_2822.as_bytes()),
746	///     Some(utc),
747	/// );
748	/// ```
749	pub fn to_rfc2822(&self) -> String {
750		let mut out = String::with_capacity(31);
751
752		macro_rules! push {
753			($($expr:expr),+) => ($( out.push(((($expr) % 10) | b'0') as char); )+);
754		}
755
756		out.push_str(self.weekday().abbreviation());
757		out.push_str(", ");
758		push!(self.inner.d / 10, self.inner.d);
759		out.push(' ');
760		out.push_str(self.month().abbreviation());
761		out.push_str(self.inner.y.as_str()); // Includes spaces on either end.
762		push!(self.inner.hh / 10, self.inner.hh);
763		out.push(':');
764		push!(self.inner.mm / 10, self.inner.mm);
765		out.push(':');
766		push!(self.inner.ss / 10, self.inner.ss);
767
768		if let Some(offset) = offset_suffix(self.offset) {
769			out.push(' ');
770			out.push_str(DateChar::as_str(offset.as_slice()));
771		}
772		else { out.push_str(" +0000"); }
773
774		out
775	}
776
777	#[inline]
778	#[must_use]
779	/// # To RFC3339.
780	///
781	/// Return a string formatted according to [RFC3339](https://datatracker.ietf.org/doc/html/rfc3339).
782	///
783	/// ## Examples
784	///
785	/// ```
786	/// use utc2k::{Local2k, Utc2k};
787	///
788	/// // A proper UTC date in RFC3339.
789	/// let utc = Utc2k::new(2021, 12, 13, 11, 56, 1);
790	/// assert_eq!(utc.to_rfc3339(), "2021-12-13T11:56:01Z");
791	///
792	/// // The same date localized to, say, California.
793	/// let local = Local2k::from(utc);
794	/// # let local = Local2k::fixed_from_utc2k(utc, -28800);
795	/// assert_eq!(local.to_rfc3339(), "2021-12-13T03:56:01-0800");
796	/// ```
797	///
798	/// The RFC3339 date/time format is portable, whether local or UTC.
799	///
800	/// ```
801	/// # use utc2k::{Local2k, Utc2k};
802	/// # let utc = Utc2k::new(2021, 12, 13, 11, 56, 1);
803	/// # let local = Local2k::from(utc);
804	/// let utc_3339 = utc.to_rfc3339();
805	/// let local_3339 = local.to_rfc3339();
806	///
807	/// // The RFC3339 representations will vary if there's an offset, but
808	/// // if parsed back into a Utc2k, that'll get sorted and they'll match!
809	/// assert_eq!(
810	///     Utc2k::from_ascii(utc_3339.as_bytes()),
811	///     Some(utc),
812	/// );
813	/// assert_eq!(
814	///     Utc2k::from_ascii(local_3339.as_bytes()),
815	///     Some(utc),
816	/// );
817	/// ```
818	pub fn to_rfc3339(&self) -> String {
819		FmtLocal2k::from_local2k(*self).to_rfc3339()
820	}
821
822	#[must_use]
823	/// # Into UTC.
824	///
825	/// Convert a local date/time back into UTC one.
826	///
827	/// ```
828	/// use utc2k::{Utc2k, Local2k};
829	///
830	/// let utc = Utc2k::now();
831	/// let local = Local2k::from(utc);
832	/// assert_eq!(
833	///     local.to_utc2k(),
834	///     utc,
835	/// );
836	/// ```
837	pub const fn to_utc2k(&self) -> Utc2k {
838		if let Some(offset) = self.offset {
839			let (y, m, d, hh, mm, ss) = self.parts();
840			Utc2k::from_abacus(Abacus::new_with_offset(y, m, d, hh, mm, ss, offset.get()))
841		}
842		else { self.inner }
843	}
844
845	#[inline]
846	#[must_use]
847	/// # Unixtime.
848	///
849	/// Return the (original) unix timestamp used to create this instance.
850	///
851	/// ## Examples
852	///
853	/// ```
854	/// use utc2k::{Local2k, Utc2k};
855	///
856	/// let utc = Utc2k::from_unixtime(1_434_765_671_u32);
857	/// let local = Local2k::from(utc);
858	///
859	/// assert_eq!(utc.unixtime(),   1_434_765_671);
860	/// assert_eq!(local.unixtime(), 1_434_765_671, "local {:?}", local.offset());
861	/// ```
862	pub const fn unixtime(&self) -> u32 {
863		let unixtime = self.inner.unixtime();
864		if let Some(offset) = self.offset {
865			unixtime.saturating_add_signed(0 - offset.get())
866		}
867		else { unixtime }
868	}
869}
870
871/// # Get Parts.
872impl Local2k {
873	#[inline]
874	#[must_use]
875	/// # Is UTC?
876	///
877	/// Returns `true` if there is no offset applied to the "local" date/time.
878	///
879	/// ## Examples
880	///
881	/// ```
882	/// use utc2k::Local2k;
883	///
884	/// let date = Local2k::now();
885	/// assert_eq!(
886	///     date.is_utc(),
887	///     date.offset().is_none(),
888	/// );
889	/// ```
890	pub const fn is_utc(&self) -> bool { self.offset.is_none() }
891
892	#[inline]
893	#[must_use]
894	/// # Offset.
895	///
896	/// Return the UTC offset in seconds, if any.
897	///
898	/// ## Examples
899	///
900	/// ```
901	/// use std::num::NonZeroI32;
902	/// use utc2k::{Local2k, Utc2k};
903	///
904	/// let utc = Utc2k::new(2005, 1, 1, 12, 0, 0);
905	/// let local = Local2k::from(utc);
906	/// # let local = Local2k::fixed_from_utc2k(utc, -28800);
907	/// assert_eq!(
908	///     local.offset(),
909	///     NonZeroI32::new(-28_800), // e.g. California.
910	/// );
911	///
912	/// // Don't forget about Daylight Saving! 🕱
913	/// let utc = Utc2k::new(2005, 6, 1, 12, 0, 0);
914	/// let local = Local2k::from(utc);
915	/// # let local = Local2k::fixed_from_utc2k(utc, -25200);
916	/// assert_eq!(
917	///     local.offset(),
918	///     NonZeroI32::new(-25_200), // e.g. California.
919	/// );
920	///
921	/// // Remember, 1999 and 2100 DO NOT EXIST. To prevent a loss of
922	/// // precision, UTC will be used to represent the first or final hours
923	/// // of the century to prevent precision loss.
924	/// let local = Local2k::from(Utc2k::MIN);
925	/// # let local = Local2k::fixed_from_utc2k(Utc2k::MIN, -28800);
926	/// assert!(local.offset().is_none()); // Can't apply a -0800 offset
927	///                                    // without leaving the century!
928	///
929	/// let local = Local2k::from(Utc2k::MAX);
930	/// # let local = Local2k::fixed_from_utc2k(Utc2k::MAX, -28800);
931	/// assert!(local.offset().is_some()); // The -0800 is no problem on the
932	///                                    // other end, though.
933	/// # // In Moscow, it'd be the other way around.
934	/// # let local = Local2k::fixed_from_utc2k(Utc2k::MIN, 14400);
935	/// # assert_eq!(local.offset(), NonZeroI32::new(14400));
936	/// # let local = Local2k::fixed_from_utc2k(Utc2k::MAX, 14400);
937	/// # assert!(local.offset().is_none());
938	/// ```
939	pub const fn offset(&self) -> Option<NonZeroI32> { self.offset }
940
941	#[inline]
942	#[must_use]
943	/// # Parts.
944	///
945	/// Return the individual numerical components of the datetime, from years
946	/// down to seconds.
947	///
948	/// Alternatively, if you only want the date bits, use [`Local2k::ymd`], or
949	/// if you only want the time bits, use [`Local2k::hms`].
950	///
951	/// ## Examples
952	///
953	/// ```
954	/// use utc2k::{Local2k, Utc2k};
955	///
956	/// let utc = Utc2k::new(2010, 5, 4, 16, 30, 1);
957	/// assert_eq!(
958	///     utc.parts(),
959	///     (2010, 5, 4, 16, 30, 1),
960	/// );
961	///
962	/// let local = Local2k::from(utc);
963	/// # let local = Local2k::fixed_from_utc2k(utc, -25200);
964	/// assert_eq!(
965	///     local.parts(),
966	///     (2010, 5, 4, 9, 30, 1), // e.g. California.
967	/// //               ^ -0700
968	/// );
969	/// ```
970	pub const fn parts(&self) -> (u16, u8, u8, u8, u8, u8) { self.inner.parts() }
971
972	#[inline]
973	#[must_use]
974	/// # Date Parts.
975	///
976	/// Return the year, month, and day.
977	///
978	/// If you want the time too, call [`Utc2k::parts`] instead.
979	///
980	/// ## Examples
981	///
982	/// ```
983	/// use utc2k::{Local2k, Utc2k};
984	///
985	/// let utc = Utc2k::new(2010, 5, 5, 16, 30, 1);
986	/// let local = Local2k::from(utc);
987	/// # let local = Local2k::fixed_from_utc2k(utc, -25200);
988	/// assert_eq!(local.ymd(), (2010, 5, 5)); // e.g. California.
989	/// ```
990	pub const fn ymd(&self) -> (u16, u8, u8) { self.inner.ymd() }
991
992	#[inline]
993	#[must_use]
994	/// # Time Parts.
995	///
996	/// Return the hours, minutes, and seconds.
997	///
998	/// If you want the date too, call [`Utc2k::parts`] instead.
999	///
1000	/// ## Examples
1001	///
1002	/// ```
1003	/// use utc2k::{Local2k, Utc2k};
1004	///
1005	/// let utc = Utc2k::new(2010, 5, 5, 16, 30, 1);
1006	/// let local = Local2k::from(utc);
1007	/// # let local = Local2k::fixed_from_utc2k(utc, -25200);
1008	/// assert_eq!(local.hms(), (9, 30, 1)); // e.g. California.
1009	/// ```
1010	pub const fn hms(&self) -> (u8, u8, u8) { self.inner.hms() }
1011
1012	#[inline]
1013	#[must_use]
1014	/// # Year.
1015	///
1016	/// This returns the year value.
1017	///
1018	/// ## Examples
1019	///
1020	/// ```
1021	/// use utc2k::{Local2k, Utc2k};
1022	///
1023	/// let utc = Utc2k::new(2010, 5, 15, 16, 30, 1);
1024	/// let local = Local2k::from(utc);
1025	/// # let local = Local2k::fixed_from_utc2k(utc, -25200);
1026	/// assert_eq!(local.year(), 2010);
1027	/// ```
1028	pub const fn year(&self) -> u16 { self.inner.year() }
1029
1030	#[inline]
1031	#[must_use]
1032	/// # Month (enum).
1033	///
1034	/// This returns the month value as a [`Month`].
1035	///
1036	/// ## Examples
1037	///
1038	/// ```
1039	/// use utc2k::{Local2k, Month, Utc2k};
1040	///
1041	/// let utc = Utc2k::new(2010, 5, 15, 16, 30, 1);
1042	/// let local = Local2k::from(utc);
1043	/// # let local = Local2k::fixed_from_utc2k(utc, -25200);
1044	/// assert_eq!(local.month(), Month::May);
1045	/// ```
1046	pub const fn month(&self) -> Month { self.inner.month() }
1047
1048	#[inline]
1049	#[must_use]
1050	/// # Day.
1051	///
1052	/// This returns the day value.
1053	///
1054	/// ## Examples
1055	///
1056	/// ```
1057	/// use utc2k::{Local2k, Utc2k};
1058	///
1059	/// let utc = Utc2k::new(2010, 5, 15, 16, 30, 1);
1060	/// let local = Local2k::from(utc);
1061	/// # let local = Local2k::fixed_from_utc2k(utc, -25200);
1062	/// assert_eq!(local.day(), 15);
1063	/// ```
1064	pub const fn day(&self) -> u8 { self.inner.day() }
1065
1066	#[inline]
1067	#[must_use]
1068	/// # Hour.
1069	///
1070	/// This returns the hour value.
1071	///
1072	/// ## Examples
1073	///
1074	/// ```
1075	/// use utc2k::{Local2k, Utc2k};
1076	///
1077	/// let utc = Utc2k::new(2010, 5, 15, 16, 30, 1);
1078	/// let local = Local2k::from(utc);
1079	/// # let local = Local2k::fixed_from_utc2k(utc, -25200);
1080	/// assert_eq!(local.hour(), 9); // e.g. California.
1081	/// ```
1082	pub const fn hour(&self) -> u8 { self.inner.hour() }
1083
1084	#[inline]
1085	#[must_use]
1086	/// # Minute.
1087	///
1088	/// This returns the minute value.
1089	///
1090	/// ## Examples
1091	///
1092	/// ```
1093	/// use utc2k::{Local2k, Utc2k};
1094	///
1095	/// let utc = Utc2k::new(2010, 5, 15, 16, 30, 1);
1096	/// let local = Local2k::from(utc);
1097	/// # let local = Local2k::fixed_from_utc2k(utc, -25200);
1098	/// assert_eq!(local.minute(), 30);
1099	/// ```
1100	pub const fn minute(&self) -> u8 { self.inner.minute() }
1101
1102	#[inline]
1103	#[must_use]
1104	/// # Second.
1105	///
1106	/// This returns the second value.
1107	///
1108	/// ## Examples
1109	///
1110	/// ```
1111	/// use utc2k::{Local2k, Utc2k};
1112	///
1113	/// let utc = Utc2k::new(2010, 5, 15, 16, 30, 1);
1114	/// let local = Local2k::from(utc);
1115	/// # let local = Local2k::fixed_from_utc2k(utc, -25200);
1116	/// assert_eq!(local.second(), 1);
1117	/// ```
1118	pub const fn second(&self) -> u8 { self.inner.second() }
1119}
1120
1121/// ## Other Getters.
1122impl Local2k {
1123	#[inline]
1124	#[must_use]
1125	/// # Is Leap Year?
1126	///
1127	/// This returns `true` if this date is/was in a leap year.
1128	///
1129	/// ## Examples
1130	///
1131	/// ```
1132	/// use utc2k::{Local2k, Utc2k};
1133	///
1134	/// let date = Local2k::from(
1135	///     Utc2k::try_from("2020-05-10").unwrap()
1136	/// );
1137	/// assert!(date.leap_year());
1138	///
1139	/// let date = Local2k::from(
1140	///     Utc2k::try_from("2021-03-15").unwrap()
1141	/// );
1142	/// assert!(! date.leap_year());
1143	/// ```
1144	pub const fn leap_year(&self) -> bool { self.inner.leap_year() }
1145
1146	#[inline]
1147	#[must_use]
1148	/// # Month Size (Days).
1149	///
1150	/// This method returns the "size" of the datetime's month, or its last
1151	/// day, whichever way you prefer to think of it.
1152	///
1153	/// The value will always be between `28..=31`, with leap Februaries
1154	/// returning `29`.
1155	///
1156	/// ## Examples
1157	///
1158	/// ```
1159	/// use utc2k::{Local2k, Utc2k};
1160	///
1161	/// let utc = Utc2k::new(2021, 7, 8, 0, 0, 0);
1162	/// let local = Local2k::from(utc);
1163	/// assert_eq!(local.month_size(), 31);
1164	///
1165	/// let utc = Utc2k::new(2020, 2, 20, 0, 0, 0);
1166	/// let local = Local2k::from(utc);
1167	/// assert_eq!(local.month_size(), 29); // Leap!
1168	/// ```
1169	pub const fn month_size(&self) -> u8 { self.inner.month_size() }
1170
1171	#[inline]
1172	#[must_use]
1173	/// # Ordinal.
1174	///
1175	/// Return the day-of-year value. This will be between `1..=365` (or `1..=366`
1176	/// for leap years).
1177	///
1178	/// ## Examples
1179	///
1180	/// ```no_run
1181	/// use utc2k::{Local2k, Utc2k};
1182	///
1183	/// let utc = Utc2k::new(2020, 5, 10, 12, 0, 0);
1184	/// let local = Local2k::from(utc);
1185	/// # let local = Local2k::fixed_from_utc2k(utc, -25200);
1186	/// assert_eq!(local.ordinal(), 131);
1187	/// ```
1188	pub const fn ordinal(&self) -> u16 { self.inner.ordinal() }
1189
1190	#[inline]
1191	#[must_use]
1192	/// # Seconds From Midnight.
1193	///
1194	/// Return the number of seconds since (the current day's) midnight. In
1195	/// other words, this adds up all of the time bits.
1196	///
1197	/// ## Examples
1198	///
1199	/// ```
1200	/// use utc2k::{DAY_IN_SECONDS, Local2k, Utc2k};
1201	///
1202	/// let utc = Utc2k::new(2010, 11, 01, 0, 0, 0);
1203	/// assert_eq!(utc.seconds_from_midnight(), 0); // It _is_ midnight!
1204	///
1205	/// // In California, though, it's still Halloween!
1206	/// let local = Local2k::from(utc);
1207	/// # let local = Local2k::fixed_from_utc2k(utc, -28800);
1208	/// assert_eq!(
1209	///     local.parts(),
1210	///     (2010, 10, 31, 16, 0, 0),
1211	/// );
1212	///
1213	/// // The distance from _its_ midnight is very different!
1214	/// assert_eq!(local.seconds_from_midnight(), 57_600);
1215	/// ```
1216	pub const fn seconds_from_midnight(&self) -> u32 {
1217		self.inner.seconds_from_midnight()
1218	}
1219
1220	#[inline]
1221	#[must_use]
1222	/// # Weekday.
1223	///
1224	/// Return the [`Weekday`] corresponding to the given date.
1225	///
1226	/// ## Examples
1227	///
1228	/// ```
1229	/// use utc2k::{Local2k, Utc2k, Weekday};
1230	///
1231	/// let utc = Utc2k::new(2021, 7, 8, 5, 22, 1);
1232	/// assert_eq!(utc.weekday(), Weekday::Thursday);
1233	///
1234	/// // Local date/times may differ. In California, for example, it'd
1235	/// // still be the night before.
1236	/// let local = Local2k::from(utc);
1237	/// # let local = Local2k::fixed_from_utc2k(utc, -25200);
1238	/// assert_eq!(local.weekday(), Weekday::Wednesday);
1239	/// ```
1240	pub const fn weekday(&self) -> Weekday { self.inner.weekday() }
1241}
1242
1243/// ## Internal Helpers.
1244impl Local2k {
1245	#[must_use]
1246	/// # From `FmtLocal2k`.
1247	const fn from_fmtlocal2k(src: FmtLocal2k) -> Self {
1248		Self {
1249			inner: Utc2k::from_fmtutc2k(src.inner),
1250			offset: src.offset,
1251		}
1252	}
1253}
1254
1255
1256
1257#[expect(clippy::cast_possible_truncation, reason = "False positive.")]
1258/// # Offset Suffix.
1259///
1260/// Convert an offset back to `±hhmm` format.
1261const fn offset_suffix(offset: Option<NonZeroI32>) -> Option<[DateChar; 5]> {
1262	if let Some(offset) = offset {
1263		let sign =
1264			if offset.get() < 0 { DateChar::Dash }
1265			else { DateChar::Plus };
1266
1267		// Offsets that make it here are less than a day.
1268		let offset = offset.get().unsigned_abs();
1269		let hh = offset.wrapping_div(HOUR_IN_SECONDS) as u8;
1270		let mm = (offset % HOUR_IN_SECONDS).wrapping_div(MINUTE_IN_SECONDS) as u8;
1271
1272		Some([
1273			sign,
1274			DateChar::from_digit(hh / 10),
1275			DateChar::from_digit(hh),
1276			DateChar::from_digit(mm / 10),
1277			DateChar::from_digit(mm),
1278		])
1279	}
1280	else { None }
1281}
1282
1283#[expect(clippy::cast_possible_wrap, reason = "False positive.")]
1284/// # Sanitize Offset.
1285///
1286/// Strip multi-day bullshit, make sure it is a multiple of sixty, and return
1287/// if nonzero.
1288const fn nonzero_offset(offset: i32) -> Option<NonZeroI32> {
1289	let offset = offset % DAY_IN_SECONDS as i32;
1290	if offset.unsigned_abs().is_multiple_of(MINUTE_IN_SECONDS) {
1291		NonZeroI32::new(offset)
1292	}
1293	else { None }
1294}
1295
1296#[inline]
1297#[must_use]
1298/// # Offset From Unixtime.
1299///
1300/// Return the local offset details for a given UTC date/time, ensuring it is
1301/// less than a day (absolutely) and limited to hour/minute precision.
1302///
1303/// The local time zone details are cached on the first call; subsequent runs
1304/// should be much faster.
1305fn unixtime_offset(unixtime: u32) -> Option<NonZeroI32> {
1306	TZ.get_or_init(|| TimeZone::local().ok())
1307		.as_ref()
1308		.and_then(|tz|
1309			tz.find_local_time_type(i64::from(unixtime))
1310				.ok()
1311				.and_then(|tz| nonzero_offset(tz.ut_offset()))
1312		)
1313}
1314
1315
1316
1317#[cfg(test)]
1318mod tests {
1319	use super::*;
1320
1321	#[test]
1322	fn t_lossless() {
1323		// Make sure the first couple days of the century can be losslessly
1324		// converted to/from utc/local with both positive and negative offsets.
1325		for i in Utc2k::MIN_UNIXTIME..=Utc2k::MIN_UNIXTIME + DAY_IN_SECONDS * 2 {
1326			let utc = Utc2k::from_unixtime(i);
1327			let local = Local2k::fixed_from_utc2k(utc, -28860);
1328			assert_eq!(utc, local);
1329			assert_eq!(utc, local.to_utc2k());
1330
1331			let local = Local2k::fixed_from_utc2k(utc, 28860);
1332			assert_eq!(utc, local);
1333			assert_eq!(utc, local.to_utc2k());
1334		}
1335
1336		// Now the same for the end.
1337		for i in Utc2k::MAX_UNIXTIME - DAY_IN_SECONDS * 2..=Utc2k::MAX_UNIXTIME {
1338			let utc = Utc2k::from_unixtime(i);
1339			let local = Local2k::fixed_from_utc2k(utc, -28860);
1340			assert_eq!(utc, local);
1341			assert_eq!(utc, local.to_utc2k());
1342
1343			let local = Local2k::fixed_from_utc2k(utc, 28860);
1344			assert_eq!(utc, local);
1345			assert_eq!(utc, local.to_utc2k());
1346		}
1347
1348		// Lastly, let's check the first 15 days of March, since there'll be
1349		// a Daylight Saving boundary in there for some time zones.
1350		for i in Utc2k::new(2025, 3, 1, 0, 0, 0).unixtime()..=Utc2k::new(2025, 3, 15, 0, 0, 0).unixtime() {
1351			let utc = Utc2k::from_unixtime(i);
1352			let local = Local2k::from_utc2k(utc);
1353			assert_eq!(utc, local);
1354			assert_eq!(utc, local.to_utc2k());
1355		}
1356	}
1357}