Skip to main content

time_format/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::{
4    convert::TryInto,
5    ffi::CString,
6    fmt,
7    mem::MaybeUninit,
8    os::raw::{c_char, c_int},
9};
10
11#[cfg(not(target_env = "msvc"))]
12use std::os::raw::c_long;
13
14#[allow(non_camel_case_types)]
15type time_t = i64;
16
17/// A UNIX timestamp in seconds.
18pub type TimeStamp = i64;
19
20/// A UNIX timestamp with millisecond precision.
21#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
22pub struct TimeStampMs {
23    /// Seconds since the UNIX epoch.
24    pub seconds: i64,
25    /// Milliseconds component (0-999).
26    pub milliseconds: u16,
27}
28
29impl TimeStampMs {
30    /// Create a new TimeStampMs from seconds and milliseconds.
31    pub fn new(seconds: i64, milliseconds: u16) -> Self {
32        let milliseconds = milliseconds % 1000;
33        Self {
34            seconds,
35            milliseconds,
36        }
37    }
38
39    /// Convert from a TimeStamp (seconds only).
40    pub fn from_timestamp(ts: TimeStamp) -> Self {
41        Self {
42            seconds: ts,
43            milliseconds: 0,
44        }
45    }
46
47    /// Get the total milliseconds since the UNIX epoch.
48    pub fn total_milliseconds(&self) -> i64 {
49        self.seconds * 1000 + self.milliseconds as i64
50    }
51}
52
53// Unix/Linux/macOS tm struct with timezone fields
54#[cfg(not(target_env = "msvc"))]
55#[repr(C)]
56#[derive(Debug, Copy, Clone)]
57struct tm {
58    pub tm_sec: c_int,
59    pub tm_min: c_int,
60    pub tm_hour: c_int,
61    pub tm_mday: c_int,
62    pub tm_mon: c_int,
63    pub tm_year: c_int,
64    pub tm_wday: c_int,
65    pub tm_yday: c_int,
66    pub tm_isdst: c_int,
67    pub tm_gmtoff: c_long,
68    pub tm_zone: *mut c_char,
69}
70
71// Windows MSVC tm struct without timezone fields
72#[cfg(target_env = "msvc")]
73#[repr(C)]
74#[derive(Debug, Copy, Clone)]
75struct tm {
76    pub tm_sec: c_int,
77    pub tm_min: c_int,
78    pub tm_hour: c_int,
79    pub tm_mday: c_int,
80    pub tm_mon: c_int,
81    pub tm_year: c_int,
82    pub tm_wday: c_int,
83    pub tm_yday: c_int,
84    pub tm_isdst: c_int,
85}
86
87// Unix/Linux/macOS - use _r variants
88#[cfg(not(target_env = "msvc"))]
89extern "C" {
90    fn gmtime_r(ts: *const time_t, tm: *mut tm) -> *mut tm;
91    fn localtime_r(ts: *const time_t, tm: *mut tm) -> *mut tm;
92    fn strftime(s: *mut c_char, maxsize: usize, format: *const c_char, timeptr: *const tm)
93        -> usize;
94}
95
96// Windows MSVC - use _s variants with explicit 64-bit time (note: reversed parameter order)
97#[cfg(target_env = "msvc")]
98extern "C" {
99    fn _gmtime64_s(tm: *mut tm, ts: *const time_t) -> c_int;
100    fn _localtime64_s(tm: *mut tm, ts: *const time_t) -> c_int;
101    fn strftime(s: *mut c_char, maxsize: usize, format: *const c_char, timeptr: *const tm)
102        -> usize;
103}
104
105// Platform-specific wrappers for gmtime
106#[cfg(not(target_env = "msvc"))]
107unsafe fn safe_gmtime(ts: *const time_t, tm: *mut tm) -> bool {
108    !gmtime_r(ts, tm).is_null()
109}
110
111#[cfg(target_env = "msvc")]
112unsafe fn safe_gmtime(ts: *const time_t, tm: *mut tm) -> bool {
113    _gmtime64_s(tm, ts) == 0
114}
115
116// Platform-specific wrappers for localtime
117#[cfg(not(target_env = "msvc"))]
118unsafe fn safe_localtime(ts: *const time_t, tm: *mut tm) -> bool {
119    !localtime_r(ts, tm).is_null()
120}
121
122#[cfg(target_env = "msvc")]
123unsafe fn safe_localtime(ts: *const time_t, tm: *mut tm) -> bool {
124    _localtime64_s(tm, ts) == 0
125}
126
127#[derive(Debug, Clone, Copy, Eq, PartialEq)]
128pub enum Error {
129    /// Error occurred while parsing or converting time
130    TimeError,
131    /// Error occurred with timestamp value (e.g., timestamp out of range)
132    InvalidTimestamp,
133    /// Error occurred while formatting time
134    FormatError,
135    /// Error with format string (e.g., invalid format specifier)
136    InvalidFormatString,
137    /// Error with UTF-8 conversion from C string
138    Utf8Error,
139    /// Error with null bytes in input strings
140    NullByteError,
141}
142
143impl fmt::Display for Error {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        match self {
146            Error::TimeError => write!(f, "Time processing error"),
147            Error::InvalidTimestamp => write!(f, "Invalid timestamp value"),
148            Error::FormatError => write!(f, "Time formatting error"),
149            Error::InvalidFormatString => write!(f, "Invalid format string"),
150            Error::Utf8Error => write!(f, "UTF-8 conversion error"),
151            Error::NullByteError => write!(f, "String contains null bytes"),
152        }
153    }
154}
155
156impl std::error::Error for Error {}
157
158/// Validates a strftime format string for correct syntax.
159/// This performs a basic validation to catch common errors.
160///
161/// Returns Ok(()) if the format appears valid, or an error describing the issue.
162pub fn validate_format(format: impl AsRef<str>) -> Result<(), Error> {
163    let format = format.as_ref();
164
165    // Check for empty format
166    if format.is_empty() {
167        return Err(Error::InvalidFormatString);
168    }
169
170    // Check for null bytes (which would cause CString creation to fail)
171    if format.contains('\0') {
172        return Err(Error::NullByteError);
173    }
174
175    let mut chars = format.chars();
176    while let Some(c) = chars.next() {
177        // Look for % sequences
178        if c == '%' {
179            match chars.next() {
180                // These are the most common format specifiers
181                Some('a') | Some('A') | Some('b') | Some('B') | Some('c') | Some('C')
182                | Some('d') | Some('D') | Some('e') | Some('F') | Some('g') | Some('G')
183                | Some('h') | Some('H') | Some('I') | Some('j') | Some('k') | Some('l')
184                | Some('m') | Some('M') | Some('n') | Some('p') | Some('P') | Some('r')
185                | Some('R') | Some('s') | Some('S') | Some('t') | Some('T') | Some('u')
186                | Some('U') | Some('V') | Some('w') | Some('W') | Some('x') | Some('X')
187                | Some('y') | Some('Y') | Some('z') | Some('Z') | Some('%') | Some('E')
188                | Some('O') | Some('+') => {}
189                Some(_c) => {
190                    // Unknown format specifier
191                    return Err(Error::InvalidFormatString);
192                }
193                None => {
194                    // % at end of string
195                    return Err(Error::InvalidFormatString);
196                }
197            }
198        }
199    }
200
201    // Check for the special {ms} sequence format
202    let ms_braces = format.matches('{').count();
203    let ms_closing_braces = format.matches('}').count();
204    if ms_braces != ms_closing_braces {
205        return Err(Error::InvalidFormatString);
206    }
207
208    Ok(())
209}
210
211/// Time components.
212#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
213pub struct Components {
214    /// Second.
215    pub sec: u8,
216    /// Minute.
217    pub min: u8,
218    /// Hour.
219    pub hour: u8,
220    /// Day of month.
221    pub month_day: u8,
222    /// Month - January is 1, December is 12.
223    pub month: u8,
224    /// Year.
225    pub year: i16,
226    /// Day of week.
227    pub week_day: u8,
228    /// Day of year.    
229    pub year_day: u16,
230}
231
232/// Split a timestamp into its components in UTC timezone.
233pub fn components_utc(ts_seconds: TimeStamp) -> Result<Components, Error> {
234    let mut tm = MaybeUninit::<tm>::uninit();
235    if !unsafe { safe_gmtime(&ts_seconds, tm.as_mut_ptr()) } {
236        return Err(Error::TimeError);
237    }
238    let tm = unsafe { tm.assume_init() };
239    Ok(Components {
240        sec: tm.tm_sec as _,
241        min: tm.tm_min as _,
242        hour: tm.tm_hour as _,
243        month_day: tm.tm_mday as _,
244        month: (1 + tm.tm_mon) as _,
245        year: (1900 + tm.tm_year) as _,
246        week_day: tm.tm_wday as _,
247        year_day: tm.tm_yday as _,
248    })
249}
250
251/// Split a timestamp into its components in the local timezone.
252pub fn components_local(ts_seconds: TimeStamp) -> Result<Components, Error> {
253    let mut tm = MaybeUninit::<tm>::uninit();
254    if !unsafe { safe_localtime(&ts_seconds, tm.as_mut_ptr()) } {
255        return Err(Error::TimeError);
256    }
257    let tm = unsafe { tm.assume_init() };
258    Ok(Components {
259        sec: tm.tm_sec as _,
260        min: tm.tm_min as _,
261        hour: tm.tm_hour as _,
262        month_day: tm.tm_mday as _,
263        month: (1 + tm.tm_mon) as _,
264        year: (1900 + tm.tm_year) as _,
265        week_day: tm.tm_wday as _,
266        year_day: tm.tm_yday as _,
267    })
268}
269
270/// Convert a `std::time::SystemTime` to a UNIX timestamp in seconds.
271///
272/// This function converts a `std::time::SystemTime` instance to a `TimeStamp` (Unix timestamp in seconds).
273/// It handles the conversion and error cases related to negative timestamps or other time conversion issues.
274///
275/// # Examples
276///
277/// ```rust
278/// use std::time::{SystemTime, UNIX_EPOCH, Duration};
279///
280/// // Convert the current system time to a timestamp
281/// let system_time = SystemTime::now();
282/// let timestamp = time_format::from_system_time(system_time).unwrap();
283///
284/// // Convert a specific time
285/// let past_time = UNIX_EPOCH + Duration::from_secs(1500000000);
286/// let past_timestamp = time_format::from_system_time(past_time).unwrap();
287/// assert_eq!(past_timestamp, 1500000000);
288/// ```
289///
290/// ## Working with Time Components
291///
292/// You can use the function to convert a `SystemTime` to components:
293///
294/// ```rust
295/// use std::time::{SystemTime, UNIX_EPOCH, Duration};
296///
297/// // Create a specific time: January 15, 2023 at 14:30:45 UTC
298/// let specific_time = UNIX_EPOCH + Duration::from_secs(1673793045);
299///
300/// // Convert to timestamp
301/// let ts = time_format::from_system_time(specific_time).unwrap();
302///
303/// // Get the time components
304/// let components = time_format::components_utc(ts).unwrap();
305///
306/// // Verify the time components
307/// assert_eq!(components.year, 2023);
308/// assert_eq!(components.month, 1); // January
309/// assert_eq!(components.month_day, 15);
310/// assert_eq!(components.hour, 14);
311/// assert_eq!(components.min, 30);
312/// assert_eq!(components.sec, 45);
313/// ```
314///
315/// ## Formatting with strftime
316///
317/// Convert a `SystemTime` and format it as a string:
318///
319/// ```rust
320/// use std::time::{SystemTime, UNIX_EPOCH, Duration};
321///
322/// // Create a specific time
323/// let specific_time = UNIX_EPOCH + Duration::from_secs(1673793045);
324///
325/// // Convert to timestamp
326/// let ts = time_format::from_system_time(specific_time).unwrap();
327///
328/// // Format as ISO 8601
329/// let iso8601 = time_format::format_iso8601_utc(ts).unwrap();
330/// assert_eq!(iso8601, "2023-01-15T14:30:45Z");
331///
332/// // Custom formatting
333/// let custom_format = time_format::strftime_utc("%B %d, %Y at %H:%M:%S", ts).unwrap();
334/// assert_eq!(custom_format, "January 15, 2023 at 14:30:45");
335/// ```
336pub fn from_system_time(time: std::time::SystemTime) -> Result<TimeStamp, Error> {
337    time.duration_since(std::time::UNIX_EPOCH)
338        .map_err(|_| Error::TimeError)?
339        .as_secs()
340        .try_into()
341        .map_err(|_| Error::InvalidTimestamp)
342}
343
344/// Return the current UNIX timestamp in seconds.
345pub fn now() -> Result<TimeStamp, Error> {
346    from_system_time(std::time::SystemTime::now())
347}
348
349/// Convert a `std::time::SystemTime` to a UNIX timestamp with millisecond precision.
350///
351/// This function converts a `std::time::SystemTime` instance to a `TimeStampMs` (Unix timestamp with millisecond precision).
352/// It extracts both the seconds and milliseconds components from the system time.
353///
354/// # Examples
355///
356/// ```rust
357/// use std::time::{SystemTime, UNIX_EPOCH, Duration};
358///
359/// // Convert the current system time to a timestamp with millisecond precision
360/// let system_time = SystemTime::now();
361/// let timestamp_ms = time_format::from_system_time_ms(system_time).unwrap();
362/// println!("Seconds: {}, Milliseconds: {}", timestamp_ms.seconds, timestamp_ms.milliseconds);
363///
364/// // Convert a specific time with millisecond precision
365/// let specific_time = UNIX_EPOCH + Duration::from_millis(1500000123);
366/// let specific_ts_ms = time_format::from_system_time_ms(specific_time).unwrap();
367/// assert_eq!(specific_ts_ms.seconds, 1500000);
368/// assert_eq!(specific_ts_ms.milliseconds, 123);
369/// ```
370///
371/// ## Using with TimeStampMs methods
372///
373/// ```rust
374/// use std::time::{SystemTime, UNIX_EPOCH, Duration};
375///
376/// // Create a precise time: 1500000 seconds and 123 milliseconds after the epoch
377/// let specific_time = UNIX_EPOCH + Duration::from_millis(1500000123);
378///
379/// // Convert to TimeStampMs
380/// let ts_ms = time_format::from_system_time_ms(specific_time).unwrap();
381///
382/// // Get total milliseconds
383/// let total_ms = ts_ms.total_milliseconds();
384/// assert_eq!(total_ms, 1500000123);
385/// ```
386///
387/// ## Formatting timestamps with millisecond precision
388///
389/// You can format a timestamp with millisecond precision:
390///
391/// ```rust
392/// use std::time::{SystemTime, UNIX_EPOCH, Duration};
393///
394/// // Create a specific timestamp with millisecond precision
395/// // We'll use a fixed timestamp rather than a date calculation to avoid test failures
396/// let ts_ms = time_format::TimeStampMs::new(1743087045, 678);
397///
398/// // Format with milliseconds using your preferred pattern
399/// let formatted = time_format::strftime_ms_utc("%Y-%m-%d %H:%M:%S.{ms}", ts_ms).unwrap();
400///
401/// // Verify the milliseconds are included
402/// assert!(formatted.contains(".678"));
403///
404/// // Format as ISO 8601 with milliseconds
405/// let iso8601_ms = time_format::format_iso8601_ms_utc(ts_ms).unwrap();
406/// assert!(iso8601_ms.ends_with(".678Z"));
407///
408/// // Use with common date formats
409/// let rfc3339 = time_format::format_common_ms_utc(ts_ms, time_format::DateFormat::RFC3339).unwrap();
410/// assert!(rfc3339.contains(".678"));
411/// ```
412///
413/// ## Converting between TimeStamp and TimeStampMs
414///
415/// ```rust
416/// use std::time::{SystemTime, UNIX_EPOCH, Duration};
417///
418/// // Create a system time with millisecond precision
419/// let system_time = UNIX_EPOCH + Duration::from_millis(1673793045678);
420///
421/// // Convert to TimeStampMs
422/// let ts_ms = time_format::from_system_time_ms(system_time).unwrap();
423/// assert_eq!(ts_ms.seconds, 1673793045);
424/// assert_eq!(ts_ms.milliseconds, 678);
425///
426/// // Convert to TimeStamp (loses millisecond precision)
427/// let ts = time_format::from_system_time(system_time).unwrap();
428/// assert_eq!(ts, 1673793045);
429///
430/// // Convert from TimeStamp to TimeStampMs
431/// let ts_ms_from_ts = time_format::TimeStampMs::from_timestamp(ts);
432/// assert_eq!(ts_ms_from_ts.seconds, ts);
433/// assert_eq!(ts_ms_from_ts.milliseconds, 0); // milliseconds are lost
434/// ```
435pub fn from_system_time_ms(time: std::time::SystemTime) -> Result<TimeStampMs, Error> {
436    let duration = time
437        .duration_since(std::time::UNIX_EPOCH)
438        .map_err(|_| Error::TimeError)?;
439
440    let seconds = duration
441        .as_secs()
442        .try_into()
443        .map_err(|_| Error::InvalidTimestamp)?;
444    let millis = duration.subsec_millis() as u16;
445
446    Ok(TimeStampMs::new(seconds, millis))
447}
448
449/// Return the current UNIX timestamp with millisecond precision.
450pub fn now_ms() -> Result<TimeStampMs, Error> {
451    from_system_time_ms(std::time::SystemTime::now())
452}
453
454/// Return the current time in the specified format, in the UTC time zone.
455/// The time is assumed to be the number of seconds since the Epoch.
456///
457/// This function will validate the format string before attempting to format the time.
458pub fn strftime_utc(format: impl AsRef<str>, ts_seconds: TimeStamp) -> Result<String, Error> {
459    let format = format.as_ref();
460
461    // Validate the format string
462    validate_format(format)?;
463
464    let mut tm = MaybeUninit::<tm>::uninit();
465    if !unsafe { safe_gmtime(&ts_seconds, tm.as_mut_ptr()) } {
466        return Err(Error::TimeError);
467    }
468    let tm = unsafe { tm.assume_init() };
469
470    format_time_with_tm(format, &tm)
471}
472
473/// Return the current time in the specified format, in the local time zone.
474/// The time is assumed to be the number of seconds since the Epoch.
475///
476/// This function will validate the format string before attempting to format the time.
477pub fn strftime_local(format: impl AsRef<str>, ts_seconds: TimeStamp) -> Result<String, Error> {
478    let format = format.as_ref();
479
480    // Validate the format string
481    validate_format(format)?;
482
483    let mut tm = MaybeUninit::<tm>::uninit();
484    if !unsafe { safe_localtime(&ts_seconds, tm.as_mut_ptr()) } {
485        return Err(Error::TimeError);
486    }
487    let tm = unsafe { tm.assume_init() };
488
489    format_time_with_tm(format, &tm)
490}
491
492// Internal helper function to format time with a tm struct
493fn format_time_with_tm(format: &str, tm: &tm) -> Result<String, Error> {
494    let format = CString::new(format).map_err(|_| Error::NullByteError)?;
495
496    const MAX_BUF_SIZE: usize = 1024 * 1024;
497    let mut buf_size = format.as_bytes().len().max(128);
498    let mut buf: Vec<u8> = vec![0; buf_size];
499
500    loop {
501        let len = unsafe {
502            strftime(
503                buf.as_mut_ptr() as *mut c_char,
504                buf_size,
505                format.as_ptr() as *const c_char,
506                tm,
507            )
508        };
509
510        if len > 0 {
511            buf.truncate(len);
512            return Ok(String::from_utf8_lossy(&buf).into_owned());
513        }
514
515        if buf_size >= MAX_BUF_SIZE {
516            return Err(Error::InvalidFormatString);
517        }
518
519        buf_size *= 2;
520        buf.resize(buf_size, 0);
521    }
522}
523
524/// Return the current time in the specified format, in the UTC time zone,
525/// with support for custom millisecond formatting.
526///
527/// The standard format directives from strftime are supported.
528/// Additionally, the special text sequence '{ms}' will be replaced with the millisecond component.
529///
530/// Example: strftime_ms_utc("%Y-%m-%d %H:%M:%S.{ms}", ts_ms)
531///
532/// This function will validate the format string before attempting to format the time.
533pub fn strftime_ms_utc(format: impl AsRef<str>, ts_ms: TimeStampMs) -> Result<String, Error> {
534    let format_str = format.as_ref();
535    let seconds_formatted = strftime_utc(format_str, ts_ms.seconds)?;
536
537    // If the format contains the {ms} placeholder, replace it with the milliseconds
538    if format_str.contains("{ms}") {
539        // Format milliseconds with leading zeros
540        let ms_str = format!("{:03}", ts_ms.milliseconds);
541        Ok(seconds_formatted.replace("{ms}", &ms_str))
542    } else {
543        Ok(seconds_formatted)
544    }
545}
546
547/// Return the current time in the specified format, in the local time zone,
548/// with support for custom millisecond formatting.
549///
550/// The standard format directives from strftime are supported.
551/// Additionally, the special text sequence '{ms}' will be replaced with the millisecond component.
552///
553/// Example: strftime_ms_local("%Y-%m-%d %H:%M:%S.{ms}", ts_ms)
554///
555/// This function will validate the format string before attempting to format the time.
556pub fn strftime_ms_local(format: impl AsRef<str>, ts_ms: TimeStampMs) -> Result<String, Error> {
557    let format_str = format.as_ref();
558    let seconds_formatted = strftime_local(format_str, ts_ms.seconds)?;
559
560    // If the format contains the {ms} placeholder, replace it with the milliseconds
561    if format_str.contains("{ms}") {
562        // Format milliseconds with leading zeros
563        let ms_str = format!("{:03}", ts_ms.milliseconds);
564        Ok(seconds_formatted.replace("{ms}", &ms_str))
565    } else {
566        Ok(seconds_formatted)
567    }
568}
569
570/// Format a timestamp according to ISO 8601 format in UTC.
571///
572/// ISO 8601 is an international standard for date and time representations.
573/// This function returns the timestamp in the format: `YYYY-MM-DDThh:mm:ssZ`
574///
575/// Example: "2025-05-20T14:30:45Z"
576///
577/// For more details on ISO 8601, see: https://en.wikipedia.org/wiki/ISO_8601
578pub fn format_iso8601_utc(ts: TimeStamp) -> Result<String, Error> {
579    strftime_utc("%Y-%m-%dT%H:%M:%SZ", ts)
580}
581
582/// Format a timestamp with millisecond precision according to ISO 8601 format in UTC.
583///
584/// ISO 8601 is an international standard for date and time representations.
585/// This function returns the timestamp in the format: `YYYY-MM-DDThh:mm:ss.sssZ`
586///
587/// Example: "2025-05-20T14:30:45.123Z"
588///
589/// For more details on ISO 8601, see: https://en.wikipedia.org/wiki/ISO_8601
590pub fn format_iso8601_ms_utc(ts_ms: TimeStampMs) -> Result<String, Error> {
591    strftime_ms_utc("%Y-%m-%dT%H:%M:%S.{ms}Z", ts_ms)
592}
593
594/// Format a timestamp according to ISO 8601 format in the local timezone.
595///
596/// This function returns the timestamp in the format: `YYYY-MM-DDThh:mm:ss±hh:mm`
597/// where the `±hh:mm` part represents the timezone offset from UTC.
598///
599/// Example: "2025-05-20T09:30:45-05:00"
600///
601/// For more details on ISO 8601, see: https://en.wikipedia.org/wiki/ISO_8601
602pub fn format_iso8601_local(ts: TimeStamp) -> Result<String, Error> {
603    strftime_local("%Y-%m-%dT%H:%M:%S%z", ts).map(|s| {
604        // Standard ISO 8601 requires a colon in timezone offset (e.g., -05:00 not -0500)
605        // But strftime just gives us -0500, so we need to insert the colon
606        if s.len() > 5 && s.chars().last().unwrap().is_ascii_digit() {
607            let len = s.len();
608            format!("{}:{}", &s[..len - 2], &s[len - 2..])
609        } else {
610            s
611        }
612    })
613}
614
615/// Format a timestamp with millisecond precision according to ISO 8601 format in the local timezone.
616///
617/// This function returns the timestamp in the format: `YYYY-MM-DDThh:mm:ss.sss±hh:mm`
618/// where the `±hh:mm` part represents the timezone offset from UTC.
619///
620/// Example: "2025-05-20T09:30:45.123-05:00"
621///
622/// For more details on ISO 8601, see: https://en.wikipedia.org/wiki/ISO_8601
623pub fn format_iso8601_ms_local(ts_ms: TimeStampMs) -> Result<String, Error> {
624    strftime_ms_local("%Y-%m-%dT%H:%M:%S.{ms}%z", ts_ms).map(|s| {
625        // Insert colon in timezone offset for ISO 8601 compliance
626        let len = s.len();
627        if len > 5 && s.chars().last().unwrap().is_ascii_digit() {
628            format!("{}:{}", &s[..len - 2], &s[len - 2..])
629        } else {
630            s
631        }
632    })
633}
634
635/// Format types for common date strings
636///
637/// This enum provides common date and time format patterns.
638#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
639pub enum DateFormat {
640    /// RFC 3339 (similar to ISO 8601) format: "2025-05-20T14:30:45Z" or "2025-05-20T14:30:45-05:00"
641    RFC3339,
642    /// RFC 2822 format: "Tue, 20 May 2025 14:30:45 -0500"
643    RFC2822,
644    /// HTTP format (RFC 7231): "Tue, 20 May 2025 14:30:45 GMT"
645    HTTP,
646    /// SQL format: "2025-05-20 14:30:45"
647    SQL,
648    /// US date format: "05/20/2025 02:30:45 PM"
649    US,
650    /// European date format: "20/05/2025 14:30:45"
651    European,
652    /// Short date: "05/20/25"
653    ShortDate,
654    /// Long date: "Tuesday, May 20, 2025"
655    LongDate,
656    /// Short time: "14:30"
657    ShortTime,
658    /// Long time: "14:30:45"
659    LongTime,
660    /// Date and time: "2025-05-20 14:30:45"
661    DateTime,
662    /// Custom format string
663    Custom(&'static str),
664}
665
666impl DateFormat {
667    /// Get the format string for this format
668    fn get_format_string(&self) -> &'static str {
669        match self {
670            Self::RFC3339 => "%Y-%m-%dT%H:%M:%S%z",
671            Self::RFC2822 => "%a, %d %b %Y %H:%M:%S %z",
672            Self::HTTP => "%a, %d %b %Y %H:%M:%S GMT",
673            Self::SQL => "%Y-%m-%d %H:%M:%S",
674            Self::US => "%m/%d/%Y %I:%M:%S %p",
675            Self::European => "%d/%m/%Y %H:%M:%S",
676            Self::ShortDate => "%m/%d/%y",
677            Self::LongDate => "%A, %B %d, %Y",
678            Self::ShortTime => "%H:%M",
679            Self::LongTime => "%H:%M:%S",
680            Self::DateTime => "%Y-%m-%d %H:%M:%S",
681            Self::Custom(fmt) => fmt,
682        }
683    }
684}
685
686/// Format a timestamp using a common date format in UTC timezone
687///
688/// Examples:
689/// ```rust
690/// let ts = time_format::now().unwrap();
691///
692/// // Format as RFC 3339
693/// let rfc3339 = time_format::format_common_utc(ts, time_format::DateFormat::RFC3339).unwrap();
694///
695/// // Format as HTTP date
696/// let http_date = time_format::format_common_utc(ts, time_format::DateFormat::HTTP).unwrap();
697///
698/// // Format with a custom format
699/// let custom = time_format::format_common_utc(ts, time_format::DateFormat::Custom("%Y-%m-%d")).unwrap();
700/// ```
701pub fn format_common_utc(ts: TimeStamp, format: DateFormat) -> Result<String, Error> {
702    let format_str = format.get_format_string();
703
704    match format {
705        DateFormat::RFC3339 => {
706            // Handle RFC3339 specially to ensure proper timezone formatting
707            strftime_utc(format_str, ts).map(|s| {
708                if s.chars().last().unwrap().is_ascii_digit() {
709                    let len = s.len();
710                    format!("{}:{}", &s[..len - 2], &s[len - 2..])
711                } else {
712                    s
713                }
714            })
715        }
716        _ => strftime_utc(format_str, ts),
717    }
718}
719
720/// Format a timestamp using a common date format in local timezone
721///
722/// Examples:
723/// ```rust
724/// let ts = time_format::now().unwrap();
725///
726/// // Format as RFC 2822
727/// let rfc2822 = time_format::format_common_local(ts, time_format::DateFormat::RFC2822).unwrap();
728///
729/// // Format as US date
730/// let us_date = time_format::format_common_local(ts, time_format::DateFormat::US).unwrap();
731/// ```
732pub fn format_common_local(ts: TimeStamp, format: DateFormat) -> Result<String, Error> {
733    let format_str = format.get_format_string();
734
735    match format {
736        DateFormat::RFC3339 => format_iso8601_local(ts),
737        DateFormat::HTTP => {
738            // HTTP dates are always in GMT/UTC, so redirect to the UTC version
739            format_common_utc(ts, format)
740        }
741        _ => strftime_local(format_str, ts),
742    }
743}
744
745/// Format a timestamp with millisecond precision using a common date format in UTC timezone
746///
747/// This function extends common date formats to include milliseconds where appropriate.
748/// For formats that don't typically include milliseconds (like ShortDate), the milliseconds are ignored.
749///
750/// Examples:
751/// ```rust
752/// let ts_ms = time_format::now_ms().unwrap();
753///
754/// // Format as RFC 3339 with milliseconds
755/// let rfc3339 = time_format::format_common_ms_utc(ts_ms, time_format::DateFormat::RFC3339).unwrap();
756/// // Example: "2025-05-20T14:30:45.123Z"
757/// ```
758pub fn format_common_ms_utc(ts_ms: TimeStampMs, format: DateFormat) -> Result<String, Error> {
759    // For formats that can reasonably include milliseconds, add them
760    let format_str = match format {
761        DateFormat::RFC3339 => "%Y-%m-%dT%H:%M:%S.{ms}%z",
762        DateFormat::SQL => "%Y-%m-%d %H:%M:%S.{ms}",
763        DateFormat::DateTime => "%Y-%m-%d %H:%M:%S.{ms}",
764        DateFormat::LongTime => "%H:%M:%S.{ms}",
765        DateFormat::Custom(fmt) => fmt,
766        _ => format.get_format_string(), // Use standard format for others
767    };
768
769    match format {
770        DateFormat::RFC3339 => {
771            // Handle RFC3339 specially for timezone formatting
772            strftime_ms_utc(format_str, ts_ms).map(|s| {
773                if s.chars().last().unwrap().is_ascii_digit() {
774                    let len = s.len();
775                    format!("{}:{}", &s[..len - 2], &s[len - 2..])
776                } else {
777                    s
778                }
779            })
780        }
781        _ => strftime_ms_utc(format_str, ts_ms),
782    }
783}
784
785/// Format a timestamp with millisecond precision using a common date format in local timezone
786///
787/// This function extends common date formats to include milliseconds where appropriate.
788/// For formats that don't typically include milliseconds (like ShortDate), the milliseconds are ignored.
789///
790/// Examples:
791/// ```rust
792/// let ts_ms = time_format::now_ms().unwrap();
793///
794/// // Format as RFC 3339 with milliseconds in local time
795/// let local_time = time_format::format_common_ms_local(ts_ms, time_format::DateFormat::RFC3339).unwrap();
796/// // Example: "2025-05-20T09:30:45.123-05:00"
797/// ```
798pub fn format_common_ms_local(ts_ms: TimeStampMs, format: DateFormat) -> Result<String, Error> {
799    // For formats that can reasonably include milliseconds, add them
800    let format_str = match format {
801        DateFormat::SQL => "%Y-%m-%d %H:%M:%S.{ms}",
802        DateFormat::DateTime => "%Y-%m-%d %H:%M:%S.{ms}",
803        DateFormat::LongTime => "%H:%M:%S.{ms}",
804        DateFormat::Custom(fmt) => fmt,
805        _ => format.get_format_string(), // Use standard format for others
806    };
807
808    match format {
809        DateFormat::RFC3339 => format_iso8601_ms_local(ts_ms),
810        DateFormat::HTTP => {
811            // HTTP dates are always in GMT/UTC, so redirect to the UTC version
812            format_common_ms_utc(ts_ms, format)
813        }
814        _ => strftime_ms_local(format_str, ts_ms),
815    }
816}
817
818#[cfg(test)]
819mod tests {
820    use super::*;
821
822    #[test]
823    fn test_components_utc() {
824        // Test a known timestamp: 2023-01-15 14:30:45 UTC
825        let ts = 1673793045;
826        let components = components_utc(ts).unwrap();
827
828        assert_eq!(components.year, 2023);
829        assert_eq!(components.month, 1);
830        assert_eq!(components.month_day, 15);
831        assert_eq!(components.hour, 14);
832        assert_eq!(components.min, 30);
833        assert_eq!(components.sec, 45);
834    }
835
836    #[test]
837    fn test_strftime_utc() {
838        // Test a known timestamp: 2023-01-15 14:30:45 UTC
839        let ts = 1673793045;
840        let formatted = strftime_utc("%Y-%m-%d %H:%M:%S", ts).unwrap();
841        assert_eq!(formatted, "2023-01-15 14:30:45");
842    }
843
844    #[test]
845    fn test_strftime_locale_specifiers() {
846        let ts = 1673793045;
847
848        let c_format = strftime_utc("%c", ts).unwrap();
849        assert!(!c_format.is_empty());
850
851        let z_format = strftime_utc("%Z", ts).unwrap();
852        assert!(!z_format.is_empty());
853
854        let c_local = strftime_local("%c", ts).unwrap();
855        assert!(!c_local.is_empty());
856
857        let z_local = strftime_local("%Z", ts).unwrap();
858        assert!(!z_local.is_empty());
859    }
860
861    #[test]
862    fn test_iso8601_utc() {
863        let ts = 1673793045;
864        let formatted = format_iso8601_utc(ts).unwrap();
865        assert_eq!(formatted, "2023-01-15T14:30:45Z");
866    }
867
868    #[test]
869    fn test_timestamp_ms() {
870        let ts_ms = TimeStampMs::new(1673793045, 678);
871        assert_eq!(ts_ms.seconds, 1673793045);
872        assert_eq!(ts_ms.milliseconds, 678);
873        assert_eq!(ts_ms.total_milliseconds(), 1673793045678);
874    }
875
876    #[test]
877    fn test_strftime_ms_utc() {
878        let ts_ms = TimeStampMs::new(1673793045, 678);
879        let formatted = strftime_ms_utc("%Y-%m-%d %H:%M:%S.{ms}", ts_ms).unwrap();
880        assert_eq!(formatted, "2023-01-15 14:30:45.678");
881    }
882
883    #[test]
884    fn test_iso8601_ms_utc() {
885        let ts_ms = TimeStampMs::new(1673793045, 678);
886        let formatted = format_iso8601_ms_utc(ts_ms).unwrap();
887        assert_eq!(formatted, "2023-01-15T14:30:45.678Z");
888    }
889
890    #[test]
891    fn test_validate_format() {
892        assert!(validate_format("%Y-%m-%d").is_ok());
893        assert!(validate_format("%Y-%m-%d %H:%M:%S").is_ok());
894        assert!(validate_format("").is_err());
895        assert!(validate_format("%").is_err());
896        assert!(validate_format("%Q").is_err()); // Invalid specifier
897        assert!(validate_format("test\0test").is_err()); // Null byte
898    }
899
900    #[test]
901    fn test_common_formats() {
902        let ts = 1673793045;
903
904        // Test various common formats
905        let sql = format_common_utc(ts, DateFormat::SQL).unwrap();
906        assert_eq!(sql, "2023-01-15 14:30:45");
907
908        let datetime = format_common_utc(ts, DateFormat::DateTime).unwrap();
909        assert_eq!(datetime, "2023-01-15 14:30:45");
910
911        let short_time = format_common_utc(ts, DateFormat::ShortTime).unwrap();
912        assert_eq!(short_time, "14:30");
913
914        let long_time = format_common_utc(ts, DateFormat::LongTime).unwrap();
915        assert_eq!(long_time, "14:30:45");
916    }
917
918    #[test]
919    fn test_from_system_time() {
920        use std::time::{Duration, UNIX_EPOCH};
921
922        let system_time = UNIX_EPOCH + Duration::from_secs(1673793045);
923        let ts = from_system_time(system_time).unwrap();
924        assert_eq!(ts, 1673793045);
925
926        let components = components_utc(ts).unwrap();
927        assert_eq!(components.year, 2023);
928        assert_eq!(components.month, 1);
929        assert_eq!(components.month_day, 15);
930    }
931
932    #[test]
933    fn test_from_system_time_ms() {
934        use std::time::{Duration, UNIX_EPOCH};
935
936        let system_time = UNIX_EPOCH + Duration::from_millis(1673793045678);
937        let ts_ms = from_system_time_ms(system_time).unwrap();
938        assert_eq!(ts_ms.seconds, 1673793045);
939        assert_eq!(ts_ms.milliseconds, 678);
940    }
941
942    #[test]
943    fn test_epoch() {
944        // Test Unix epoch (January 1, 1970, 00:00:00 UTC)
945        let components = components_utc(0).unwrap();
946        assert_eq!(components.year, 1970);
947        assert_eq!(components.month, 1);
948        assert_eq!(components.month_day, 1);
949        assert_eq!(components.hour, 0);
950        assert_eq!(components.min, 0);
951        assert_eq!(components.sec, 0);
952    }
953
954    #[test]
955    fn test_y2k() {
956        // Test Y2K (January 1, 2000, 00:00:00 UTC)
957        let ts = 946684800;
958        let components = components_utc(ts).unwrap();
959        assert_eq!(components.year, 2000);
960        assert_eq!(components.month, 1);
961        assert_eq!(components.month_day, 1);
962    }
963
964    #[test]
965    fn test_leap_year() {
966        // Test February 29, 2020 (leap year)
967        let ts = 1582934400;
968        let components = components_utc(ts).unwrap();
969        assert_eq!(components.year, 2020);
970        assert_eq!(components.month, 2);
971        assert_eq!(components.month_day, 29);
972    }
973}