things3_common/
utils.rs

1//! Utility functions for Things 3 integration
2
3use chrono::{DateTime, NaiveDate, Utc};
4
5/// Format a date for display
6#[must_use]
7pub fn format_date(date: &NaiveDate) -> String {
8    date.format("%Y-%m-%d").to_string()
9}
10
11/// Format a datetime for display
12#[must_use]
13pub fn format_datetime(dt: &DateTime<Utc>) -> String {
14    dt.format("%Y-%m-%d %H:%M:%S UTC").to_string()
15}
16
17/// Parse a date string in YYYY-MM-DD format
18///
19/// # Errors
20/// Returns `chrono::ParseError` if the date string is not in the expected format
21pub fn parse_date(date_str: &str) -> Result<NaiveDate, chrono::ParseError> {
22    NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
23}
24
25/// Validate a UUID string
26#[must_use]
27pub fn is_valid_uuid(uuid_str: &str) -> bool {
28    uuid::Uuid::parse_str(uuid_str).is_ok()
29}
30
31/// Truncate a string to a maximum length
32///
33/// # Examples
34///
35/// ```
36/// use things3_common::truncate_string;
37///
38/// assert_eq!(truncate_string("hello world", 5), "he...");
39/// assert_eq!(truncate_string("hi", 10), "hi");
40/// assert_eq!(truncate_string("test", 3), "...");
41/// ```
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, TimeZone, Utc};
55
56    #[test]
57    fn test_format_date() {
58        let date = NaiveDate::from_ymd_opt(2023, 12, 25).unwrap();
59        let formatted = format_date(&date);
60        assert_eq!(formatted, "2023-12-25");
61
62        // Test edge cases
63        let date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
64        let formatted = format_date(&date);
65        assert_eq!(formatted, "2024-01-01");
66
67        let date = NaiveDate::from_ymd_opt(2024, 2, 29).unwrap(); // Leap year
68        let formatted = format_date(&date);
69        assert_eq!(formatted, "2024-02-29");
70    }
71
72    #[test]
73    fn test_format_datetime() {
74        // Test with specific datetime for predictable results
75        let dt = Utc.with_ymd_and_hms(2023, 12, 25, 15, 30, 45).unwrap();
76        let formatted = format_datetime(&dt);
77        assert_eq!(formatted, "2023-12-25 15:30:45 UTC");
78
79        // Test edge cases
80        let dt = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
81        let formatted = format_datetime(&dt);
82        assert_eq!(formatted, "2024-01-01 00:00:00 UTC");
83    }
84
85    #[test]
86    fn test_parse_date_valid() {
87        let result = parse_date("2023-12-25");
88        assert!(result.is_ok());
89        let date = result.unwrap();
90        assert_eq!(date.year(), 2023);
91        assert_eq!(date.month(), 12);
92        assert_eq!(date.day(), 25);
93
94        // Test edge cases
95        assert!(parse_date("2024-01-01").is_ok());
96        assert!(parse_date("2024-02-29").is_ok()); // Leap year
97    }
98
99    #[test]
100    fn test_parse_date_invalid() {
101        // Test invalid formats
102        assert!(parse_date("2023/12/25").is_err());
103        assert!(parse_date("2023-13-01").is_err()); // Invalid month
104        assert!(parse_date("2023-02-30").is_err()); // Invalid day
105        assert!(parse_date("").is_err());
106        assert!(parse_date("not-a-date").is_err());
107        assert!(parse_date("2023-02-29").is_err()); // Non-leap year Feb 29
108    }
109
110    #[test]
111    fn test_is_valid_uuid_valid() {
112        // Test valid UUIDs
113        assert!(is_valid_uuid("550e8400-e29b-41d4-a716-446655440000"));
114        assert!(is_valid_uuid("6ba7b810-9dad-11d1-80b4-00c04fd430c8"));
115        assert!(is_valid_uuid("00000000-0000-0000-0000-000000000000"));
116        assert!(is_valid_uuid("ffffffff-ffff-ffff-ffff-ffffffffffff"));
117        assert!(is_valid_uuid("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF")); // Uppercase
118    }
119
120    #[test]
121    fn test_is_valid_uuid_invalid() {
122        // Test invalid UUIDs
123        assert!(!is_valid_uuid(""));
124        assert!(!is_valid_uuid("not-a-uuid"));
125        assert!(!is_valid_uuid("550e8400-e29b-41d4-a716")); // Too short
126        assert!(!is_valid_uuid("550e8400-e29b-41d4-a716-44665544000g")); // Invalid char
127        assert!(!is_valid_uuid("550e8400-e29b-41d4-a716-446655440000-extra")); // Extra content
128    }
129
130    #[test]
131    fn test_truncate_string() {
132        // Test string shorter than max length
133        assert_eq!(truncate_string("hello", 10), "hello");
134        assert_eq!(truncate_string("hello", 5), "hello");
135
136        // Test string longer than max length
137        assert_eq!(truncate_string("hello world", 8), "hello...");
138        assert_eq!(truncate_string("hello world", 5), "he...");
139
140        // Test edge cases
141        assert_eq!(truncate_string("hello", 3), "...");
142        assert_eq!(truncate_string("hello", 4), "h...");
143        assert_eq!(truncate_string("", 10), "");
144        assert_eq!(truncate_string("", 0), "");
145        assert_eq!(truncate_string("test", 0), "...");
146    }
147
148    #[test]
149    fn test_integration() {
150        // Test integration between functions
151        let date_str = "2023-12-25";
152        let parsed_date = parse_date(date_str).unwrap();
153        let formatted_date = format_date(&parsed_date);
154        assert_eq!(formatted_date, date_str);
155
156        // Test UUID validation with truncation
157        let uuid = "550e8400-e29b-41d4-a716-446655440000";
158        assert!(is_valid_uuid(uuid));
159        let truncated = truncate_string(uuid, 20);
160        assert_eq!(truncated, "550e8400-e29b-41d...");
161    }
162
163    #[test]
164    fn test_comprehensive_coverage() {
165        // Additional tests to ensure comprehensive coverage
166
167        // Test all months for format_date
168        for month in 1..=12 {
169            let date = NaiveDate::from_ymd_opt(2023, month, 1).unwrap();
170            let formatted = format_date(&date);
171            assert!(formatted.contains(&format!("{month:02}")));
172        }
173
174        // Test various datetime formats
175        let times = [(0, 0, 0), (12, 0, 0), (23, 59, 59)];
176        for (hour, min, sec) in times {
177            let dt = Utc.with_ymd_and_hms(2023, 6, 15, hour, min, sec).unwrap();
178            let formatted = format_datetime(&dt);
179            assert!(formatted.contains(&format!("{hour:02}:{min:02}:{sec:02}")));
180            assert!(formatted.ends_with("UTC"));
181        }
182
183        // Test more invalid date formats
184        let invalid_dates = [
185            "2023",
186            "2023-01",
187            "01-01-2023",
188            "2023.01.01",
189            "2023-00-01",
190            "2023-01-00",
191            "2023-04-31",
192        ];
193        for date_str in &invalid_dates {
194            assert!(parse_date(date_str).is_err());
195        }
196
197        // Test more invalid UUIDs
198        let invalid_uuids = [
199            "550e8400_e29b_41d4_a716_446655440000",  // Underscores
200            "550e8400.e29b.41d4.a716.446655440000",  // Dots
201            " 550e8400-e29b-41d4-a716-446655440000", // Leading space
202            "550e8400-e29b-41d4-a716-446655440000 ", // Trailing space
203        ];
204        for uuid in &invalid_uuids {
205            assert!(!is_valid_uuid(uuid));
206        }
207
208        // Test truncate_string with Unicode
209        assert_eq!(truncate_string("hello δΈ–η•Œ", 8), "hello...");
210        assert_eq!(truncate_string("πŸ¦€πŸ¦€πŸ¦€", 3), "...");
211    }
212}