1use std::time::{Duration, SystemTime, UNIX_EPOCH};
2
3#[derive(Default)]
5pub struct DateConfig {
6 pub date_string: Option<String>,
8 pub date_file: Option<String>,
10 pub iso_format: Option<IsoFormat>,
12 pub rfc_email: bool,
14 pub rfc_3339: Option<Rfc3339Format>,
16 pub reference_file: Option<String>,
18 pub set_string: Option<String>,
20 pub utc: bool,
22 pub format: Option<String>,
24}
25
26#[derive(Clone, Debug, PartialEq)]
28pub enum IsoFormat {
29 Date,
30 Hours,
31 Minutes,
32 Seconds,
33 Ns,
34}
35
36#[derive(Clone, Debug, PartialEq)]
38pub enum Rfc3339Format {
39 Date,
40 Seconds,
41 Ns,
42}
43
44pub fn parse_iso_format(s: &str) -> Result<IsoFormat, String> {
46 match s {
47 "" | "date" => Ok(IsoFormat::Date),
48 "hours" => Ok(IsoFormat::Hours),
49 "minutes" => Ok(IsoFormat::Minutes),
50 "seconds" => Ok(IsoFormat::Seconds),
51 "ns" => Ok(IsoFormat::Ns),
52 _ => Err(format!("invalid ISO 8601 format: '{}'", s)),
53 }
54}
55
56pub fn parse_rfc3339_format(s: &str) -> Result<Rfc3339Format, String> {
58 match s {
59 "date" => Ok(Rfc3339Format::Date),
60 "seconds" => Ok(Rfc3339Format::Seconds),
61 "ns" => Ok(Rfc3339Format::Ns),
62 _ => Err(format!("invalid RFC 3339 format: '{}'", s)),
63 }
64}
65
66pub fn format_date(time: &SystemTime, format: &str, utc: bool) -> String {
71 let dur = time.duration_since(UNIX_EPOCH).unwrap_or_default();
72 let secs = dur.as_secs() as i64;
73 let nanos = dur.subsec_nanos();
74
75 let mut tm: libc::tm = unsafe { std::mem::zeroed() };
76 if utc {
77 unsafe {
78 libc::gmtime_r(&secs, &mut tm);
79 }
80 } else {
81 unsafe {
82 libc::localtime_r(&secs, &mut tm);
83 }
84 }
85
86 let mut result = String::with_capacity(format.len() * 2);
89 let chars: Vec<char> = format.chars().collect();
90 let mut i = 0;
91
92 while i < chars.len() {
93 if chars[i] == '%' && i + 1 < chars.len() {
94 let modifier = if i + 2 < chars.len()
96 && (chars[i + 1] == '-' || chars[i + 1] == '_' || chars[i + 1] == '0')
97 && chars[i + 2].is_ascii_alphabetic()
98 {
99 let m = chars[i + 1];
100 i += 1; Some(m)
102 } else {
103 None
104 };
105
106 match chars[i + 1] {
107 'N' => {
108 result.push_str(&format!("{:09}", nanos));
110 i += 2;
111 }
112 'q' => {
113 let month = tm.tm_mon; let quarter = (month / 3) + 1;
116 result.push_str(&quarter.to_string());
117 i += 2;
118 }
119 'P' => {
120 let ampm = if tm.tm_hour < 12 { "am" } else { "pm" };
122 result.push_str(ampm);
123 i += 2;
124 }
125 'Z' if utc => {
126 result.push_str("UTC");
128 i += 2;
129 }
130 'n' => {
131 result.push('\n');
132 i += 2;
133 }
134 't' => {
135 result.push('\t');
136 i += 2;
137 }
138 _ => {
139 let spec = format!("%{}", chars[i + 1]);
141 let formatted = strftime_single(&tm, &spec);
142 let formatted = if let Some(mod_char) = modifier {
144 apply_format_modifier(&formatted, mod_char)
145 } else {
146 formatted
147 };
148 result.push_str(&formatted);
149 i += 2;
150 }
151 }
152 } else {
153 result.push(chars[i]);
154 i += 1;
155 }
156 }
157
158 result
159}
160
161fn strftime_single(tm: &libc::tm, fmt: &str) -> String {
163 let c_fmt = match std::ffi::CString::new(fmt) {
164 Ok(c) => c,
165 Err(_) => return String::new(),
166 };
167 let mut buf = vec![0u8; 128];
168 let len = unsafe {
169 libc::strftime(
170 buf.as_mut_ptr() as *mut libc::c_char,
171 buf.len(),
172 c_fmt.as_ptr(),
173 tm,
174 )
175 };
176 if len == 0 && !fmt.is_empty() && fmt != "%%" {
177 return String::new();
180 }
181 buf.truncate(len);
182 String::from_utf8_lossy(&buf).into_owned()
183}
184
185fn apply_format_modifier(formatted: &str, modifier: char) -> String {
190 match modifier {
191 '-' => {
192 let trimmed = formatted.trim_start_matches(['0', ' ']);
194 if trimmed.is_empty() {
195 "0".to_string()
196 } else {
197 trimmed.to_string()
198 }
199 }
200 '_' => {
201 let mut result = String::with_capacity(formatted.len());
203 let mut leading = true;
204 for ch in formatted.chars() {
205 if leading && ch == '0' {
206 result.push(' ');
207 } else {
208 leading = false;
209 result.push(ch);
210 }
211 }
212 result
213 }
214 '0' => {
215 let mut result = String::with_capacity(formatted.len());
217 let mut leading = true;
218 for ch in formatted.chars() {
219 if leading && ch == ' ' {
220 result.push('0');
221 } else {
222 leading = false;
223 result.push(ch);
224 }
225 }
226 result
227 }
228 _ => formatted.to_string(),
229 }
230}
231
232pub fn format_iso(time: &SystemTime, precision: &IsoFormat, utc: bool) -> String {
234 match precision {
235 IsoFormat::Date => format_date(time, "%Y-%m-%d", utc),
236 IsoFormat::Hours => {
237 let date_part = format_date(time, "%Y-%m-%dT%H", utc);
238 let tz = format_timezone_colon(time, utc);
239 format!("{}{}", date_part, tz)
240 }
241 IsoFormat::Minutes => {
242 let date_part = format_date(time, "%Y-%m-%dT%H:%M", utc);
243 let tz = format_timezone_colon(time, utc);
244 format!("{}{}", date_part, tz)
245 }
246 IsoFormat::Seconds => {
247 let date_part = format_date(time, "%Y-%m-%dT%H:%M:%S", utc);
248 let tz = format_timezone_colon(time, utc);
249 format!("{}{}", date_part, tz)
250 }
251 IsoFormat::Ns => {
252 let dur = time.duration_since(UNIX_EPOCH).unwrap_or_default();
253 let nanos = dur.subsec_nanos();
254 let date_part = format_date(time, "%Y-%m-%dT%H:%M:%S", utc);
255 let tz = format_timezone_colon(time, utc);
256 format!("{},{:09}{}", date_part, nanos, tz)
257 }
258 }
259}
260
261pub fn format_rfc_email(time: &SystemTime, utc: bool) -> String {
263 format_date(time, "%a, %d %b %Y %H:%M:%S %z", utc)
264}
265
266pub fn format_rfc3339(time: &SystemTime, precision: &Rfc3339Format, utc: bool) -> String {
268 match precision {
269 Rfc3339Format::Date => format_date(time, "%Y-%m-%d", utc),
270 Rfc3339Format::Seconds => {
271 let date_part = format_date(time, "%Y-%m-%d %H:%M:%S", utc);
272 let tz = format_timezone_colon(time, utc);
273 format!("{}{}", date_part, tz)
274 }
275 Rfc3339Format::Ns => {
276 let dur = time.duration_since(UNIX_EPOCH).unwrap_or_default();
277 let nanos = dur.subsec_nanos();
278 let date_part = format_date(time, "%Y-%m-%d %H:%M:%S", utc);
279 let tz = format_timezone_colon(time, utc);
280 format!("{}.{:09}{}", date_part, nanos, tz)
281 }
282 }
283}
284
285fn format_timezone_colon(time: &SystemTime, utc: bool) -> String {
287 if utc {
288 return "+00:00".to_string();
289 }
290 let raw = format_date(time, "%z", false);
291 if raw.len() >= 5 {
293 format!("{}:{}", &raw[..3], &raw[3..5])
294 } else {
295 raw
296 }
297}
298
299pub fn parse_date_string(s: &str) -> Result<SystemTime, String> {
307 let s = s.trim();
308
309 if let Some(epoch_str) = s.strip_prefix('@') {
311 let secs: i64 = epoch_str
312 .trim()
313 .parse()
314 .map_err(|_| format!("invalid date '@{}'", epoch_str))?;
315 if secs >= 0 {
316 return Ok(UNIX_EPOCH + Duration::from_secs(secs as u64));
317 } else {
318 return Ok(UNIX_EPOCH - Duration::from_secs((-secs) as u64));
319 }
320 }
321
322 let now = SystemTime::now();
323
324 match s.to_lowercase().as_str() {
326 "now" | "today" => return Ok(now),
327 "yesterday" => {
328 return Ok(now - Duration::from_secs(86400));
329 }
330 "tomorrow" => {
331 return Ok(now + Duration::from_secs(86400));
332 }
333 _ => {}
334 }
335
336 if let Some(result) = try_parse_relative(s, &now) {
338 return Ok(result);
339 }
340
341 if let Some(result) = try_parse_iso(s) {
343 return Ok(result);
344 }
345
346 Err(format!("invalid date '{}'", s))
347}
348
349fn try_parse_relative(s: &str, now: &SystemTime) -> Option<SystemTime> {
351 let lower = s.to_lowercase();
352 let parts: Vec<&str> = lower.split_whitespace().collect();
353
354 if parts.len() < 2 {
355 return None;
356 }
357
358 let is_ago = parts.last().map_or(false, |&p| p == "ago");
359 let num_str = parts[0].trim_start_matches('+');
360 let amount: i64 = num_str.parse().ok()?;
361
362 let unit_idx = 1;
363 if unit_idx >= parts.len() {
364 return None;
365 }
366 let unit = parts[unit_idx];
367
368 let seconds = match unit.trim_end_matches('s') {
369 "second" => amount,
370 "minute" => amount * 60,
371 "hour" => amount * 3600,
372 "day" => amount * 86400,
373 "week" => amount * 86400 * 7,
374 "month" => amount * 86400 * 30,
375 "year" => amount * 86400 * 365,
376 _ => return None,
377 };
378
379 let duration = Duration::from_secs(seconds.unsigned_abs());
380 if is_ago || seconds < 0 {
381 Some(*now - duration)
382 } else {
383 Some(*now + duration)
384 }
385}
386
387fn try_parse_iso(s: &str) -> Option<SystemTime> {
389 let s = s.replace('T', " ");
391 let parts: Vec<&str> = s.splitn(2, ' ').collect();
392 let date_part = parts[0];
393 let time_part = if parts.len() > 1 {
394 parts[1]
395 } else {
396 "00:00:00"
397 };
398
399 let date_fields: Vec<&str> = date_part.split('-').collect();
400 if date_fields.len() != 3 {
401 return None;
402 }
403
404 let year: i32 = date_fields[0].parse().ok()?;
405 let month: u32 = date_fields[1].parse().ok()?;
406 let day: u32 = date_fields[2].parse().ok()?;
407
408 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
409 return None;
410 }
411
412 let time_clean = time_part
414 .split('+')
415 .next()
416 .unwrap_or(time_part)
417 .split('Z')
418 .next()
419 .unwrap_or(time_part);
420 let time_fields: Vec<&str> = time_clean.split(':').collect();
421 let hour: u32 = time_fields
422 .first()
423 .and_then(|s| s.parse().ok())
424 .unwrap_or(0);
425 let minute: u32 = time_fields.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
426 let second: u32 = time_fields
427 .get(2)
428 .and_then(|s| s.split('.').next())
429 .and_then(|s| s.parse().ok())
430 .unwrap_or(0);
431
432 if hour > 23 || minute > 59 || second > 60 {
433 return None;
434 }
435
436 let mut tm: libc::tm = unsafe { std::mem::zeroed() };
438 tm.tm_year = year - 1900;
439 tm.tm_mon = month as i32 - 1;
440 tm.tm_mday = day as i32;
441 tm.tm_hour = hour as i32;
442 tm.tm_min = minute as i32;
443 tm.tm_sec = second as i32;
444 tm.tm_isdst = -1; let epoch_secs = unsafe { libc::mktime(&mut tm) };
447 if epoch_secs == -1 {
448 return None;
449 }
450
451 if epoch_secs >= 0 {
452 Some(UNIX_EPOCH + Duration::from_secs(epoch_secs as u64))
453 } else {
454 Some(UNIX_EPOCH - Duration::from_secs((-epoch_secs) as u64))
455 }
456}
457
458pub fn file_mod_time(path: &str) -> Result<SystemTime, String> {
460 std::fs::metadata(path)
461 .map_err(|e| format!("{}: {}", path, e))?
462 .modified()
463 .map_err(|e| format!("{}: {}", path, e))
464}
465
466pub fn default_format() -> &'static str {
468 "%a %b %e %H:%M:%S %Z %Y"
469}