use core::fmt;
use core::sync::atomic::{AtomicU64, Ordering};
use embassy_time::Instant;
const UNSET: u64 = u64::MAX;
static EPOCH_AT_BOOT: AtomicU64 = AtomicU64::new(UNSET);
pub fn set(unix_millis: u64) {
let elapsed = Instant::now().as_millis();
EPOCH_AT_BOOT.store(unix_millis.saturating_sub(elapsed), Ordering::Relaxed);
}
pub fn now_unix_millis() -> Option<u64> {
match EPOCH_AT_BOOT.load(Ordering::Relaxed) {
UNSET => None,
base => Some(base.saturating_add(Instant::now().as_millis())),
}
}
pub fn now_unix() -> Option<u64> {
now_unix_millis().map(|millis| millis / 1_000)
}
pub fn now() -> Option<DateTime> {
now_unix().map(DateTime::from_unix)
}
#[cfg(feature = "tls")]
pub fn rustls_time_provider() -> impl rustls::time_provider::TimeProvider {
WallClock
}
#[cfg(feature = "tls")]
#[derive(Debug)]
struct WallClock;
#[cfg(feature = "tls")]
impl rustls::time_provider::TimeProvider for WallClock {
fn current_time(&self) -> Option<rustls::pki_types::UnixTime> {
now_unix_millis().map(|millis| {
rustls::pki_types::UnixTime::since_unix_epoch(core::time::Duration::from_millis(millis))
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DateTime {
pub year: u64,
pub month: u64,
pub day: u64,
pub hour: u64,
pub minute: u64,
pub second: u64,
}
impl DateTime {
pub fn from_unix(unix_seconds: u64) -> Self {
let days = unix_seconds / 86_400;
let seconds_of_day = unix_seconds % 86_400;
let shifted = days + 719_468;
let era = shifted / 146_097;
let day_of_era = shifted - era * 146_097;
let year_of_era =
(day_of_era - day_of_era / 1460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
let month_prime = (5 * day_of_year + 2) / 153;
let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
let month = if month_prime < 10 {
month_prime + 3
} else {
month_prime - 9
};
let mut year = year_of_era + era * 400;
if month <= 2 {
year += 1;
}
DateTime {
year,
month,
day,
hour: seconds_of_day / 3600,
minute: (seconds_of_day % 3600) / 60,
second: seconds_of_day % 60,
}
}
pub fn epoch_day(&self) -> i64 {
days_from_civil(self.year as i64, self.month as i64, self.day as i64)
}
}
impl fmt::Display for DateTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:04}-{:02}-{:02} {:02}:{:02}:{:02} UTC",
self.year, self.month, self.day, self.hour, self.minute, self.second
)
}
}
pub fn days_from_civil(year: i64, month: i64, day: i64) -> i64 {
let year = if month <= 2 { year - 1 } else { year };
let era = year.div_euclid(400);
let year_of_era = year - era * 400;
let month_prime = if month > 2 { month - 3 } else { month + 9 };
let day_of_year = (153 * month_prime + 2) / 5 + day - 1;
let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
era * 146_097 + day_of_era - 719_468
}
#[cfg(feature = "tz")]
pub use local::{LocalTime, utc_offset};
#[cfg(feature = "tz")]
mod local {
use core::fmt;
use tz::TimeZone;
use super::DateTime;
pub fn utc_offset(unix_seconds: u64, zone: Option<&TimeZone>) -> i64 {
zone.and_then(|zone| zone.find_local_time_type(unix_seconds as i64).ok())
.map_or(0, |local| i64::from(local.ut_offset()))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LocalTime<'z> {
pub at: DateTime,
pub abbreviation: &'z str,
}
impl<'z> LocalTime<'z> {
pub fn from_unix(unix_seconds: u64, zone: &'z TimeZone) -> Option<Self> {
let local = zone.find_local_time_type(unix_seconds as i64).ok()?;
Some(LocalTime {
at: DateTime::from_unix(
unix_seconds.checked_add_signed(i64::from(local.ut_offset()))?,
),
abbreviation: local.time_zone_designation(),
})
}
}
impl fmt::Display for LocalTime<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let at = &self.at;
write!(
f,
"{:04}-{:02}-{:02} {:02}:{:02}:{:02} {}",
at.year, at.month, at.day, at.hour, at.minute, at.second, self.abbreviation
)
}
}
}
#[cfg(test)]
mod tests {
use alloc::format;
use super::*;
fn at(year: u64, month: u64, day: u64, hour: u64, minute: u64, second: u64) -> DateTime {
DateTime {
year,
month,
day,
hour,
minute,
second,
}
}
#[test]
fn known_instants() {
assert_eq!(DateTime::from_unix(0), at(1970, 1, 1, 0, 0, 0));
assert_eq!(DateTime::from_unix(315_532_800), at(1980, 1, 1, 0, 0, 0));
assert_eq!(DateTime::from_unix(951_782_400), at(2000, 2, 29, 0, 0, 0));
assert_eq!(
DateTime::from_unix(951_868_799),
at(2000, 2, 29, 23, 59, 59)
);
assert_eq!(
DateTime::from_unix(2_147_483_648),
at(2038, 1, 19, 3, 14, 8)
);
assert_eq!(
DateTime::from_unix(4_354_819_199),
at(2107, 12, 31, 23, 59, 59)
);
}
#[test]
fn century_years_are_not_leap_unless_divisible_by_400() {
let feb_28 = days_from_civil(2100, 2, 28);
assert_eq!(
DateTime::from_unix((feb_28 as u64 + 1) * 86_400),
at(2100, 3, 1, 0, 0, 0)
);
let feb_28 = days_from_civil(2000, 2, 28);
assert_eq!(
DateTime::from_unix((feb_28 as u64 + 1) * 86_400),
at(2000, 2, 29, 0, 0, 0)
);
}
#[test]
fn days_from_civil_inverts_from_unix() {
let last = days_from_civil(2107, 12, 31);
for day in 0..=last {
let date = DateTime::from_unix(day as u64 * 86_400);
assert_eq!(date.epoch_day(), day, "{date}");
}
}
#[test]
fn days_before_1970_are_negative() {
assert_eq!(days_from_civil(1969, 12, 31), -1);
assert_eq!(days_from_civil(1900, 1, 1), -25_567);
assert_eq!(days_from_civil(0, 3, 1), -719_468);
}
#[test]
fn display() {
assert_eq!(
format!("{}", DateTime::from_unix(1_790_000_000)),
"2026-09-21 14:13:20 UTC"
);
}
#[test]
fn set_and_read_back() {
use embassy_time::{Duration, MockDriver};
let driver = MockDriver::get();
driver.advance(Duration::from_secs(10));
assert_eq!(now_unix_millis(), None);
assert_eq!(now(), None);
#[cfg(feature = "tls")]
let provider = {
use rustls::time_provider::TimeProvider;
let provider = rustls_time_provider();
assert_eq!(provider.current_time(), None);
provider
};
set(1_790_000_000_123);
assert_eq!(now_unix_millis(), Some(1_790_000_000_123));
assert_eq!(now_unix(), Some(1_790_000_000));
#[cfg(feature = "tls")]
{
use rustls::time_provider::TimeProvider;
assert_eq!(
provider.current_time().map(|t| t.as_secs()),
Some(1_790_000_000)
);
}
driver.advance(Duration::from_millis(1_500));
assert_eq!(now_unix_millis(), Some(1_790_000_001_623));
set(1_800_000_000_000);
assert_eq!(now_unix(), Some(1_800_000_000));
set(5_000);
assert_eq!(now_unix(), Some(11));
}
#[cfg(feature = "tz")]
#[test]
fn local_time_applies_the_offset() {
let zone = tz::TimeZone::fixed(-5 * 3600).unwrap();
let local = LocalTime::from_unix(1_790_000_000, &zone).unwrap();
assert_eq!(local.at, at(2026, 9, 21, 9, 13, 20));
assert_eq!(utc_offset(1_790_000_000, Some(&zone)), -5 * 3600);
assert_eq!(utc_offset(1_790_000_000, None), 0);
assert!(LocalTime::from_unix(0, &zone).is_none());
}
}