1use chrono::{NaiveDateTime, Timelike};
10use edtf::level_0::Edtf;
11
12pub fn date_from_parts(year: i32, month: u32, day: u32) -> String {
18 if year == 0 {
19 return String::new();
20 }
21 match (month, day) {
22 (0, _) => format!("{:04}", year),
23 (m, 0) => format!("{:04}-{:02}", year, m),
24 (m, d) => format!("{:04}-{:02}-{:02}", year, m, d),
25 }
26}
27
28pub fn date_from_str_parts(year: &str, month: &str, day: &str) -> String {
31 let y: i32 = year.trim().parse().unwrap_or(0);
32 let m: u32 = month.trim().parse().unwrap_or(0);
33 let d: u32 = day.trim().parse().unwrap_or(0);
34 date_from_parts(y, m, d)
35}
36
37pub fn parse_date(s: &str) -> String {
46 if s.is_empty() {
47 return String::new();
48 }
49
50 if let Ok(edtf) = Edtf::parse(s) {
52 if let Some(date) = edtf.as_date() {
53 return date_from_parts(date.year(), date.month(), date.day());
54 }
55 if let Some(dt) = edtf.as_datetime() {
56 let d = dt.date();
57 return date_from_parts(d.year(), d.month(), d.day());
58 }
59 if let Some((start, _)) = edtf.as_interval() {
60 return date_from_parts(start.year(), start.month(), start.day());
61 }
62 }
63
64 let date_part = {
66 let s = match s.find('T') { Some(p) => &s[..p], None => s };
67 match s.find(' ') { Some(p) => &s[..p], None => s }
68 };
69 let mut parts = date_part.splitn(3, '-');
70 let year: i32 = match parts.next().and_then(|s| s.trim().parse().ok()) {
71 Some(y) if y != 0 => y,
72 _ => return String::new(),
73 };
74 let month: u32 = parts.next().and_then(|s| s.trim().parse().ok()).unwrap_or(0);
75 let day: u32 = parts.next().and_then(|s| s.trim().parse().ok()).unwrap_or(0);
76 date_from_parts(year, month, day)
77}
78
79pub fn parse_datetime(s: &str) -> String {
86 if s.is_empty() {
87 return String::new();
88 }
89
90 let stripped = strip_milliseconds(s);
92 let input = if stripped.is_empty() { s } else { &stripped };
93
94 if let Ok(edtf) = Edtf::parse(input) {
96 if let Some(dt) = edtf.as_datetime() {
97 let t = dt.time();
98 if t.hour() == 0 && t.minute() == 0 && t.second() == 0 {
99 let d = dt.date();
100 return date_from_parts(d.year(), d.month(), d.day());
101 }
102 let d = dt.date();
103 return format!(
104 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
105 d.year(), d.month(), d.day(),
106 t.hour(), t.minute(), t.second()
107 );
108 }
109 if let Some(date) = edtf.as_date() {
110 return date_from_parts(date.year(), date.month(), date.day());
111 }
112 }
113
114 let bare = input.trim_end_matches('Z');
116 for fmt in &["%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%S%.f", "%Y-%m-%d %H:%M:%S", "%Y%m%d%H%M%S"] {
117 if let Ok(dt) = NaiveDateTime::parse_from_str(bare, fmt) {
118 if dt.hour() == 0 && dt.minute() == 0 && dt.second() == 0 {
119 return parse_date(input);
120 }
121 return dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
122 }
123 }
124
125 parse_date(s)
126}
127
128pub fn normalize_date(date: &str) -> String {
133 let (date_part, time_suffix) = match date.find('T') {
134 Some(pos) => (&date[..pos], &date[pos..]),
135 None => (date, ""),
136 };
137 let parts: Vec<&str> = date_part.split('-').collect();
138 let padded = match parts.as_slice() {
139 [y] => y.to_string(),
140 [y, m] => format!("{}-{:0>2}", y, m),
141 [y, m, d] => format!("{}-{:0>2}-{:0>2}", y, m, d),
142 _ => date_part.to_string(),
143 };
144 format!("{}{}", padded, time_suffix)
145}
146
147pub fn sanitize_date(s: &str) -> String {
154 if s.is_empty() {
155 return String::new();
156 }
157 if let Ok(edtf) = Edtf::parse(s) {
160 return match edtf {
161 Edtf::Date(d) => d.to_string(),
162 Edtf::DateTime(_) => s.to_string(),
163 Edtf::Interval(start, _) => start.to_string(),
164 };
165 }
166 let padded = normalize_date(s);
168 if let Ok(edtf) = Edtf::parse(&padded) {
169 return match edtf {
170 Edtf::Date(d) => d.to_string(),
171 Edtf::DateTime(_) => padded,
172 Edtf::Interval(start, _) => start.to_string(),
173 };
174 }
175 let b = s.as_bytes();
177 if s.len() == 10 && b.get(2) == Some(&b'.') && b.get(5) == Some(&b'.') {
178 let r = date_from_str_parts(&s[6..10], &s[3..5], &s[0..2]);
179 if !r.is_empty() {
180 return r;
181 }
182 }
183 String::new()
184}
185
186pub fn unix_to_date(ts: i64) -> String {
190 if ts == 0 {
191 return String::new();
192 }
193 chrono::DateTime::from_timestamp(ts, 0)
194 .map(|dt: chrono::DateTime<chrono::Utc>| dt.format("%Y-%m-%d").to_string())
195 .unwrap_or_default()
196}
197
198pub fn unix_to_datetime(ts: i64) -> String {
200 if ts == 0 {
201 return String::new();
202 }
203 chrono::DateTime::from_timestamp(ts, 0)
204 .map(|dt: chrono::DateTime<chrono::Utc>| dt.format("%Y-%m-%dT%H:%M:%SZ").to_string())
205 .unwrap_or_default()
206}
207
208pub fn strip_milliseconds(s: &str) -> String {
217 if s.is_empty() {
218 return String::new();
219 }
220 if s.contains("T00:00:00") {
221 return s.split('T').next().unwrap_or(s).to_string();
222 }
223 if let Some(dot) = s.find('.') {
224 let rest = &s[dot + 1..];
225 let frac_end = rest
226 .find(|c: char| !c.is_ascii_digit())
227 .map(|i| i + dot + 1)
228 .unwrap_or(s.len());
229 let suffix = &s[frac_end..];
230 let suffix = if suffix.is_empty() { "Z" } else { suffix };
231 return format!("{}{}", &s[..dot], suffix);
232 }
233 if let Some(plus) = s.rfind("+00:00") {
234 return format!("{}Z", &s[..plus]);
235 }
236 s.to_string()
237}
238
239pub fn date_only(s: &str) -> String {
242 if s.len() >= 10 {
243 s[..10].to_string()
244 } else {
245 s.to_string()
246 }
247}
248
249#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn test_date_from_parts() {
257 assert_eq!(date_from_parts(2023, 8, 5), "2023-08-05");
258 assert_eq!(date_from_parts(2023, 8, 0), "2023-08");
259 assert_eq!(date_from_parts(2023, 0, 0), "2023");
260 assert_eq!(date_from_parts(0, 0, 0), "");
261 }
262
263 #[test]
264 fn test_date_from_str_parts() {
265 assert_eq!(date_from_str_parts("2023", "8", "5"), "2023-08-05");
266 assert_eq!(date_from_str_parts("2023", "8", ""), "2023-08");
267 assert_eq!(date_from_str_parts("2023", "", ""), "2023");
268 }
269
270 #[test]
271 fn test_parse_date() {
272 assert_eq!(parse_date("2023-08-25"), "2023-08-25");
274 assert_eq!(parse_date("2023-08-25T00:00:00Z"), "2023-08-25");
275 assert_eq!(parse_date("2023-08-25T12:30:00+05:00"), "2023-08-25");
276 assert_eq!(parse_date("2023-08"), "2023-08");
277 assert_eq!(parse_date("2023"), "2023");
278 assert_eq!(parse_date("2019-04-04/2021-06-06"), "2019-04-04");
280 assert_eq!(parse_date("2023-8-25"), "2023-08-25");
282 assert_eq!(parse_date("2023-08-25T00:00:00"), "2023-08-25");
283 assert_eq!(parse_date(""), "");
284 }
285
286 #[test]
287 fn test_sanitize_date() {
288 assert_eq!(sanitize_date("2023-08-25"), "2023-08-25");
289 assert_eq!(sanitize_date("2023-8-25"), "2023-08-25");
290 assert_eq!(sanitize_date("1996-04-04T00:00:00"), "1996-04-04T00:00:00");
291 assert_eq!(sanitize_date("11.03.2016"), "2016-03-11");
292 assert_eq!(sanitize_date("2020/2020"), "2020");
293 assert_eq!(sanitize_date("2023-01-01/2023-12-31"), "2023-01-01");
294 assert_eq!(sanitize_date(""), "");
295 assert_eq!(sanitize_date("not-a-date"), "");
296 }
297
298 #[test]
299 fn test_normalize_date() {
300 assert_eq!(normalize_date("2023-8-25"), "2023-08-25");
301 assert_eq!(normalize_date("2023-8-5T12:00:00"), "2023-08-05T12:00:00");
302 assert_eq!(normalize_date("2023-08-25"), "2023-08-25");
303 assert_eq!(normalize_date("2023"), "2023");
304 }
305
306 #[test]
307 fn test_parse_datetime() {
308 assert_eq!(parse_datetime("2023-08-25T12:30:00Z"), "2023-08-25T12:30:00Z");
309 assert_eq!(parse_datetime("2023-08-25T00:00:00Z"), "2023-08-25");
310 assert_eq!(parse_datetime("2023-08-25T00:00:00"), "2023-08-25");
311 assert_eq!(parse_datetime("2024-01-15T12:34:56.789012+00:00"), "2024-01-15T12:34:56Z");
313 }
314
315 #[test]
316 fn test_strip_milliseconds() {
317 assert_eq!(strip_milliseconds("2024-01-15T12:34:56.789Z"), "2024-01-15T12:34:56Z");
318 assert_eq!(strip_milliseconds("2024-01-15T00:00:00"), "2024-01-15");
319 assert_eq!(strip_milliseconds("2024-01-15T00:00:00.000Z"), "2024-01-15");
320 assert_eq!(strip_milliseconds("2024-01-15T12:34:56+00:00"), "2024-01-15T12:34:56Z");
321 assert_eq!(strip_milliseconds(""), "");
322 }
323
324 #[test]
325 fn test_unix_to_date() {
326 assert_eq!(unix_to_date(1711238400), "2024-03-24");
327 assert_eq!(unix_to_date(0), "");
328 }
329
330 #[test]
331 fn test_unix_to_datetime() {
332 assert_eq!(unix_to_datetime(1711238400), "2024-03-24T00:00:00Z");
333 assert_eq!(unix_to_datetime(0), "");
334 }
335}