Skip to main content

fat/
time.rs

1//! FAT packed date/time decode.
2//!
3//! FAT stores wall-clock local time: a 16-bit date (day/month/year-since-1980)
4//! and a 16-bit time (2-second resolution), with an optional 10 ms "tenths"
5//! field on creation. There is no timezone — consumers treat these as local.
6
7/// A decoded FAT timestamp as seconds since the Unix epoch plus a sub-second
8/// remainder. The value is wall-clock local time (FAT carries no zone).
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct FatTimestamp {
11    /// Seconds since 1970-01-01 (interpreting the fields as if UTC).
12    pub unix_seconds: i64,
13    /// Sub-second remainder in nanoseconds (from the 10 ms tenths field).
14    pub subsec_nanos: u32,
15}
16
17/// Decode a packed FAT `(date, time, tenths)` triple. Returns `None` for an
18/// unset entry (`date == 0`). Out-of-range fields are clamped rather than
19/// rejected — a corrupt timestamp still yields a value, never a panic.
20pub fn decode(date: u16, time: u16, tenths: u8) -> Option<FatTimestamp> {
21    if date == 0 {
22        return None;
23    }
24    let day = i64::from(date & 0x1F);
25    let month = i64::from((date >> 5) & 0x0F);
26    let year = 1980 + i64::from((date >> 9) & 0x7F);
27
28    let two_sec = i64::from(time & 0x1F);
29    let minute = i64::from((time >> 5) & 0x3F);
30    let hour = i64::from((time >> 11) & 0x1F);
31
32    let days = days_from_civil(year, month.clamp(1, 12), day.clamp(1, 31));
33    let base = days * 86_400 + hour.min(23) * 3600 + minute.min(59) * 60 + two_sec * 2;
34
35    let tenths = i64::from(tenths);
36    let unix_seconds = base + tenths / 100;
37    let subsec_nanos = u32::try_from((tenths % 100) * 10_000_000).unwrap_or(0);
38    Some(FatTimestamp {
39        unix_seconds,
40        subsec_nanos,
41    })
42}
43
44/// Days since 1970-01-01 for a proleptic-Gregorian date (Howard Hinnant's
45/// algorithm). Valid for any in-range civil date.
46fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
47    let y = if m <= 2 { y - 1 } else { y };
48    let era = if y >= 0 { y } else { y - 399 } / 400;
49    let yoe = y - era * 400;
50    let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
51    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
52    era * 146_097 + doe - 719_468
53}
54
55#[cfg(test)]
56mod tests {
57    use super::{decode, FatTimestamp};
58
59    /// Pack a FAT date field from a civil date.
60    fn mk_date(year: u16, month: u16, day: u16) -> u16 {
61        ((year - 1980) << 9) | (month << 5) | day
62    }
63
64    /// Pack a FAT time field (seconds are stored halved).
65    fn mk_time(hour: u16, minute: u16, second: u16) -> u16 {
66        (hour << 11) | (minute << 5) | (second / 2)
67    }
68
69    #[test]
70    fn decodes_a_known_datetime() {
71        let ts = decode(mk_date(2021, 6, 15), mk_time(13, 37, 20), 0).unwrap();
72        // 2021-06-15T13:37:20Z == 1623764240 unix seconds
73        assert_eq!(ts.unix_seconds, 1_623_764_240);
74        assert_eq!(ts.subsec_nanos, 0);
75    }
76
77    #[test]
78    fn tenths_add_sub_second_and_carry() {
79        // tenths = 150 → +1 second and 500 ms.
80        let ts = decode(mk_date(2021, 6, 15), mk_time(13, 37, 20), 150).unwrap();
81        assert_eq!(ts.unix_seconds, 1_623_764_241);
82        assert_eq!(ts.subsec_nanos, 500_000_000);
83    }
84
85    #[test]
86    fn zero_date_is_unset() {
87        assert!(decode(0, 0, 0).is_none());
88    }
89
90    #[test]
91    fn epoch_1980_is_positive() {
92        let ts = decode(mk_date(1980, 1, 1), 0, 0).unwrap();
93        assert_eq!(ts.unix_seconds, 315_532_800); // 1980-01-01T00:00:00Z
94    }
95
96    #[test]
97    fn typed_fields_accessible() {
98        let _ = FatTimestamp {
99            unix_seconds: 0,
100            subsec_nanos: 0,
101        };
102    }
103}