pub fn decode_things_date(packed: i64) -> Option<String> {
if packed == 0 {
return None;
}
let year = ((packed >> 16) & 0xFFF) as i32;
let month = ((packed >> 12) & 0x0F) as u32;
let day = ((packed >> 7) & 0x1F) as u32;
if year < 1900 || !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return None;
}
Some(format!("{year:04}-{month:02}-{day:02}"))
}
pub fn pack_things_date(year: i32, month: u32, day: u32) -> i64 {
((year as i64) << 16) | ((month as i64) << 12) | ((day as i64) << 7)
}
pub fn parse_iso_date(iso: &str) -> Option<(i32, u32, u32)> {
if iso.len() != 10 {
return None;
}
let bytes = iso.as_bytes();
if bytes[4] != b'-' || bytes[7] != b'-' {
return None;
}
let y: i32 = iso.get(0..4)?.parse().ok()?;
let m: u32 = iso.get(5..7)?.parse().ok()?;
let d: u32 = iso.get(8..10)?.parse().ok()?;
if !(1..=12).contains(&m) || !(1..=31).contains(&d) {
return None;
}
Some((y, m, d))
}
pub fn ymd_to_unix_utc(year: i32, month: u32, day: u32) -> i64 {
let mut days: i64 = 0;
let from_year = 1970;
if year >= from_year {
for yi in from_year..year {
let leap = (yi % 4 == 0 && yi % 100 != 0) || (yi % 400 == 0);
days += if leap { 366 } else { 365 };
}
} else {
for yi in year..from_year {
let leap = (yi % 4 == 0 && yi % 100 != 0) || (yi % 400 == 0);
days -= if leap { 366 } else { 365 };
}
}
let leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
let months_len: [u32; 12] = [
31,
if leap { 29 } else { 28 },
31,
30,
31,
30,
31,
31,
30,
31,
30,
31,
];
for mo in 1..month {
days += months_len[(mo - 1) as usize] as i64;
}
days += (day - 1) as i64;
days * 86_400
}
pub fn today_packed_utc() -> i64 {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let (y, m, d, _, _, _) = crate::core::backup::unix_to_ymdhms(secs);
pack_things_date(y, m, d)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_zero_is_none() {
assert_eq!(decode_things_date(0), None);
}
#[test]
fn decode_known_packed_value() {
let p = pack_things_date(2026, 5, 20);
assert_eq!(decode_things_date(p), Some("2026-05-20".to_string()));
}
#[test]
fn pack_then_decode_round_trip() {
for (y, m, d) in [(2000, 1, 1), (2026, 12, 31), (2099, 12, 31)] {
let p = pack_things_date(y, m, d);
assert_eq!(
decode_things_date(p),
Some(format!("{y:04}-{m:02}-{d:02}"))
);
}
}
#[test]
fn decode_rejects_malformed_year() {
let p = pack_things_date(1800, 1, 1);
assert_eq!(decode_things_date(p), None);
}
#[test]
fn parse_iso_date_happy_path() {
assert_eq!(parse_iso_date("2026-05-20"), Some((2026, 5, 20)));
}
#[test]
fn parse_iso_date_rejects_wrong_separators() {
assert_eq!(parse_iso_date("2026/05/20"), None);
}
#[test]
fn parse_iso_date_rejects_wrong_length() {
assert_eq!(parse_iso_date("2026-5-20"), None);
assert_eq!(parse_iso_date("2026-05-2"), None);
}
#[test]
fn ymd_to_unix_utc_at_epoch() {
assert_eq!(ymd_to_unix_utc(1970, 1, 1), 0);
assert_eq!(ymd_to_unix_utc(1970, 1, 2), 86_400);
}
#[test]
fn ymd_to_unix_utc_leap_day() {
let a = ymd_to_unix_utc(2024, 2, 29);
let b = ymd_to_unix_utc(2024, 3, 1);
assert_eq!(b - a, 86_400);
}
#[test]
fn today_packed_utc_decodes_to_real_date() {
let p = today_packed_utc();
let s = decode_things_date(p).expect("today must decode");
assert_eq!(s.len(), 10);
assert_eq!(&s[4..5], "-");
assert_eq!(&s[7..8], "-");
}
}