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 's' => {
108 result.push_str(&secs.to_string());
112 i += 2;
113 }
114 'N' => {
115 result.push_str(&format!("{:09}", nanos));
117 i += 2;
118 }
119 'q' => {
120 let month = tm.tm_mon; let quarter = (month / 3) + 1;
123 result.push_str(&quarter.to_string());
124 i += 2;
125 }
126 'P' => {
127 let ampm = if tm.tm_hour < 12 { "am" } else { "pm" };
129 result.push_str(ampm);
130 i += 2;
131 }
132 'Z' if utc => {
133 result.push_str("UTC");
135 i += 2;
136 }
137 'n' => {
138 result.push('\n');
139 i += 2;
140 }
141 't' => {
142 result.push('\t');
143 i += 2;
144 }
145 _ => {
146 let spec = format!("%{}", chars[i + 1]);
148 let formatted = strftime_single(&tm, &spec);
149 let formatted = if let Some(mod_char) = modifier {
151 apply_format_modifier(&formatted, mod_char)
152 } else {
153 formatted
154 };
155 result.push_str(&formatted);
156 i += 2;
157 }
158 }
159 } else {
160 result.push(chars[i]);
161 i += 1;
162 }
163 }
164
165 result
166}
167
168fn strftime_single(tm: &libc::tm, fmt: &str) -> String {
170 let c_fmt = match std::ffi::CString::new(fmt) {
171 Ok(c) => c,
172 Err(_) => return String::new(),
173 };
174 let mut buf = vec![0u8; 128];
175 let len = unsafe {
176 libc::strftime(
177 buf.as_mut_ptr() as *mut libc::c_char,
178 buf.len(),
179 c_fmt.as_ptr(),
180 tm,
181 )
182 };
183 if len == 0 && !fmt.is_empty() && fmt != "%%" {
184 return String::new();
187 }
188 buf.truncate(len);
189 String::from_utf8_lossy(&buf).into_owned()
190}
191
192fn apply_format_modifier(formatted: &str, modifier: char) -> String {
197 match modifier {
198 '-' => {
199 let trimmed = formatted.trim_start_matches(['0', ' ']);
201 if trimmed.is_empty() {
202 "0".to_string()
203 } else {
204 trimmed.to_string()
205 }
206 }
207 '_' => {
208 let mut result = String::with_capacity(formatted.len());
210 let mut leading = true;
211 for ch in formatted.chars() {
212 if leading && ch == '0' {
213 result.push(' ');
214 } else {
215 leading = false;
216 result.push(ch);
217 }
218 }
219 result
220 }
221 '0' => {
222 let mut result = String::with_capacity(formatted.len());
224 let mut leading = true;
225 for ch in formatted.chars() {
226 if leading && ch == ' ' {
227 result.push('0');
228 } else {
229 leading = false;
230 result.push(ch);
231 }
232 }
233 result
234 }
235 _ => formatted.to_string(),
236 }
237}
238
239pub fn format_iso(time: &SystemTime, precision: &IsoFormat, utc: bool) -> String {
241 match precision {
242 IsoFormat::Date => format_date(time, "%Y-%m-%d", utc),
243 IsoFormat::Hours => {
244 let date_part = format_date(time, "%Y-%m-%dT%H", utc);
245 let tz = format_timezone_colon(time, utc);
246 format!("{}{}", date_part, tz)
247 }
248 IsoFormat::Minutes => {
249 let date_part = format_date(time, "%Y-%m-%dT%H:%M", utc);
250 let tz = format_timezone_colon(time, utc);
251 format!("{}{}", date_part, tz)
252 }
253 IsoFormat::Seconds => {
254 let date_part = format_date(time, "%Y-%m-%dT%H:%M:%S", utc);
255 let tz = format_timezone_colon(time, utc);
256 format!("{}{}", date_part, tz)
257 }
258 IsoFormat::Ns => {
259 let dur = time.duration_since(UNIX_EPOCH).unwrap_or_default();
260 let nanos = dur.subsec_nanos();
261 let date_part = format_date(time, "%Y-%m-%dT%H:%M:%S", utc);
262 let tz = format_timezone_colon(time, utc);
263 format!("{},{:09}{}", date_part, nanos, tz)
264 }
265 }
266}
267
268pub fn format_rfc_email(time: &SystemTime, utc: bool) -> String {
270 format_date(time, "%a, %d %b %Y %H:%M:%S %z", utc)
271}
272
273pub fn format_rfc3339(time: &SystemTime, precision: &Rfc3339Format, utc: bool) -> String {
275 match precision {
276 Rfc3339Format::Date => format_date(time, "%Y-%m-%d", utc),
277 Rfc3339Format::Seconds => {
278 let date_part = format_date(time, "%Y-%m-%d %H:%M:%S", utc);
279 let tz = format_timezone_colon(time, utc);
280 format!("{}{}", date_part, tz)
281 }
282 Rfc3339Format::Ns => {
283 let dur = time.duration_since(UNIX_EPOCH).unwrap_or_default();
284 let nanos = dur.subsec_nanos();
285 let date_part = format_date(time, "%Y-%m-%d %H:%M:%S", utc);
286 let tz = format_timezone_colon(time, utc);
287 format!("{}.{:09}{}", date_part, nanos, tz)
288 }
289 }
290}
291
292fn format_timezone_colon(time: &SystemTime, utc: bool) -> String {
294 if utc {
295 return "+00:00".to_string();
296 }
297 let raw = format_date(time, "%z", false);
298 if raw.len() >= 5 {
300 format!("{}:{}", &raw[..3], &raw[3..5])
301 } else {
302 raw
303 }
304}
305
306pub fn parse_date_string(s: &str, utc: bool) -> Result<SystemTime, String> {
314 let s = s.trim();
315
316 if let Some(epoch_str) = s.strip_prefix('@') {
318 let secs: i64 = epoch_str
319 .trim()
320 .parse()
321 .map_err(|_| format!("invalid date '@{}'", epoch_str))?;
322 if secs >= 0 {
323 return Ok(UNIX_EPOCH + Duration::from_secs(secs as u64));
324 } else {
325 return Ok(UNIX_EPOCH - Duration::from_secs((-secs) as u64));
326 }
327 }
328
329 let now = SystemTime::now();
330
331 match s.to_lowercase().as_str() {
333 "now" | "today" => return Ok(now),
334 "yesterday" => {
335 return Ok(now - Duration::from_secs(86400));
336 }
337 "tomorrow" => {
338 return Ok(now + Duration::from_secs(86400));
339 }
340 _ => {}
341 }
342
343 if let Some(result) = try_parse_relative(s, &now) {
345 return Ok(result);
346 }
347
348 if let Some(result) = try_parse_iso(s, utc) {
350 return Ok(result);
351 }
352
353 Err(format!("invalid date '{}'", s))
354}
355
356fn try_parse_relative(s: &str, now: &SystemTime) -> Option<SystemTime> {
358 let lower = s.to_lowercase();
359 let parts: Vec<&str> = lower.split_whitespace().collect();
360
361 if parts.len() < 2 {
362 return None;
363 }
364
365 let is_ago = parts.last().map_or(false, |&p| p == "ago");
366 let num_str = parts[0].trim_start_matches('+');
367 let amount: i64 = num_str.parse().ok()?;
368
369 let unit_idx = 1;
370 if unit_idx >= parts.len() {
371 return None;
372 }
373 let unit = parts[unit_idx];
374
375 let seconds = match unit.trim_end_matches('s') {
376 "second" => amount,
377 "minute" => amount * 60,
378 "hour" => amount * 3600,
379 "day" => amount * 86400,
380 "week" => amount * 86400 * 7,
381 "month" => amount * 86400 * 30,
382 "year" => amount * 86400 * 365,
383 _ => return None,
384 };
385
386 let duration = Duration::from_secs(seconds.unsigned_abs());
387 if is_ago || seconds < 0 {
388 Some(*now - duration)
389 } else {
390 Some(*now + duration)
391 }
392}
393
394fn try_parse_iso(s: &str, utc: bool) -> Option<SystemTime> {
396 let s = s.replace('T', " ");
398 let parts: Vec<&str> = s.splitn(2, ' ').collect();
399 let date_part = parts[0];
400 let time_part = if parts.len() > 1 {
401 parts[1]
402 } else {
403 "00:00:00"
404 };
405
406 let date_fields: Vec<&str> = date_part.split('-').collect();
407 if date_fields.len() != 3 {
408 return None;
409 }
410
411 let year: i32 = date_fields[0].parse().ok()?;
412 let month: u32 = date_fields[1].parse().ok()?;
413 let day: u32 = date_fields[2].parse().ok()?;
414
415 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
416 return None;
417 }
418
419 let time_clean = time_part
421 .split('+')
422 .next()
423 .unwrap_or(time_part)
424 .split('Z')
425 .next()
426 .unwrap_or(time_part);
427 let time_fields: Vec<&str> = time_clean.split(':').collect();
428 let hour: u32 = time_fields
429 .first()
430 .and_then(|s| s.parse().ok())
431 .unwrap_or(0);
432 let minute: u32 = time_fields.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
433 let second: u32 = time_fields
434 .get(2)
435 .and_then(|s| s.split('.').next())
436 .and_then(|s| s.parse().ok())
437 .unwrap_or(0);
438
439 if hour > 23 || minute > 59 || second > 60 {
440 return None;
441 }
442
443 let mut tm: libc::tm = unsafe { std::mem::zeroed() };
445 tm.tm_year = year - 1900;
446 tm.tm_mon = month as i32 - 1;
447 tm.tm_mday = day as i32;
448 tm.tm_hour = hour as i32;
449 tm.tm_min = minute as i32;
450 tm.tm_sec = second as i32;
451 tm.tm_isdst = -1; let epoch_secs = if utc {
454 unsafe { libc::timegm(&mut tm) }
455 } else {
456 unsafe { libc::mktime(&mut tm) }
457 };
458 if epoch_secs == -1 {
459 return None;
460 }
461
462 if epoch_secs >= 0 {
463 Some(UNIX_EPOCH + Duration::from_secs(epoch_secs as u64))
464 } else {
465 Some(UNIX_EPOCH - Duration::from_secs((-epoch_secs) as u64))
466 }
467}
468
469pub fn file_mod_time(path: &str) -> Result<SystemTime, String> {
471 std::fs::metadata(path)
472 .map_err(|e| format!("{}: {}", path, e))?
473 .modified()
474 .map_err(|e| format!("{}: {}", path, e))
475}
476
477pub fn default_format() -> &'static str {
480 "%a %b %e %r %Z %Y"
481}