things3_common/
utils.rs

1//! Utility functions for Things 3 integration
2
3use chrono::{DateTime, NaiveDate, Utc};
4use std::path::PathBuf;
5
6/// Get the default Things 3 database path
7#[must_use]
8pub fn get_default_database_path() -> PathBuf {
9    let home = std::env::var("HOME").unwrap_or_else(|_| "~".to_string());
10    PathBuf::from(format!(
11        "{home}/Library/Group Containers/JLMPQHK86H.com.culturedcode.ThingsMac/ThingsData-0Z0Z2/Things Database.thingsdatabase/main.sqlite"
12    ))
13}
14
15/// Format a date for display
16#[must_use]
17pub fn format_date(date: &NaiveDate) -> String {
18    date.format("%Y-%m-%d").to_string()
19}
20
21/// Format a datetime for display
22#[must_use]
23pub fn format_datetime(dt: &DateTime<Utc>) -> String {
24    dt.format("%Y-%m-%d %H:%M:%S UTC").to_string()
25}
26
27/// Parse a date string in YYYY-MM-DD format
28///
29/// # Errors
30/// Returns `chrono::ParseError` if the date string is not in the expected format
31pub fn parse_date(date_str: &str) -> Result<NaiveDate, chrono::ParseError> {
32    NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
33}
34
35/// Validate a UUID string
36#[must_use]
37pub fn is_valid_uuid(uuid_str: &str) -> bool {
38    uuid::Uuid::parse_str(uuid_str).is_ok()
39}
40
41/// Truncate a string to a maximum length
42#[must_use]
43pub fn truncate_string(s: &str, max_len: usize) -> String {
44    if s.len() <= max_len {
45        s.to_string()
46    } else {
47        format!("{}...", &s[..max_len.saturating_sub(3)])
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54    use chrono::{Datelike, NaiveDate};
55
56    #[test]
57    fn test_get_default_database_path() {
58        let path = get_default_database_path();
59
60        // Should contain the expected path components
61        assert!(path.to_string_lossy().contains("Library"));
62        assert!(path.to_string_lossy().contains("Group Containers"));
63        assert!(path
64            .to_string_lossy()
65            .contains("JLMPQHK86H.com.culturedcode.ThingsMac"));
66        assert!(path.to_string_lossy().contains("ThingsData-0Z0Z2"));
67        assert!(path
68            .to_string_lossy()
69            .contains("Things Database.thingsdatabase"));
70        assert!(path.to_string_lossy().contains("main.sqlite"));
71
72        // Should start with home directory
73        let home = std::env::var("HOME").unwrap_or_else(|_| "~".to_string());
74        assert!(path.to_string_lossy().starts_with(&home));
75    }
76
77    #[test]
78    fn test_format_date() {
79        let date = NaiveDate::from_ymd_opt(2023, 12, 25).unwrap();
80        let formatted = format_date(&date);
81        assert_eq!(formatted, "2023-12-25");
82    }
83
84    #[test]
85    fn test_format_date_edge_cases() {
86        // Test January 1st
87        let date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
88        let formatted = format_date(&date);
89        assert_eq!(formatted, "2024-01-01");
90
91        // Test December 31st
92        let date = NaiveDate::from_ymd_opt(2023, 12, 31).unwrap();
93        let formatted = format_date(&date);
94        assert_eq!(formatted, "2023-12-31");
95
96        // Test leap year
97        let date = NaiveDate::from_ymd_opt(2024, 2, 29).unwrap();
98        let formatted = format_date(&date);
99        assert_eq!(formatted, "2024-02-29");
100    }
101
102    #[test]
103    fn test_format_datetime() {
104        let dt = Utc::now();
105        let formatted = format_datetime(&dt);
106
107        // Should contain the expected format components
108        assert!(formatted.contains("UTC"));
109        assert!(formatted.contains("-"));
110        assert!(formatted.contains(" "));
111        assert!(formatted.contains(":"));
112
113        // Should be in the expected format
114        assert!(formatted.len() >= 20); // At least "YYYY-MM-DD HH:MM:SS UTC"
115    }
116
117    #[test]
118    fn test_format_datetime_specific() {
119        // Test with a specific datetime
120        let dt = DateTime::parse_from_rfc3339("2023-12-25T15:30:45Z")
121            .unwrap()
122            .with_timezone(&Utc);
123        let formatted = format_datetime(&dt);
124        assert_eq!(formatted, "2023-12-25 15:30:45 UTC");
125    }
126
127    #[test]
128    fn test_parse_date_valid() {
129        let result = parse_date("2023-12-25");
130        assert!(result.is_ok());
131        let date = result.unwrap();
132        assert_eq!(date.year(), 2023);
133        assert_eq!(date.month(), 12);
134        assert_eq!(date.day(), 25);
135    }
136
137    #[test]
138    fn test_parse_date_edge_cases() {
139        // Test January 1st
140        let result = parse_date("2024-01-01");
141        assert!(result.is_ok());
142        let date = result.unwrap();
143        assert_eq!(date.year(), 2024);
144        assert_eq!(date.month(), 1);
145        assert_eq!(date.day(), 1);
146
147        // Test December 31st
148        let result = parse_date("2023-12-31");
149        assert!(result.is_ok());
150        let date = result.unwrap();
151        assert_eq!(date.year(), 2023);
152        assert_eq!(date.month(), 12);
153        assert_eq!(date.day(), 31);
154
155        // Test leap year
156        let result = parse_date("2024-02-29");
157        assert!(result.is_ok());
158        let date = result.unwrap();
159        assert_eq!(date.year(), 2024);
160        assert_eq!(date.month(), 2);
161        assert_eq!(date.day(), 29);
162    }
163
164    #[test]
165    fn test_parse_date_invalid() {
166        // Test invalid format
167        let result = parse_date("2023/12/25");
168        assert!(result.is_err());
169
170        // Test invalid date
171        let result = parse_date("2023-13-01");
172        assert!(result.is_err());
173
174        // Test invalid day
175        let result = parse_date("2023-02-30");
176        assert!(result.is_err());
177
178        // Test empty string
179        let result = parse_date("");
180        assert!(result.is_err());
181
182        // Test malformed string
183        let result = parse_date("not-a-date");
184        assert!(result.is_err());
185    }
186
187    #[test]
188    fn test_is_valid_uuid_valid() {
189        // Test valid UUIDs
190        assert!(is_valid_uuid("550e8400-e29b-41d4-a716-446655440000"));
191        assert!(is_valid_uuid("6ba7b810-9dad-11d1-80b4-00c04fd430c8"));
192        assert!(is_valid_uuid("6ba7b811-9dad-11d1-80b4-00c04fd430c8"));
193        assert!(is_valid_uuid("00000000-0000-0000-0000-000000000000"));
194        assert!(is_valid_uuid("ffffffff-ffff-ffff-ffff-ffffffffffff"));
195    }
196
197    #[test]
198    fn test_is_valid_uuid_invalid() {
199        // Test invalid UUIDs
200        assert!(!is_valid_uuid(""));
201        assert!(!is_valid_uuid("not-a-uuid"));
202        assert!(!is_valid_uuid("550e8400-e29b-41d4-a716"));
203        assert!(!is_valid_uuid("550e8400-e29b-41d4-a716-44665544000"));
204        assert!(!is_valid_uuid("550e8400-e29b-41d4-a716-4466554400000"));
205        assert!(!is_valid_uuid("550e8400-e29b-41d4-a716-44665544000g"));
206        assert!(!is_valid_uuid("550e8400-e29b-41d4-a716-44665544000-"));
207        assert!(!is_valid_uuid("550e8400-e29b-41d4-a716-44665544000 "));
208    }
209
210    #[test]
211    fn test_truncate_string_short() {
212        // Test string shorter than max length
213        let result = truncate_string("hello", 10);
214        assert_eq!(result, "hello");
215
216        // Test string equal to max length
217        let result = truncate_string("hello", 5);
218        assert_eq!(result, "hello");
219    }
220
221    #[test]
222    fn test_truncate_string_long() {
223        // Test string longer than max length
224        let result = truncate_string("hello world", 8);
225        assert_eq!(result, "hello...");
226
227        // Test string much longer than max length
228        let result = truncate_string("this is a very long string", 10);
229        assert_eq!(result, "this is...");
230    }
231
232    #[test]
233    fn test_truncate_string_edge_cases() {
234        // Test with max_len = 0
235        let result = truncate_string("hello", 0);
236        assert_eq!(result, "...");
237
238        // Test with max_len = 1
239        let result = truncate_string("hello", 1);
240        assert_eq!(result, "...");
241
242        // Test with max_len = 2
243        let result = truncate_string("hello", 2);
244        assert_eq!(result, "...");
245
246        // Test with max_len = 3
247        let result = truncate_string("hello", 3);
248        assert_eq!(result, "...");
249
250        // Test with max_len = 4
251        let result = truncate_string("hello", 4);
252        assert_eq!(result, "h...");
253
254        // Test with max_len = 5
255        let result = truncate_string("hello", 5);
256        assert_eq!(result, "hello");
257    }
258
259    #[test]
260    fn test_truncate_string_empty() {
261        // Test empty string
262        let result = truncate_string("", 10);
263        assert_eq!(result, "");
264
265        // Test empty string with max_len = 0
266        let result = truncate_string("", 0);
267        assert_eq!(result, "");
268    }
269
270    #[test]
271    fn test_truncate_string_unicode() {
272        // Test with unicode characters
273        let result = truncate_string("hello δΈ–η•Œ", 8);
274        assert_eq!(result, "hello...");
275
276        // Test with emoji
277        let result = truncate_string("hello πŸ˜€", 8);
278        assert_eq!(result, "hello...");
279    }
280
281    #[test]
282    fn test_truncate_string_very_long() {
283        // Test with very long string
284        let long_string = "a".repeat(1000);
285        let result = truncate_string(&long_string, 10);
286        assert_eq!(result, "aaaaaaa...");
287        assert_eq!(result.len(), 10);
288    }
289
290    #[test]
291    fn test_utils_integration() {
292        // Test integration between functions
293        let date_str = "2023-12-25";
294        let parsed_date = parse_date(date_str).unwrap();
295        let formatted_date = format_date(&parsed_date);
296        assert_eq!(formatted_date, date_str);
297
298        // Test UUID validation with truncation
299        let uuid = "550e8400-e29b-41d4-a716-446655440000";
300        assert!(is_valid_uuid(uuid));
301        let truncated = truncate_string(uuid, 20);
302        assert_eq!(truncated, "550e8400-e29b-41d...");
303    }
304
305    #[test]
306    fn test_get_default_database_path_consistency() {
307        // Test that the function returns the same path on multiple calls
308        let path1 = get_default_database_path();
309        let path2 = get_default_database_path();
310        assert_eq!(path1, path2);
311    }
312
313    #[test]
314    fn test_format_date_consistency() {
315        // Test that formatting and parsing are consistent
316        let date = NaiveDate::from_ymd_opt(2023, 12, 25).unwrap();
317        let formatted = format_date(&date);
318        let parsed = parse_date(&formatted).unwrap();
319        assert_eq!(date, parsed);
320    }
321}