Skip to main content

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