use crate::{
DateChar,
DAY_IN_SECONDS,
FmtUtc2k,
HOUR_IN_SECONDS,
macros,
MINUTE_IN_SECONDS,
Month,
Utc2k,
Weekday,
};
use std::{
borrow::Cow,
cmp::Ordering,
fmt,
hash,
num::NonZeroI32,
sync::OnceLock,
};
use super::Abacus;
use tz::timezone::TimeZone;
static TZ: OnceLock<Option<TimeZone>> = OnceLock::new();
#[derive(Debug, Clone, Copy)]
pub struct FmtLocal2k {
inner: FmtUtc2k,
offset: Option<NonZeroI32>,
}
impl AsRef<[u8]> for FmtLocal2k {
#[inline]
fn as_ref(&self) -> &[u8] { self.as_bytes() }
}
macros::as_ref_borrow_cast!(FmtLocal2k: as_str str);
impl fmt::Display for FmtLocal2k {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
<FmtUtc2k as fmt::Display>::fmt(&self.inner, f)
}
}
impl Eq for FmtLocal2k {}
impl From<Local2k> for FmtLocal2k {
#[inline]
fn from(src: Local2k) -> Self { Self::from_local2k(src) }
}
impl From<FmtLocal2k> for String {
#[inline]
fn from(src: FmtLocal2k) -> Self { src.as_str().to_owned() }
}
impl hash::Hash for FmtLocal2k {
#[inline]
fn hash<H: hash::Hasher>(&self, state: &mut H) {
<Local2k as hash::Hash>::hash(&Local2k::from_fmtlocal2k(*self), state);
}
}
impl Ord for FmtLocal2k {
#[inline]
fn cmp(&self, other: &Self) -> Ordering {
if self.offset == other.offset { self.inner.cmp(&other.inner) }
else {
Local2k::from_fmtlocal2k(*self).cmp(&Local2k::from_fmtlocal2k(*other))
}
}
}
impl PartialEq for FmtLocal2k {
#[inline]
fn eq(&self, other: &Self) -> bool {
if self.offset == other.offset { self.inner == other.inner }
else {
Local2k::from_fmtlocal2k(*self) == Local2k::from_fmtlocal2k(*other)
}
}
}
impl PartialEq<str> for FmtLocal2k {
#[inline]
fn eq(&self, other: &str) -> bool { self.as_str() == other }
}
impl PartialEq<FmtLocal2k> for str {
#[inline]
fn eq(&self, other: &FmtLocal2k) -> bool { <FmtLocal2k as PartialEq<Self>>::eq(other, self) }
}
macro_rules! fmt_eq {
($($ty:ty)+) => ($(
impl PartialEq<$ty> for FmtLocal2k {
#[inline]
fn eq(&self, other: &$ty) -> bool { <Self as PartialEq<str>>::eq(self, other) }
}
impl PartialEq<FmtLocal2k> for $ty {
#[inline]
fn eq(&self, other: &FmtLocal2k) -> bool { <FmtLocal2k as PartialEq<str>>::eq(other, self) }
}
)+);
}
fmt_eq! { &str &String String &Cow<'_, str> Cow<'_, str> &Box<str> Box<str> }
impl PartialOrd for FmtLocal2k {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
}
impl FmtLocal2k {
#[inline]
#[must_use]
pub fn now() -> Self { Self::from_local2k(Local2k::now()) }
}
impl FmtLocal2k {
#[inline]
#[must_use]
pub const fn as_bytes(&self) -> &[u8] { self.inner.as_bytes() }
#[inline]
#[must_use]
pub const fn as_str(&self) -> &str { self.inner.as_str() }
#[inline]
#[must_use]
pub const fn date(&self) -> &str { self.inner.date() }
#[inline]
#[must_use]
pub const fn year(&self) -> &str { self.inner.year() }
#[inline]
#[must_use]
pub const fn time(&self) -> &str { self.inner.time() }
}
impl FmtLocal2k {
#[must_use]
pub fn to_rfc2822(&self) -> String {
let local = Local2k::from_fmtlocal2k(*self);
let mut out = String::with_capacity(31);
out.push_str(local.weekday().abbreviation());
out.push_str(", ");
out.push(self.inner.0[8].as_char());
out.push(self.inner.0[9].as_char());
out.push(' ');
out.push_str(local.month().abbreviation());
out.push(' ');
out.push_str(self.year());
out.push(' ');
out.push_str(self.time());
if let Some(offset) = offset_suffix(self.offset) {
out.push(' ');
out.push_str(DateChar::as_str(offset.as_slice()));
}
else { out.push_str(" +0000"); }
out
}
#[must_use]
pub fn to_rfc3339(&self) -> String {
let mut out = String::with_capacity(if self.offset.is_some() { 24 } else { 20 });
out.push_str(self.date());
out.push('T');
out.push_str(self.time());
if let Some(offset) = offset_suffix(self.offset) {
out.push_str(DateChar::as_str(offset.as_slice()));
}
else { out.push('Z'); }
out
}
}
impl FmtLocal2k {
#[must_use]
const fn from_local2k(src: Local2k) -> Self {
Self {
inner: FmtUtc2k::from_utc2k(src.inner),
offset: src.offset,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Local2k {
inner: Utc2k,
offset: Option<NonZeroI32>,
}
impl fmt::Display for Local2k {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
<FmtLocal2k as fmt::Display>::fmt(&FmtLocal2k::from_local2k(*self), f)
}
}
impl Eq for Local2k {}
impl From<&FmtLocal2k> for Local2k {
#[inline]
fn from(src: &FmtLocal2k) -> Self { Self::from_fmtlocal2k(*src) }
}
impl From<FmtLocal2k> for Local2k {
#[inline]
fn from(src: FmtLocal2k) -> Self { Self::from_fmtlocal2k(src) }
}
impl From<&Utc2k> for Local2k {
#[inline]
fn from(src: &Utc2k) -> Self { Self::from_utc2k(*src) }
}
impl From<Utc2k> for Local2k {
#[inline]
fn from(src: Utc2k) -> Self { Self::from_utc2k(src) }
}
impl From<Local2k> for String {
#[inline]
fn from(src: Local2k) -> Self { Self::from(FmtLocal2k::from_local2k(src)) }
}
impl From<&Local2k> for Utc2k {
#[inline]
fn from(src: &Local2k) -> Self { src.to_utc2k() }
}
impl From<Local2k> for Utc2k {
#[inline]
fn from(src: Local2k) -> Self { src.to_utc2k() }
}
impl hash::Hash for Local2k {
#[inline]
fn hash<H: hash::Hasher>(&self, state: &mut H) {
<Utc2k as hash::Hash>::hash(&self.to_utc2k(), state);
}
}
impl Ord for Local2k {
#[inline]
fn cmp(&self, other: &Self) -> Ordering {
if self.offset == other.offset { self.inner.cmp(&other.inner) }
else { self.unixtime().cmp(&other.unixtime()) }
}
}
impl PartialEq for Local2k {
#[inline]
fn eq(&self, other: &Self) -> bool {
if self.offset == other.offset { self.inner == other.inner }
else { self.unixtime() == other.unixtime() }
}
}
impl PartialEq<Utc2k> for Local2k {
#[inline]
fn eq(&self, other: &Utc2k) -> bool { self.unixtime() == other.unixtime() }
}
impl PartialEq<Local2k> for Utc2k {
#[inline]
fn eq(&self, other: &Local2k) -> bool { <Local2k as PartialEq<Self>>::eq(other, self) }
}
impl PartialOrd for Local2k {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
}
impl Local2k {
#[must_use]
pub fn from_utc2k(src: Utc2k) -> Self {
let unixtime = src.unixtime();
if let Some(offset) = unixtime_offset(unixtime) {
let localtime = unixtime.saturating_add_signed(offset.get());
if (Utc2k::MIN_UNIXTIME..=Utc2k::MAX_UNIXTIME).contains(&localtime) {
return Self {
inner: Utc2k::from_unixtime(localtime),
offset: Some(offset),
};
}
}
Self { inner: src, offset: None }
}
#[doc(hidden)]
#[must_use]
pub fn fixed_from_utc2k(src: Utc2k, offset: i32) -> Self {
let unixtime = src.unixtime();
if let Some(offset) = nonzero_offset(offset) {
let localtime = unixtime.saturating_add_signed(offset.get());
if (Utc2k::MIN_UNIXTIME..=Utc2k::MAX_UNIXTIME).contains(&localtime) {
return Self {
inner: Utc2k::from_unixtime(localtime),
offset: Some(offset),
};
}
}
Self { inner: src, offset: None }
}
#[inline]
#[must_use]
pub fn now() -> Self { Self::from_utc2k(Utc2k::now()) }
#[inline]
#[must_use]
pub fn tomorrow() -> Self { Self::from_utc2k(Utc2k::tomorrow()) }
#[inline]
#[must_use]
pub fn yesterday() -> Self { Self::from_utc2k(Utc2k::yesterday()) }
}
impl Local2k {
#[inline]
#[must_use]
pub const fn formatted(self) -> FmtLocal2k { FmtLocal2k::from_local2k(self) }
#[must_use]
pub fn to_rfc2822(&self) -> String {
let mut out = String::with_capacity(31);
macro_rules! push {
($($expr:expr),+) => ($( out.push(((($expr) % 10) | b'0') as char); )+);
}
out.push_str(self.weekday().abbreviation());
out.push_str(", ");
push!(self.inner.d / 10, self.inner.d);
out.push(' ');
out.push_str(self.month().abbreviation());
out.push_str(self.inner.y.as_str()); push!(self.inner.hh / 10, self.inner.hh);
out.push(':');
push!(self.inner.mm / 10, self.inner.mm);
out.push(':');
push!(self.inner.ss / 10, self.inner.ss);
if let Some(offset) = offset_suffix(self.offset) {
out.push(' ');
out.push_str(DateChar::as_str(offset.as_slice()));
}
else { out.push_str(" +0000"); }
out
}
#[inline]
#[must_use]
pub fn to_rfc3339(&self) -> String {
FmtLocal2k::from_local2k(*self).to_rfc3339()
}
#[must_use]
pub const fn to_utc2k(&self) -> Utc2k {
if let Some(offset) = self.offset {
let (y, m, d, hh, mm, ss) = self.parts();
Utc2k::from_abacus(Abacus::new_with_offset(y, m, d, hh, mm, ss, offset.get()))
}
else { self.inner }
}
#[inline]
#[must_use]
pub const fn unixtime(&self) -> u32 {
let unixtime = self.inner.unixtime();
if let Some(offset) = self.offset {
unixtime.saturating_add_signed(0 - offset.get())
}
else { unixtime }
}
}
impl Local2k {
#[inline]
#[must_use]
pub const fn is_utc(&self) -> bool { self.offset.is_none() }
#[inline]
#[must_use]
pub const fn offset(&self) -> Option<NonZeroI32> { self.offset }
#[inline]
#[must_use]
pub const fn parts(&self) -> (u16, u8, u8, u8, u8, u8) { self.inner.parts() }
#[inline]
#[must_use]
pub const fn ymd(&self) -> (u16, u8, u8) { self.inner.ymd() }
#[inline]
#[must_use]
pub const fn hms(&self) -> (u8, u8, u8) { self.inner.hms() }
#[inline]
#[must_use]
pub const fn year(&self) -> u16 { self.inner.year() }
#[inline]
#[must_use]
pub const fn month(&self) -> Month { self.inner.month() }
#[inline]
#[must_use]
pub const fn day(&self) -> u8 { self.inner.day() }
#[inline]
#[must_use]
pub const fn hour(&self) -> u8 { self.inner.hour() }
#[inline]
#[must_use]
pub const fn minute(&self) -> u8 { self.inner.minute() }
#[inline]
#[must_use]
pub const fn second(&self) -> u8 { self.inner.second() }
}
impl Local2k {
#[inline]
#[must_use]
pub const fn leap_year(&self) -> bool { self.inner.leap_year() }
#[inline]
#[must_use]
pub const fn month_size(&self) -> u8 { self.inner.month_size() }
#[inline]
#[must_use]
pub const fn ordinal(&self) -> u16 { self.inner.ordinal() }
#[inline]
#[must_use]
pub const fn seconds_from_midnight(&self) -> u32 {
self.inner.seconds_from_midnight()
}
#[inline]
#[must_use]
pub const fn weekday(&self) -> Weekday { self.inner.weekday() }
}
impl Local2k {
#[must_use]
const fn from_fmtlocal2k(src: FmtLocal2k) -> Self {
Self {
inner: Utc2k::from_fmtutc2k(src.inner),
offset: src.offset,
}
}
}
#[expect(clippy::cast_possible_truncation, reason = "False positive.")]
const fn offset_suffix(offset: Option<NonZeroI32>) -> Option<[DateChar; 5]> {
if let Some(offset) = offset {
let sign =
if offset.get() < 0 { DateChar::Dash }
else { DateChar::Plus };
let offset = offset.get().unsigned_abs();
let hh = offset.wrapping_div(HOUR_IN_SECONDS) as u8;
let mm = (offset % HOUR_IN_SECONDS).wrapping_div(MINUTE_IN_SECONDS) as u8;
Some([
sign,
DateChar::from_digit(hh / 10),
DateChar::from_digit(hh),
DateChar::from_digit(mm / 10),
DateChar::from_digit(mm),
])
}
else { None }
}
#[expect(clippy::cast_possible_wrap, reason = "False positive.")]
const fn nonzero_offset(offset: i32) -> Option<NonZeroI32> {
let offset = offset % DAY_IN_SECONDS as i32;
if offset.unsigned_abs().is_multiple_of(MINUTE_IN_SECONDS) {
NonZeroI32::new(offset)
}
else { None }
}
#[inline]
#[must_use]
fn unixtime_offset(unixtime: u32) -> Option<NonZeroI32> {
TZ.get_or_init(|| TimeZone::local().ok())
.as_ref()
.and_then(|tz|
tz.find_local_time_type(i64::from(unixtime))
.ok()
.and_then(|tz| nonzero_offset(tz.ut_offset()))
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn t_lossless() {
for i in Utc2k::MIN_UNIXTIME..=Utc2k::MIN_UNIXTIME + DAY_IN_SECONDS * 2 {
let utc = Utc2k::from_unixtime(i);
let local = Local2k::fixed_from_utc2k(utc, -28860);
assert_eq!(utc, local);
assert_eq!(utc, local.to_utc2k());
let local = Local2k::fixed_from_utc2k(utc, 28860);
assert_eq!(utc, local);
assert_eq!(utc, local.to_utc2k());
}
for i in Utc2k::MAX_UNIXTIME - DAY_IN_SECONDS * 2..=Utc2k::MAX_UNIXTIME {
let utc = Utc2k::from_unixtime(i);
let local = Local2k::fixed_from_utc2k(utc, -28860);
assert_eq!(utc, local);
assert_eq!(utc, local.to_utc2k());
let local = Local2k::fixed_from_utc2k(utc, 28860);
assert_eq!(utc, local);
assert_eq!(utc, local.to_utc2k());
}
for i in Utc2k::new(2025, 3, 1, 0, 0, 0).unixtime()..=Utc2k::new(2025, 3, 15, 0, 0, 0).unixtime() {
let utc = Utc2k::from_unixtime(i);
let local = Local2k::from_utc2k(utc);
assert_eq!(utc, local);
assert_eq!(utc, local.to_utc2k());
}
}
}