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}