///|
/// POSIX time utilities for ZIP archives.
///
/// Provides conversion between POSIX timestamps (Unix epoch) and DOS date/time
/// format used in ZIP file headers. DOS format has limited range (1980-2107)
/// and 2-second precision, while POSIX has arbitrary range and 1-second precision.
///
/// Key conversions:
/// - POSIX time <-> (year, month, day, hour, minute, second)
/// - DOS date/time <-> POSIX time (with precision loss)
/// - Current time -> ZIP-compatible timestamps
///|
/// POSIX time (seconds since Unix epoch: 1970-01-01 00:00:00 UTC).
///
/// Standard Unix timestamp representation. ZIP files require conversion to
/// DOS format which truncates to 2-second precision and has epoch 1980-01-01.
pub(all) struct Ptime(Int) derive(Eq, Compare, ToJson, FromJson, Hash)
///|
pub extend Ptime with Eq::{not_equal, equal}
///|
pub extend Ptime with Compare::{op_lt, op_le, op_ge, compare, op_gt}
///|
pub extend Ptime with ToJson::{to_json}
///|
pub extend Ptime with @json.FromJson::{from_json}
///|
pub extend Ptime with Hash::{hash, hash_combine}
///|
/// DOS epoch: 1980-01-01 00:00:00 UTC (in POSIX time).
///
/// This is the earliest representable time in ZIP archives. Any POSIX time
/// before this epoch will be clamped to this value when converting to DOS format.
///
/// Value: 315532800 seconds = 10 years after Unix epoch.
pub let dos_epoch : Ptime = 315532800
///|
let jd_posix_epoch : Int = 2440588 // Julian day of POSIX epoch
///|
/// Convert POSIX time to ((year, month, day), (hour, minute, second))
pub fn ptime_to_date_time(
ptime_s : Ptime,
) -> ((Int, Int, Int), (Int, Int, Int)) {
// Calculate Julian day
let jd = ptime_s.0 / 86400 + jd_posix_epoch
let jd_rem = ptime_s.0 % 86400
// Time components
let hh = jd_rem / 3600
let hh_rem = jd_rem % 3600
let mm = hh_rem / 60
let ss = hh_rem % 60
// Date from Julian day (algorithm from ptime library)
let a = jd + 32044
let b = (4 * a + 3) / 146097
let c = a - 146097 * b / 4
let d = (4 * c + 3) / 1461
let e = c - 1461 * d / 4
let m = (5 * e + 2) / 153
let day = e - (153 * m + 2) / 5 + 1
let month = m + 3 - 12 * (m / 10)
let year = 100 * b + d - 4800 + m / 10
((year, month, day), (hh, mm, ss))
}
///|
/// Convert MS-DOS date/time to POSIX time
///
/// # Parameters
/// - `dos_date`: 16-bit DOS date value (UInt16)
/// - Bits 0-4: Day of month (1-31)
/// - Bits 5-8: Month (1-12)
/// - Bits 9-15: Year offset from 1980 (0-127, represents 1980-2107)
/// - `dos_time`: 16-bit DOS time value (UInt16)
/// - Bits 0-4: Seconds/2 (0-29, represents 0-58 seconds in 2-second intervals)
/// - Bits 5-10: Minutes (0-59)
/// - Bits 11-15: Hours (0-23)
///
/// # Returns
/// POSIX timestamp (seconds since Unix epoch: 1970-01-01 00:00:00 UTC)
///
/// # Notes
/// - Both parameters are native `UInt16` types as stored in ZIP file format
/// - Valid range: 1980-01-01 00:00:00 to 2107-12-31 23:59:58
/// - If `dos_date < 0x21` (before 1980-01-01), returns `dos_epoch` (1980-01-01)
/// - The ZIP format uses this encoding as defined in PKWARE's APPNOTE.TXT
pub fn ptime_of_dos_date_time(dos_date : UInt16, dos_time : UInt16) -> Ptime {
if dos_date < 0x21 {
// Before 1980-01-01
dos_epoch
} else {
// Extract time components
let hh = dos_time >> 11
let mm = (dos_time >> 5) & 0x3F
let ss = (dos_time & 0x1F) * 2
// Extract date components and compute Julian day
let year = ((dos_date >> 9) & 0x7F) + 1980
let month = (dos_date >> 5) & 0xF
let day = dos_date & 0x1F
// Julian day calculation
let a = (14 - month.to_int()) / 12
let y = year.to_int() + 4800 - a
let m = month.to_int() + 12 * a - 3
let jd = day.to_int() +
(153 * m + 2) / 5 +
365 * y +
y / 4 -
y / 100 +
y / 400 -
32045
let d = jd - jd_posix_epoch
d * 86400 + hh.to_int() * 3600 + mm.to_int() * 60 + ss.to_int()
}
}
///|
/// Convert POSIX time to MS-DOS date/time format
///
/// # Parameters
/// - `ptime_s`: POSIX timestamp (seconds since Unix epoch: 1970-01-01 00:00:00 UTC)
///
/// # Returns
/// A tuple `(dos_date, dos_time)` of UInt16 values:
/// - `dos_date`: 16-bit DOS date with bits:
/// - Bits 0-4: Day of month (1-31)
/// - Bits 5-8: Month (1-12)
/// - Bits 9-15: Year offset from 1980 (0-127, represents 1980-2107)
/// - `dos_time`: 16-bit DOS time with bits:
/// - Bits 0-4: Seconds/2 (0-29, represents 0-58 seconds in 2-second intervals)
/// - Bits 5-10: Minutes (0-59)
/// - Bits 11-15: Hours (0-23)
///
/// # Notes
/// - Times before 1980-01-01 are clamped to 1980-01-01 00:00:00
/// - Times after 2107-12-31 are clamped to 2107-12-31 23:59:59
/// - Returns native `UInt16` types as required by ZIP file format
/// - Seconds are stored in 2-second intervals (precision loss)
/// - The ZIP format uses this encoding as defined in PKWARE's APPNOTE.TXT
pub fn ptime_to_dos_date_time(ptime_s : Ptime) -> (UInt16, UInt16) {
let date_time = ptime_to_date_time(ptime_s)
let ((y, _, _), _) = date_time
let ((year, month, day), (hh, mm, ss)) = if y < 1980 {
((1980, 1, 1), (0, 0, 0))
} else if y > 2107 {
((2107, 12, 31), (23, 59, 59))
} else {
date_time
}
let dos_date = day.to_uint16() |
(month.to_uint16() << 5) |
((year - 1980).to_uint16() << 9)
let dos_time = (ss / 2).to_uint16() |
(mm.to_uint16() << 5) |
(hh.to_uint16() << 11)
(dos_date, dos_time)
}
///|
/// Format POSIX time as RFC 3339 (without T separator)
pub fn ptime_format(ptime : Ptime) -> String {
let ((year, month, day), (hh, mm, ss)) = ptime_to_date_time(ptime)
// Format: YYYY-MM-DD HH:MM:SSZ
fn pad2(n : Int) -> String {
if n < 10 {
"0" + n.to_string()
} else {
n.to_string()
}
}
fn pad4(n : Int) -> String {
if n < 10 {
"000" + n.to_string()
} else if n < 100 {
"00" + n.to_string()
} else if n < 1000 {
"0" + n.to_string()
} else {
n.to_string()
}
}
pad4(year) +
"-" +
pad2(month) +
"-" +
pad2(day) +
" " +
pad2(hh) +
":" +
pad2(mm) +
":" +
pad2(ss) +
"Z"
}