things_mcp/core/reader/
dates.rs1pub fn decode_things_date(packed: i64) -> Option<String> {
21 if packed == 0 {
22 return None;
23 }
24 let year = ((packed >> 16) & 0xFFF) as i32;
25 let month = ((packed >> 12) & 0x0F) as u32;
26 let day = ((packed >> 7) & 0x1F) as u32;
27 if year < 1900 || !(1..=12).contains(&month) || !(1..=31).contains(&day) {
28 return None;
29 }
30 Some(format!("{year:04}-{month:02}-{day:02}"))
31}
32
33pub fn pack_things_date(year: i32, month: u32, day: u32) -> i64 {
37 ((year as i64) << 16) | ((month as i64) << 12) | ((day as i64) << 7)
38}
39
40pub fn parse_iso_date(iso: &str) -> Option<(i32, u32, u32)> {
44 if iso.len() != 10 {
45 return None;
46 }
47 let bytes = iso.as_bytes();
48 if bytes[4] != b'-' || bytes[7] != b'-' {
49 return None;
50 }
51 let y: i32 = iso.get(0..4)?.parse().ok()?;
52 let m: u32 = iso.get(5..7)?.parse().ok()?;
53 let d: u32 = iso.get(8..10)?.parse().ok()?;
54 if !(1..=12).contains(&m) || !(1..=31).contains(&d) {
55 return None;
56 }
57 Some((y, m, d))
58}
59
60pub fn ymd_to_unix_utc(year: i32, month: u32, day: u32) -> i64 {
65 let mut days: i64 = 0;
66 let from_year = 1970;
67 if year >= from_year {
68 for yi in from_year..year {
69 let leap = (yi % 4 == 0 && yi % 100 != 0) || (yi % 400 == 0);
70 days += if leap { 366 } else { 365 };
71 }
72 } else {
73 for yi in year..from_year {
74 let leap = (yi % 4 == 0 && yi % 100 != 0) || (yi % 400 == 0);
75 days -= if leap { 366 } else { 365 };
76 }
77 }
78 let leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
79 let months_len: [u32; 12] = [
80 31,
81 if leap { 29 } else { 28 },
82 31,
83 30,
84 31,
85 30,
86 31,
87 31,
88 30,
89 31,
90 30,
91 31,
92 ];
93 for mo in 1..month {
94 days += months_len[(mo - 1) as usize] as i64;
95 }
96 days += (day - 1) as i64;
97 days * 86_400
98}
99
100pub fn today_packed_utc() -> i64 {
103 let secs = std::time::SystemTime::now()
104 .duration_since(std::time::UNIX_EPOCH)
105 .map(|d| d.as_secs() as i64)
106 .unwrap_or(0);
107 let (y, m, d, _, _, _) = crate::core::backup::unix_to_ymdhms(secs);
108 pack_things_date(y, m, d)
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn decode_zero_is_none() {
117 assert_eq!(decode_things_date(0), None);
118 }
119
120 #[test]
121 fn decode_known_packed_value() {
122 let p = pack_things_date(2026, 5, 20);
124 assert_eq!(decode_things_date(p), Some("2026-05-20".to_string()));
125 }
126
127 #[test]
128 fn pack_then_decode_round_trip() {
129 for (y, m, d) in [(2000, 1, 1), (2026, 12, 31), (2099, 12, 31)] {
130 let p = pack_things_date(y, m, d);
131 assert_eq!(
132 decode_things_date(p),
133 Some(format!("{y:04}-{m:02}-{d:02}"))
134 );
135 }
136 }
137
138 #[test]
139 fn decode_rejects_malformed_year() {
140 let p = pack_things_date(1800, 1, 1);
142 assert_eq!(decode_things_date(p), None);
143 }
144
145 #[test]
146 fn parse_iso_date_happy_path() {
147 assert_eq!(parse_iso_date("2026-05-20"), Some((2026, 5, 20)));
148 }
149
150 #[test]
151 fn parse_iso_date_rejects_wrong_separators() {
152 assert_eq!(parse_iso_date("2026/05/20"), None);
153 }
154
155 #[test]
156 fn parse_iso_date_rejects_wrong_length() {
157 assert_eq!(parse_iso_date("2026-5-20"), None);
158 assert_eq!(parse_iso_date("2026-05-2"), None);
159 }
160
161 #[test]
162 fn ymd_to_unix_utc_at_epoch() {
163 assert_eq!(ymd_to_unix_utc(1970, 1, 1), 0);
164 assert_eq!(ymd_to_unix_utc(1970, 1, 2), 86_400);
165 }
166
167 #[test]
168 fn ymd_to_unix_utc_leap_day() {
169 let a = ymd_to_unix_utc(2024, 2, 29);
173 let b = ymd_to_unix_utc(2024, 3, 1);
174 assert_eq!(b - a, 86_400);
175 }
176
177 #[test]
178 fn today_packed_utc_decodes_to_real_date() {
179 let p = today_packed_utc();
180 let s = decode_things_date(p).expect("today must decode");
181 assert_eq!(s.len(), 10);
182 assert_eq!(&s[4..5], "-");
183 assert_eq!(&s[7..8], "-");
184 }
185}