use core::fmt;
const MAX_SECONDS: u64 = 253_402_300_799;
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct HttpDate([u8; 29]);
impl HttpDate {
#[must_use]
pub const fn from_unix_seconds(secs: u64) -> Self {
const WEEKDAYS: [&[u8; 3]; 7] = [b"Thu", b"Fri", b"Sat", b"Sun", b"Mon", b"Tue", b"Wed"];
const MONTHS: [&[u8; 3]; 12] = [
b"Jan", b"Feb", b"Mar", b"Apr", b"May", b"Jun", b"Jul", b"Aug", b"Sep", b"Oct", b"Nov",
b"Dec",
];
let secs = if secs > MAX_SECONDS {
MAX_SECONDS
} else {
secs
};
let days = secs / 86_400;
let rem = secs % 86_400;
let (year, month, day) = civil_from_days(days);
let mut out = *b"Thu, 01 Jan 1970 00:00:00 GMT";
copy3(&mut out, 0, WEEKDAYS[(days % 7) as usize]);
two_digits(&mut out, 5, day);
copy3(&mut out, 8, MONTHS[(month - 1) as usize]);
two_digits(&mut out, 12, year / 100);
two_digits(&mut out, 14, year % 100);
two_digits(&mut out, 17, rem / 3600);
two_digits(&mut out, 20, rem % 3600 / 60);
two_digits(&mut out, 23, rem % 60);
Self(out)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 29] {
&self.0
}
#[must_use]
pub fn as_str(&self) -> &str {
core::str::from_utf8(&self.0).unwrap_or("")
}
}
impl fmt::Debug for HttpDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("HttpDate").field(&self.as_str()).finish()
}
}
impl fmt::Display for HttpDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DateCache {
second: Option<u64>,
date: HttpDate,
}
impl Default for DateCache {
fn default() -> Self {
Self::new()
}
}
impl DateCache {
#[must_use]
pub const fn new() -> Self {
Self {
second: None,
date: HttpDate::from_unix_seconds(0),
}
}
pub fn update(&mut self, unix_seconds: u64) -> &HttpDate {
if self.second != Some(unix_seconds) {
self.date = HttpDate::from_unix_seconds(unix_seconds);
self.second = Some(unix_seconds);
}
&self.date
}
#[must_use]
pub const fn current(&self) -> &HttpDate {
&self.date
}
}
const fn copy3(dst: &mut [u8; 29], at: usize, src: &[u8; 3]) {
dst[at] = src[0];
dst[at + 1] = src[1];
dst[at + 2] = src[2];
}
const fn two_digits(dst: &mut [u8; 29], at: usize, v: u64) {
dst[at] = b'0' + (v / 10 % 10) as u8;
dst[at + 1] = b'0' + (v % 10) as u8;
}
const fn civil_from_days(days: u64) -> (u64, u64, u64) {
let z = days + 719_468;
let era = z / 146_097;
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let day = doy - (153 * mp + 2) / 5 + 1;
let month = if mp < 10 { mp + 3 } else { mp - 9 };
let year = yoe + era * 400 + (month <= 2) as u64;
(year, month, day)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_http_date_known_values() {
for (secs, text) in [
(0, "Thu, 01 Jan 1970 00:00:00 GMT"),
(784_111_777, "Sun, 06 Nov 1994 08:49:37 GMT"),
(951_782_400, "Tue, 29 Feb 2000 00:00:00 GMT"),
(1_791_072_000, "Sun, 04 Oct 2026 00:00:00 GMT"),
(4_107_542_400, "Mon, 01 Mar 2100 00:00:00 GMT"),
(MAX_SECONDS, "Fri, 31 Dec 9999 23:59:59 GMT"),
(MAX_SECONDS + 1, "Fri, 31 Dec 9999 23:59:59 GMT"),
] {
assert_eq!(HttpDate::from_unix_seconds(secs).as_str(), text, "{secs}");
}
}
#[test]
fn test_civil_from_days_matches_a_day_by_day_walk() {
let (mut y, mut m, mut d) = (1970u64, 1u64, 1u64);
for days in 0..157_000u64 {
assert_eq!(civil_from_days(days), (y, m, d), "day {days}");
let leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
let len = match m {
2 if leap => 29,
2 => 28,
4 | 6 | 9 | 11 => 30,
_ => 31,
};
d += 1;
if d > len {
d = 1;
m += 1;
if m > 12 {
m = 1;
y += 1;
}
}
}
}
#[test]
fn test_date_cache_reformats_only_on_a_new_second() {
let mut cache = DateCache::new();
let a = *cache.update(10);
assert_eq!(cache.second, Some(10));
assert_eq!(*cache.update(10), a);
let b = *cache.update(11);
assert_ne!(a, b);
assert_eq!(*cache.update(10), a);
assert_eq!(cache.current(), &a);
}
}