1#![allow(deprecated)]
3use crate::error::{Result as ThingsResult, ThingsError};
10use crate::models::{TaskStatus, TaskType};
11use chrono::NaiveDate;
12
13pub(crate) fn safe_timestamp_convert(ts_f64: f64) -> i64 {
15 if ts_f64.is_finite() && ts_f64 >= 0.0 {
17 let max_timestamp = 4_102_444_800_f64; if ts_f64 <= max_timestamp {
20 let ts_str = format!("{:.0}", ts_f64.trunc());
22 ts_str.parse::<i64>().unwrap_or(0)
23 } else {
24 0 }
26 } else {
27 0 }
29}
30
31pub(crate) fn things_date_to_naive_date(seconds_since_2001: i64) -> Option<chrono::NaiveDate> {
33 use chrono::{TimeZone, Utc};
34
35 if seconds_since_2001 <= 0 {
36 return None;
37 }
38
39 let base_date = Utc.with_ymd_and_hms(2001, 1, 1, 0, 0, 0).single().unwrap();
41
42 let date_time = base_date + chrono::Duration::seconds(seconds_since_2001);
44
45 Some(date_time.date_naive())
46}
47
48pub fn naive_date_to_things_timestamp(date: NaiveDate) -> i64 {
50 use chrono::{NaiveTime, TimeZone, Utc};
51
52 let base_date = Utc.with_ymd_and_hms(2001, 1, 1, 0, 0, 0).single().unwrap();
54
55 let date_time = date
57 .and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap())
58 .and_local_timezone(Utc)
59 .single()
60 .unwrap();
61
62 date_time.timestamp() - base_date.timestamp()
64}
65
66pub fn serialize_tags_to_blob(tags: &[String]) -> ThingsResult<Vec<u8>> {
70 serde_json::to_vec(tags)
71 .map_err(|e| ThingsError::unknown(format!("Failed to serialize tags: {e}")))
72}
73
74pub fn deserialize_tags_from_blob(blob: &[u8]) -> ThingsResult<Vec<String>> {
76 if blob.is_empty() {
77 return Ok(Vec::new());
78 }
79 serde_json::from_slice(blob)
80 .map_err(|e| ThingsError::unknown(format!("Failed to deserialize tags: {e}")))
81}
82
83impl TaskStatus {
84 pub(crate) fn from_i32(value: i32) -> Option<Self> {
85 match value {
86 0 => Some(TaskStatus::Incomplete),
87 2 => Some(TaskStatus::Canceled),
88 3 => Some(TaskStatus::Completed),
89 _ => None,
90 }
91 }
92}
93
94impl TaskType {
95 pub(crate) fn from_i32(value: i32) -> Option<Self> {
96 match value {
97 0 => Some(TaskType::Todo),
98 1 => Some(TaskType::Project),
99 2 => Some(TaskType::Heading),
100 3 => Some(TaskType::Area),
101 _ => None,
102 }
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn test_task_status_from_i32() {
112 assert_eq!(TaskStatus::from_i32(0), Some(TaskStatus::Incomplete));
113 assert_eq!(TaskStatus::from_i32(1), None); assert_eq!(TaskStatus::from_i32(2), Some(TaskStatus::Canceled));
115 assert_eq!(TaskStatus::from_i32(3), Some(TaskStatus::Completed));
116 assert_eq!(TaskStatus::from_i32(4), None);
117 assert_eq!(TaskStatus::from_i32(-1), None);
118 }
119
120 #[test]
121 fn test_task_type_from_i32() {
122 assert_eq!(TaskType::from_i32(0), Some(TaskType::Todo));
123 assert_eq!(TaskType::from_i32(1), Some(TaskType::Project));
124 assert_eq!(TaskType::from_i32(2), Some(TaskType::Heading));
125 assert_eq!(TaskType::from_i32(3), Some(TaskType::Area));
126 assert_eq!(TaskType::from_i32(4), None);
127 assert_eq!(TaskType::from_i32(-1), None);
128 }
129
130 #[test]
131 fn test_safe_timestamp_convert_edge_cases() {
132 assert_eq!(safe_timestamp_convert(1_609_459_200.0), 1_609_459_200); assert_eq!(safe_timestamp_convert(0.0), 0);
137
138 assert_eq!(safe_timestamp_convert(-1.0), 0);
140
141 assert_eq!(safe_timestamp_convert(f64::INFINITY), 0);
143
144 assert_eq!(safe_timestamp_convert(f64::NAN), 0);
146
147 assert_eq!(safe_timestamp_convert(5_000_000_000.0), 0);
149
150 let max_timestamp = 4_102_444_800_f64; assert_eq!(safe_timestamp_convert(max_timestamp), 4_102_444_800);
153 }
154
155 #[test]
156 fn test_task_status_from_i32_all_variants() {
157 assert_eq!(TaskStatus::from_i32(0), Some(TaskStatus::Incomplete));
158 assert_eq!(TaskStatus::from_i32(1), None); assert_eq!(TaskStatus::from_i32(2), Some(TaskStatus::Canceled));
160 assert_eq!(TaskStatus::from_i32(3), Some(TaskStatus::Completed));
161 assert_eq!(TaskStatus::from_i32(999), None);
162 assert_eq!(TaskStatus::from_i32(-1), None);
163 }
164
165 #[test]
166 fn test_task_type_from_i32_all_variants() {
167 assert_eq!(TaskType::from_i32(0), Some(TaskType::Todo));
168 assert_eq!(TaskType::from_i32(1), Some(TaskType::Project));
169 assert_eq!(TaskType::from_i32(2), Some(TaskType::Heading));
170 assert_eq!(TaskType::from_i32(3), Some(TaskType::Area));
171 assert_eq!(TaskType::from_i32(999), None);
172 assert_eq!(TaskType::from_i32(-1), None);
173 }
174
175 #[test]
176 fn test_things_date_negative_returns_none() {
177 assert_eq!(things_date_to_naive_date(-1), None);
179 assert_eq!(things_date_to_naive_date(-100), None);
180 assert_eq!(things_date_to_naive_date(i64::MIN), None);
181 }
182
183 #[test]
184 fn test_things_date_zero_returns_none() {
185 assert_eq!(things_date_to_naive_date(0), None);
187 }
188
189 #[test]
190 fn test_things_date_boundary_2001() {
191 use chrono::Datelike;
192 let result = things_date_to_naive_date(1);
194 assert!(result.is_some());
195
196 let date = result.unwrap();
197 assert_eq!(date.year(), 2001);
198 assert_eq!(date.month(), 1);
199 assert_eq!(date.day(), 1);
200 }
201
202 #[test]
203 fn test_things_date_one_day() {
204 use chrono::Datelike;
205 let seconds_per_day = 86400i64;
207 let result = things_date_to_naive_date(seconds_per_day);
208 assert!(result.is_some());
209
210 let date = result.unwrap();
211 assert_eq!(date.year(), 2001);
212 assert_eq!(date.month(), 1);
213 assert_eq!(date.day(), 2);
214 }
215
216 #[test]
217 fn test_things_date_one_year() {
218 use chrono::Datelike;
219 let seconds_per_year = 365 * 86400i64;
221 let result = things_date_to_naive_date(seconds_per_year);
222 assert!(result.is_some());
223
224 let date = result.unwrap();
225 assert_eq!(date.year(), 2002);
226 }
227
228 #[test]
229 fn test_things_date_current_era() {
230 use chrono::Datelike;
231 let days_to_2024 = 8401i64;
235 let seconds_to_2024 = days_to_2024 * 86400;
236
237 let result = things_date_to_naive_date(seconds_to_2024);
238 assert!(result.is_some());
239
240 let date = result.unwrap();
241 assert_eq!(date.year(), 2024);
242 }
243
244 #[test]
245 fn test_things_date_leap_year() {
246 use chrono::{Datelike, TimeZone, Utc};
247 let base_date = Utc.with_ymd_and_hms(2001, 1, 1, 0, 0, 0).single().unwrap();
250 let target_date = Utc.with_ymd_and_hms(2004, 2, 29, 0, 0, 0).single().unwrap();
251 let seconds_diff = (target_date - base_date).num_seconds();
252
253 let result = things_date_to_naive_date(seconds_diff);
254 assert!(result.is_some());
255
256 let date = result.unwrap();
257 assert_eq!(date.year(), 2004);
258 assert_eq!(date.month(), 2);
259 assert_eq!(date.day(), 29);
260 }
261
262 #[test]
263 fn test_safe_timestamp_convert_normal_values() {
264 let ts = 1_700_000_000.0; let result = safe_timestamp_convert(ts);
267 assert_eq!(result, 1_700_000_000);
268 }
269
270 #[test]
271 fn test_safe_timestamp_convert_zero() {
272 assert_eq!(safe_timestamp_convert(0.0), 0);
274 }
275
276 #[test]
277 fn test_safe_timestamp_convert_negative() {
278 assert_eq!(safe_timestamp_convert(-1.0), 0);
280 assert_eq!(safe_timestamp_convert(-1000.0), 0);
281 }
282
283 #[test]
284 fn test_safe_timestamp_convert_infinity() {
285 assert_eq!(safe_timestamp_convert(f64::INFINITY), 0);
287 assert_eq!(safe_timestamp_convert(f64::NEG_INFINITY), 0);
288 }
289
290 #[test]
291 fn test_safe_timestamp_convert_nan() {
292 assert_eq!(safe_timestamp_convert(f64::NAN), 0);
294 }
295
296 #[test]
297 fn test_date_roundtrip_known_dates() {
298 use chrono::{Datelike, TimeZone, Utc};
299 let test_cases = vec![
303 (2001, 1, 2), (2010, 6, 15),
305 (2020, 12, 31),
306 (2024, 2, 29), (2025, 7, 4),
308 ];
309
310 for (year, month, day) in test_cases {
311 let base_date = Utc.with_ymd_and_hms(2001, 1, 1, 0, 0, 0).single().unwrap();
312 let target_date = Utc
313 .with_ymd_and_hms(year, month, day, 0, 0, 0)
314 .single()
315 .unwrap();
316 let seconds = (target_date - base_date).num_seconds();
317
318 let converted = things_date_to_naive_date(seconds);
319 assert!(
320 converted.is_some(),
321 "Failed to convert {}-{:02}-{:02}",
322 year,
323 month,
324 day
325 );
326
327 let result_date = converted.unwrap();
328 assert_eq!(
329 result_date.year(),
330 year,
331 "Year mismatch for {}-{:02}-{:02}",
332 year,
333 month,
334 day
335 );
336 assert_eq!(
337 result_date.month(),
338 month,
339 "Month mismatch for {}-{:02}-{:02}",
340 year,
341 month,
342 day
343 );
344 assert_eq!(
345 result_date.day(),
346 day,
347 "Day mismatch for {}-{:02}-{:02}",
348 year,
349 month,
350 day
351 );
352 }
353 }
354}