affs_read/
date.rs

1//! Date/time handling for Amiga format.
2
3/// Amiga date representation.
4///
5/// Amiga stores dates as days since January 1, 1978,
6/// minutes since midnight, and ticks (1/50 second).
7#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
8pub struct AmigaDate {
9    /// Days since January 1, 1978.
10    pub days: i32,
11    /// Minutes since midnight.
12    pub mins: i32,
13    /// Ticks (1/50 second).
14    pub ticks: i32,
15}
16
17impl AmigaDate {
18    /// Create a new Amiga date from raw values.
19    #[inline]
20    pub const fn new(days: i32, mins: i32, ticks: i32) -> Self {
21        Self { days, mins, ticks }
22    }
23
24    /// Convert to a more usable date format.
25    #[inline]
26    pub fn to_date_time(self) -> DateTime {
27        let (year, month, day) = days_to_date(self.days);
28        let hour = (self.mins / 60) as u8;
29        let minute = (self.mins % 60) as u8;
30        let second = (self.ticks / 50) as u8;
31
32        DateTime {
33            year,
34            month,
35            day,
36            hour,
37            minute,
38            second,
39        }
40    }
41
42    /// Convert to Unix timestamp (seconds since 1970-01-01 00:00:00 UTC).
43    ///
44    /// This matches GRUB's `aftime2ctime()` behavior:
45    /// `days * 86400 + min * 60 + hz / 50 + epoch_offset`
46    ///
47    /// The Amiga epoch is January 1, 1978, which is 8 years (2922 days)
48    /// after the Unix epoch.
49    #[inline]
50    pub const fn to_unix_timestamp(self) -> i64 {
51        const SECONDS_PER_DAY: i64 = 86400;
52        const SECONDS_PER_MINUTE: i64 = 60;
53        const TICKS_PER_SECOND: i64 = 50;
54        // 8 years from 1970 to 1978 (including leap years 1972, 1976)
55        // = 365 * 8 + 2 = 2922 days
56        const EPOCH_OFFSET: i64 = 2922 * SECONDS_PER_DAY;
57
58        (self.days as i64) * SECONDS_PER_DAY
59            + (self.mins as i64) * SECONDS_PER_MINUTE
60            + (self.ticks as i64) / TICKS_PER_SECOND
61            + EPOCH_OFFSET
62    }
63}
64
65/// Decoded date and time.
66#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
67pub struct DateTime {
68    /// Year (e.g., 1978-2100).
69    pub year: u16,
70    /// Month (1-12).
71    pub month: u8,
72    /// Day of month (1-31).
73    pub day: u8,
74    /// Hour (0-23).
75    pub hour: u8,
76    /// Minute (0-59).
77    pub minute: u8,
78    /// Second (0-59).
79    pub second: u8,
80}
81
82/// Convert days since 1978-01-01 to (year, month, day).
83fn days_to_date(mut days: i32) -> (u16, u8, u8) {
84    const DAYS_IN_MONTH: [i32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
85
86    let mut year = 1978u16;
87
88    // Find year
89    loop {
90        let days_in_year = if is_leap_year(year) { 366 } else { 365 };
91        if days < days_in_year {
92            break;
93        }
94        days -= days_in_year;
95        year += 1;
96    }
97
98    // Find month
99    let mut month = 1u8;
100    let leap = is_leap_year(year);
101    for (i, &days_in_month) in DAYS_IN_MONTH.iter().enumerate() {
102        let dim = if i == 1 && leap { 29 } else { days_in_month };
103        if days < dim {
104            break;
105        }
106        days -= dim;
107        month += 1;
108    }
109
110    (year, month, (days + 1) as u8)
111}
112
113/// Check if a year is a leap year.
114#[inline]
115const fn is_leap_year(year: u16) -> bool {
116    if year.is_multiple_of(100) {
117        year.is_multiple_of(400)
118    } else {
119        year.is_multiple_of(4)
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_epoch() {
129        let date = AmigaDate::new(0, 0, 0);
130        let dt = date.to_date_time();
131        assert_eq!(dt.year, 1978);
132        assert_eq!(dt.month, 1);
133        assert_eq!(dt.day, 1);
134        assert_eq!(dt.hour, 0);
135        assert_eq!(dt.minute, 0);
136        assert_eq!(dt.second, 0);
137    }
138
139    #[test]
140    fn test_known_date() {
141        // 1997-02-18 is day 6988
142        let date = AmigaDate::new(6988, 0, 0);
143        let dt = date.to_date_time();
144        assert_eq!(dt.year, 1997);
145        assert_eq!(dt.month, 2);
146        assert_eq!(dt.day, 18);
147    }
148
149    #[test]
150    fn test_time() {
151        let date = AmigaDate::new(0, 754, 150); // 12:34:03
152        let dt = date.to_date_time();
153        assert_eq!(dt.hour, 12);
154        assert_eq!(dt.minute, 34);
155        assert_eq!(dt.second, 3);
156    }
157
158    #[test]
159    fn test_leap_year() {
160        assert!(is_leap_year(2000));
161        assert!(!is_leap_year(1900));
162        assert!(is_leap_year(1984));
163        assert!(!is_leap_year(1983));
164    }
165}