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().peekable();
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                    // Valid format specifier
190                    continue;
191                }
192                Some(_c) => {
193                    // Unknown format specifier
194                    return Err(Error::InvalidFormatString);
195                }
196                None => {
197                    // % at end of string
198                    return Err(Error::InvalidFormatString);
199                }
200            }
201        }
202    }
203
204    // Check for the special {ms} sequence format
205    let ms_braces = format.match_indices('{').count();
206    let ms_closing_braces = format.match_indices('}').count();
207    if ms_braces != ms_closing_braces {
208        return Err(Error::InvalidFormatString);
209    }
210
211    Ok(())
212}
213
214/// Time components.
215#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
216pub struct Components {
217    /// Second.
218    pub sec: u8,
219    /// Minute.
220    pub min: u8,
221    /// Hour.
222    pub hour: u8,
223    /// Day of month.
224    pub month_day: u8,
225    /// Month - January is 1, December is 12.
226    pub month: u8,
227    /// Year.
228    pub year: i16,
229    /// Day of week.
230    pub week_day: u8,
231    /// Day of year.    
232    pub year_day: u16,
233}
234
235/// Split a timestamp into its components in UTC timezone.
236pub fn components_utc(ts_seconds: TimeStamp) -> Result<Components, Error> {
237    let mut tm = MaybeUninit::<tm>::uninit();
238    if !unsafe { safe_gmtime(&ts_seconds, tm.as_mut_ptr()) } {
239        return Err(Error::TimeError);
240    }
241    let tm = unsafe { tm.assume_init() };
242    Ok(Components {
243        sec: tm.tm_sec as _,
244        min: tm.tm_min as _,
245        hour: tm.tm_hour as _,
246        month_day: tm.tm_mday as _,
247        month: (1 + tm.tm_mon) as _,
248        year: (1900 + tm.tm_year) as _,
249        week_day: tm.tm_wday as _,
250        year_day: tm.tm_yday as _,
251    })
252}
253
254/// Split a timestamp into its components in the local timezone.
255pub fn components_local(ts_seconds: TimeStamp) -> Result<Components, Error> {
256    let mut tm = MaybeUninit::<tm>::uninit();
257    if !unsafe { safe_localtime(&ts_seconds, tm.as_mut_ptr()) } {
258        return Err(Error::TimeError);
259    }
260    let tm = unsafe { tm.assume_init() };
261    Ok(Components {
262        sec: tm.tm_sec as _,
263        min: tm.tm_min as _,
264        hour: tm.tm_hour as _,
265        month_day: tm.tm_mday as _,
266        month: (1 + tm.tm_mon) as _,
267        year: (1900 + tm.tm_year) as _,
268        week_day: tm.tm_wday as _,
269        year_day: tm.tm_yday as _,
270    })
271}
272
273/// Convert a `std::time::SystemTime` to a UNIX timestamp in seconds.
274///
275/// This function converts a `std::time::SystemTime` instance to a `TimeStamp` (Unix timestamp in seconds).
276/// It handles the conversion and error cases related to negative timestamps or other time conversion issues.
277///
278/// # Examples
279///
280/// ```rust
281/// use std::time::{SystemTime, UNIX_EPOCH, Duration};
282///
283/// // Convert the current system time to a timestamp
284/// let system_time = SystemTime::now();
285/// let timestamp = time_format::from_system_time(system_time).unwrap();
286///
287/// // Convert a specific time
288/// let past_time = UNIX_EPOCH + Duration::from_secs(1500000000);
289/// let past_timestamp = time_format::from_system_time(past_time).unwrap();
290/// assert_eq!(past_timestamp, 1500000000);
291/// ```
292///
293/// ## Working with Time Components
294///
295/// You can use the function to convert a `SystemTime` to components:
296///
297/// ```rust
298/// use std::time::{SystemTime, UNIX_EPOCH, Duration};
299///
300/// // Create a specific time: January 15, 2023 at 14:30:45 UTC
301/// let specific_time = UNIX_EPOCH + Duration::from_secs(1673793045);
302///
303/// // Convert to timestamp
304/// let ts = time_format::from_system_time(specific_time).unwrap();
305///
306/// // Get the time components
307/// let components = time_format::components_utc(ts).unwrap();
308///
309/// // Verify the time components
310/// assert_eq!(components.year, 2023);
311/// assert_eq!(components.month, 1); // January
312/// assert_eq!(components.month_day, 15);
313/// assert_eq!(components.hour, 14);
314/// assert_eq!(components.min, 30);
315/// assert_eq!(components.sec, 45);
316/// ```
317///
318/// ## Formatting with strftime
319///
320/// Convert a `SystemTime` and format it as a string:
321///
322/// ```rust
323/// use std::time::{SystemTime, UNIX_EPOCH, Duration};
324///
325/// // Create a specific time
326/// let specific_time = UNIX_EPOCH + Duration::from_secs(1673793045);
327///
328/// // Convert to timestamp
329/// let ts = time_format::from_system_time(specific_time).unwrap();
330///
331/// // Format as ISO 8601
332/// let iso8601 = time_format::format_iso8601_utc(ts).unwrap();
333/// assert_eq!(iso8601, "2023-01-15T14:30:45Z");
334///
335/// // Custom formatting
336/// let custom_format = time_format::strftime_utc("%B %d, %Y at %H:%M:%S", ts).unwrap();
337/// assert_eq!(custom_format, "January 15, 2023 at 14:30:45");
338/// ```
339pub fn from_system_time(time: std::time::SystemTime) -> Result<TimeStamp, Error> {
340    time.duration_since(std::time::UNIX_EPOCH)
341        .map_err(|_| Error::TimeError)?
342        .as_secs()
343        .try_into()
344        .map_err(|_| Error::InvalidTimestamp)
345}
346
347/// Return the current UNIX timestamp in seconds.
348pub fn now() -> Result<TimeStamp, Error> {
349    from_system_time(std::time::SystemTime::now())
350}
351
352/// Convert a `std::time::SystemTime` to a UNIX timestamp with millisecond precision.
353///
354/// This function converts a `std::time::SystemTime` instance to a `TimeStampMs` (Unix timestamp with millisecond precision).
355/// It extracts both the seconds and milliseconds components from the system time.
356///
357/// # Examples
358///
359/// ```rust
360/// use std::time::{SystemTime, UNIX_EPOCH, Duration};
361///
362/// // Convert the current system time to a timestamp with millisecond precision
363/// let system_time = SystemTime::now();
364/// let timestamp_ms = time_format::from_system_time_ms(system_time).unwrap();
365/// println!("Seconds: {}, Milliseconds: {}", timestamp_ms.seconds, timestamp_ms.milliseconds);
366///
367/// // Convert a specific time with millisecond precision
368/// let specific_time = UNIX_EPOCH + Duration::from_millis(1500000123);
369/// let specific_ts_ms = time_format::from_system_time_ms(specific_time).unwrap();
370/// assert_eq!(specific_ts_ms.seconds, 1500000);
371/// assert_eq!(specific_ts_ms.milliseconds, 123);
372/// ```
373///
374/// ## Using with TimeStampMs methods
375///
376/// ```rust
377/// use std::time::{SystemTime, UNIX_EPOCH, Duration};
378///
379/// // Create a precise time: 1500000 seconds and 123 milliseconds after the epoch
380/// let specific_time = UNIX_EPOCH + Duration::from_millis(1500000123);
381///
382/// // Convert to TimeStampMs
383/// let ts_ms = time_format::from_system_time_ms(specific_time).unwrap();
384///
385/// // Get total milliseconds
386/// let total_ms = ts_ms.total_milliseconds();
387/// assert_eq!(total_ms, 1500000123);
388/// ```
389///
390/// ## Formatting timestamps with millisecond precision
391///
392/// You can format a timestamp with millisecond precision:
393///
394/// ```rust
395/// use std::time::{SystemTime, UNIX_EPOCH, Duration};
396///
397/// // Create a specific timestamp with millisecond precision
398/// // We'll use a fixed timestamp rather than a date calculation to avoid test failures
399/// let ts_ms = time_format::TimeStampMs::new(1743087045, 678);
400///
401/// // Format with milliseconds using your preferred pattern
402/// let formatted = time_format::strftime_ms_utc("%Y-%m-%d %H:%M:%S.{ms}", ts_ms).unwrap();
403///
404/// // Verify the milliseconds are included
405/// assert!(formatted.contains(".678"));
406///
407/// // Format as ISO 8601 with milliseconds
408/// let iso8601_ms = time_format::format_iso8601_ms_utc(ts_ms).unwrap();
409/// assert!(iso8601_ms.ends_with(".678Z"));
410///
411/// // Use with common date formats
412/// let rfc3339 = time_format::format_common_ms_utc(ts_ms, time_format::DateFormat::RFC3339).unwrap();
413/// assert!(rfc3339.contains(".678"));
414/// ```
415///
416/// ## Converting between TimeStamp and TimeStampMs
417///
418/// ```rust
419/// use std::time::{SystemTime, UNIX_EPOCH, Duration};
420///
421/// // Create a system time with millisecond precision
422/// let system_time = UNIX_EPOCH + Duration::from_millis(1673793045678);
423///
424/// // Convert to TimeStampMs
425/// let ts_ms = time_format::from_system_time_ms(system_time).unwrap();
426/// assert_eq!(ts_ms.seconds, 1673793045);
427/// assert_eq!(ts_ms.milliseconds, 678);
428///
429/// // Convert to TimeStamp (loses millisecond precision)
430/// let ts = time_format::from_system_time(system_time).unwrap();
431/// assert_eq!(ts, 1673793045);
432///
433/// // Convert from TimeStamp to TimeStampMs
434/// let ts_ms_from_ts = time_format::TimeStampMs::from_timestamp(ts);
435/// assert_eq!(ts_ms_from_ts.seconds, ts);
436/// assert_eq!(ts_ms_from_ts.milliseconds, 0); // milliseconds are lost
437/// ```
438pub fn from_system_time_ms(time: std::time::SystemTime) -> Result<TimeStampMs, Error> {
439    let duration = time
440        .duration_since(std::time::UNIX_EPOCH)
441        .map_err(|_| Error::TimeError)?;
442
443    let seconds = duration
444        .as_secs()
445        .try_into()
446        .map_err(|_| Error::InvalidTimestamp)?;
447    let millis = duration.subsec_millis() as u16;
448
449    Ok(TimeStampMs::new(seconds, millis))
450}
451
452/// Return the current UNIX timestamp with millisecond precision.
453pub fn now_ms() -> Result<TimeStampMs, Error> {
454    from_system_time_ms(std::time::SystemTime::now())
455}
456
457/// Return the current time in the specified format, in the UTC time zone.
458/// The time is assumed to be the number of seconds since the Epoch.
459///
460/// This function will validate the format string before attempting to format the time.
461pub fn strftime_utc(format: impl AsRef<str>, ts_seconds: TimeStamp) -> Result<String, Error> {
462    let format = format.as_ref();
463
464    // Validate the format string
465    validate_format(format)?;
466
467    let mut tm = MaybeUninit::<tm>::uninit();
468    if !unsafe { safe_gmtime(&ts_seconds, tm.as_mut_ptr()) } {
469        return Err(Error::TimeError);
470    }
471    let tm = unsafe { tm.assume_init() };
472
473    format_time_with_tm(format, &tm)
474}
475
476/// Return the current time in the specified format, in the local time zone.
477/// The time is assumed to be the number of seconds since the Epoch.
478///
479/// This function will validate the format string before attempting to format the time.
480pub fn strftime_local(format: impl AsRef<str>, ts_seconds: TimeStamp) -> Result<String, Error> {
481    let format = format.as_ref();
482
483    // Validate the format string
484    validate_format(format)?;
485
486    let mut tm = MaybeUninit::<tm>::uninit();
487    if !unsafe { safe_localtime(&ts_seconds, tm.as_mut_ptr()) } {
488        return Err(Error::TimeError);
489    }
490    let tm = unsafe { tm.assume_init() };
491
492    format_time_with_tm(format, &tm)
493}
494
495// Internal helper function to format time with a tm struct
496fn format_time_with_tm(format: &str, tm: &tm) -> Result<String, Error> {
497    let format_len = format.len();
498    let format = CString::new(format).map_err(|_| Error::NullByteError)?;
499    let mut buf_size = format_len;
500    let mut buf: Vec<u8> = vec![0; buf_size];
501
502    // Initial attempt
503    let mut len = unsafe {
504        strftime(
505            buf.as_mut_ptr() as *mut c_char,
506            buf_size,
507            format.as_ptr() as *const c_char,
508            tm,
509        )
510    };
511
512    // If the format is invalid, strftime returns 0 but won't use more buffer space
513    // We try once with a much larger buffer to distinguish between these cases
514    if len == 0 {
515        // Try with a larger buffer first
516        buf_size *= 10;
517        buf.resize(buf_size, 0);
518
519        len = unsafe {
520            strftime(
521                buf.as_mut_ptr() as *mut c_char,
522                buf_size,
523                format.as_ptr() as *const c_char,
524                tm,
525            )
526        };
527
528        // If still 0 with a much larger buffer, it's likely an invalid format
529        if len == 0 {
530            return Err(Error::InvalidFormatString);
531        }
532    }
533
534    // Keep growing the buffer if needed
535    while len == 0 {
536        buf_size *= 2;
537        buf.resize(buf_size, 0);
538        len = unsafe {
539            strftime(
540                buf.as_mut_ptr() as *mut c_char,
541                buf_size,
542                format.as_ptr() as *const c_char,
543                tm,
544            )
545        };
546    }
547
548    buf.truncate(len);
549    String::from_utf8(buf).map_err(|_| Error::Utf8Error)
550}
551
552/// Return the current time in the specified format, in the UTC time zone,
553/// with support for custom millisecond formatting.
554///
555/// The standard format directives from strftime are supported.
556/// Additionally, the special text sequence '{ms}' will be replaced with the millisecond component.
557///
558/// Example: strftime_ms_utc("%Y-%m-%d %H:%M:%S.{ms}", ts_ms)
559///
560/// This function will validate the format string before attempting to format the time.
561pub fn strftime_ms_utc(format: impl AsRef<str>, ts_ms: TimeStampMs) -> Result<String, Error> {
562    let format_str = format.as_ref();
563
564    // Validate the format string (validation also checks for balanced braces)
565    validate_format(format_str)?;
566
567    // First, format the seconds part
568    // Skip validation in strftime_utc since we already did it
569    let mut tm = MaybeUninit::<tm>::uninit();
570    if !unsafe { safe_gmtime(&ts_ms.seconds, tm.as_mut_ptr()) } {
571        return Err(Error::TimeError);
572    }
573    let tm = unsafe { tm.assume_init() };
574
575    let seconds_formatted = format_time_with_tm(format_str, &tm)?;
576
577    // If the format contains the {ms} placeholder, replace it with the milliseconds
578    if format_str.contains("{ms}") {
579        // Format milliseconds with leading zeros
580        let ms_str = format!("{:03}", ts_ms.milliseconds);
581        Ok(seconds_formatted.replace("{ms}", &ms_str))
582    } else {
583        Ok(seconds_formatted)
584    }
585}
586
587/// Return the current time in the specified format, in the local time zone,
588/// with support for custom millisecond formatting.
589///
590/// The standard format directives from strftime are supported.
591/// Additionally, the special text sequence '{ms}' will be replaced with the millisecond component.
592///
593/// Example: strftime_ms_local("%Y-%m-%d %H:%M:%S.{ms}", ts_ms)
594///
595/// This function will validate the format string before attempting to format the time.
596pub fn strftime_ms_local(format: impl AsRef<str>, ts_ms: TimeStampMs) -> Result<String, Error> {
597    let format_str = format.as_ref();
598
599    // Validate the format string (validation also checks for balanced braces)
600    validate_format(format_str)?;
601
602    // First, format the seconds part
603    // Skip validation in strftime_local since we already did it
604    let mut tm = MaybeUninit::<tm>::uninit();
605    if !unsafe { safe_localtime(&ts_ms.seconds, tm.as_mut_ptr()) } {
606        return Err(Error::TimeError);
607    }
608    let tm = unsafe { tm.assume_init() };
609
610    let seconds_formatted = format_time_with_tm(format_str, &tm)?;
611
612    // If the format contains the {ms} placeholder, replace it with the milliseconds
613    if format_str.contains("{ms}") {
614        // Format milliseconds with leading zeros
615        let ms_str = format!("{:03}", ts_ms.milliseconds);
616        Ok(seconds_formatted.replace("{ms}", &ms_str))
617    } else {
618        Ok(seconds_formatted)
619    }
620}
621
622/// Format a timestamp according to ISO 8601 format in UTC.
623///
624/// ISO 8601 is an international standard for date and time representations.
625/// This function returns the timestamp in the format: `YYYY-MM-DDThh:mm:ssZ`
626///
627/// Example: "2025-05-20T14:30:45Z"
628///
629/// For more details on ISO 8601, see: https://en.wikipedia.org/wiki/ISO_8601
630pub fn format_iso8601_utc(ts: TimeStamp) -> Result<String, Error> {
631    strftime_utc("%Y-%m-%dT%H:%M:%SZ", ts)
632}
633
634/// Format a timestamp with millisecond precision according to ISO 8601 format in UTC.
635///
636/// ISO 8601 is an international standard for date and time representations.
637/// This function returns the timestamp in the format: `YYYY-MM-DDThh:mm:ss.sssZ`
638///
639/// Example: "2025-05-20T14:30:45.123Z"
640///
641/// For more details on ISO 8601, see: https://en.wikipedia.org/wiki/ISO_8601
642pub fn format_iso8601_ms_utc(ts_ms: TimeStampMs) -> Result<String, Error> {
643    strftime_ms_utc("%Y-%m-%dT%H:%M:%S.{ms}Z", ts_ms)
644}
645
646/// Format a timestamp according to ISO 8601 format in the local timezone.
647///
648/// This function returns the timestamp in the format: `YYYY-MM-DDThh:mm:ss±hh:mm`
649/// where the `±hh:mm` part represents the timezone offset from UTC.
650///
651/// Example: "2025-05-20T09:30:45-05:00"
652///
653/// For more details on ISO 8601, see: https://en.wikipedia.org/wiki/ISO_8601
654pub fn format_iso8601_local(ts: TimeStamp) -> Result<String, Error> {
655    strftime_local("%Y-%m-%dT%H:%M:%S%z", ts).map(|s| {
656        // Standard ISO 8601 requires a colon in timezone offset (e.g., -05:00 not -0500)
657        // But strftime just gives us -0500, so we need to insert the colon
658        if s.len() > 5 && (s.ends_with('0') || s.chars().last().unwrap().is_ascii_digit()) {
659            let len = s.len();
660            format!("{}:{}", &s[..len - 2], &s[len - 2..])
661        } else {
662            s
663        }
664    })
665}
666
667/// Format a timestamp with millisecond precision according to ISO 8601 format in the local timezone.
668///
669/// This function returns the timestamp in the format: `YYYY-MM-DDThh:mm:ss.sss±hh:mm`
670/// where the `±hh:mm` part represents the timezone offset from UTC.
671///
672/// Example: "2025-05-20T09:30:45.123-05:00"
673///
674/// For more details on ISO 8601, see: https://en.wikipedia.org/wiki/ISO_8601
675pub fn format_iso8601_ms_local(ts_ms: TimeStampMs) -> Result<String, Error> {
676    strftime_ms_local("%Y-%m-%dT%H:%M:%S.{ms}%z", ts_ms).map(|s| {
677        // Insert colon in timezone offset for ISO 8601 compliance
678        let len = s.len();
679        if len > 5 && (s.ends_with('0') || s.chars().last().unwrap().is_ascii_digit()) {
680            format!("{}:{}", &s[..len - 2], &s[len - 2..])
681        } else {
682            s
683        }
684    })
685}
686
687/// Format types for common date strings
688///
689/// This enum provides common date and time format patterns.
690#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
691pub enum DateFormat {
692    /// RFC 3339 (similar to ISO 8601) format: "2025-05-20T14:30:45Z" or "2025-05-20T14:30:45-05:00"
693    RFC3339,
694    /// RFC 2822 format: "Tue, 20 May 2025 14:30:45 -0500"
695    RFC2822,
696    /// HTTP format (RFC 7231): "Tue, 20 May 2025 14:30:45 GMT"
697    HTTP,
698    /// SQL format: "2025-05-20 14:30:45"
699    SQL,
700    /// US date format: "05/20/2025 02:30:45 PM"
701    US,
702    /// European date format: "20/05/2025 14:30:45"
703    European,
704    /// Short date: "05/20/25"
705    ShortDate,
706    /// Long date: "Tuesday, May 20, 2025"
707    LongDate,
708    /// Short time: "14:30"
709    ShortTime,
710    /// Long time: "14:30:45"
711    LongTime,
712    /// Date and time: "2025-05-20 14:30:45"
713    DateTime,
714    /// Custom format string
715    Custom(&'static str),
716}
717
718impl DateFormat {
719    /// Get the format string for this format
720    fn get_format_string(&self) -> &'static str {
721        match self {
722            Self::RFC3339 => "%Y-%m-%dT%H:%M:%S%z",
723            Self::RFC2822 => "%a, %d %b %Y %H:%M:%S %z",
724            Self::HTTP => "%a, %d %b %Y %H:%M:%S GMT",
725            Self::SQL => "%Y-%m-%d %H:%M:%S",
726            Self::US => "%m/%d/%Y %I:%M:%S %p",
727            Self::European => "%d/%m/%Y %H:%M:%S",
728            Self::ShortDate => "%m/%d/%y",
729            Self::LongDate => "%A, %B %d, %Y",
730            Self::ShortTime => "%H:%M",
731            Self::LongTime => "%H:%M:%S",
732            Self::DateTime => "%Y-%m-%d %H:%M:%S",
733            Self::Custom(fmt) => fmt,
734        }
735    }
736}
737
738/// Format a timestamp using a common date format in UTC timezone
739///
740/// Examples:
741/// ```rust
742/// let ts = time_format::now().unwrap();
743///
744/// // Format as RFC 3339
745/// let rfc3339 = time_format::format_common_utc(ts, time_format::DateFormat::RFC3339).unwrap();
746///
747/// // Format as HTTP date
748/// let http_date = time_format::format_common_utc(ts, time_format::DateFormat::HTTP).unwrap();
749///
750/// // Format with a custom format
751/// let custom = time_format::format_common_utc(ts, time_format::DateFormat::Custom("%Y-%m-%d")).unwrap();
752/// ```
753pub fn format_common_utc(ts: TimeStamp, format: DateFormat) -> Result<String, Error> {
754    let format_str = format.get_format_string();
755
756    match format {
757        DateFormat::RFC3339 => {
758            // Handle RFC3339 specially to ensure proper timezone formatting
759            strftime_utc(format_str, ts).map(|s| {
760                if s.ends_with('0') || s.chars().last().unwrap().is_ascii_digit() {
761                    let len = s.len();
762                    format!("{}:{}", &s[..len - 2], &s[len - 2..])
763                } else {
764                    s
765                }
766            })
767        }
768        _ => strftime_utc(format_str, ts),
769    }
770}
771
772/// Format a timestamp using a common date format in local timezone
773///
774/// Examples:
775/// ```rust
776/// let ts = time_format::now().unwrap();
777///
778/// // Format as RFC 2822
779/// let rfc2822 = time_format::format_common_local(ts, time_format::DateFormat::RFC2822).unwrap();
780///
781/// // Format as US date
782/// let us_date = time_format::format_common_local(ts, time_format::DateFormat::US).unwrap();
783/// ```
784pub fn format_common_local(ts: TimeStamp, format: DateFormat) -> Result<String, Error> {
785    let format_str = format.get_format_string();
786
787    match format {
788        DateFormat::RFC3339 => {
789            // Handle RFC3339 specially to ensure proper timezone formatting
790            strftime_local(format_str, ts).map(|s| {
791                if s.ends_with('0') || s.chars().last().unwrap().is_ascii_digit() {
792                    let len = s.len();
793                    format!("{}:{}", &s[..len - 2], &s[len - 2..])
794                } else {
795                    s
796                }
797            })
798        }
799        DateFormat::HTTP => {
800            // HTTP dates are always in GMT/UTC, so redirect to the UTC version
801            format_common_utc(ts, format)
802        }
803        _ => strftime_local(format_str, ts),
804    }
805}
806
807/// Format a timestamp with millisecond precision using a common date format in UTC timezone
808///
809/// This function extends common date formats to include milliseconds where appropriate.
810/// For formats that don't typically include milliseconds (like ShortDate), the milliseconds are ignored.
811///
812/// Examples:
813/// ```rust
814/// let ts_ms = time_format::now_ms().unwrap();
815///
816/// // Format as RFC 3339 with milliseconds
817/// let rfc3339 = time_format::format_common_ms_utc(ts_ms, time_format::DateFormat::RFC3339).unwrap();
818/// // Example: "2025-05-20T14:30:45.123Z"
819/// ```
820pub fn format_common_ms_utc(ts_ms: TimeStampMs, format: DateFormat) -> Result<String, Error> {
821    // For formats that can reasonably include milliseconds, add them
822    let format_str = match format {
823        DateFormat::RFC3339 => "%Y-%m-%dT%H:%M:%S.{ms}%z",
824        DateFormat::SQL => "%Y-%m-%d %H:%M:%S.{ms}",
825        DateFormat::DateTime => "%Y-%m-%d %H:%M:%S.{ms}",
826        DateFormat::LongTime => "%H:%M:%S.{ms}",
827        DateFormat::Custom(fmt) => fmt,
828        _ => format.get_format_string(), // Use standard format for others
829    };
830
831    match format {
832        DateFormat::RFC3339 => {
833            // Handle RFC3339 specially for timezone formatting
834            strftime_ms_utc(format_str, ts_ms).map(|s| {
835                if s.ends_with('0') || s.chars().last().unwrap().is_ascii_digit() {
836                    let len = s.len();
837                    format!("{}:{}", &s[..len - 2], &s[len - 2..])
838                } else {
839                    s
840                }
841            })
842        }
843        _ => strftime_ms_utc(format_str, ts_ms),
844    }
845}
846
847/// Format a timestamp with millisecond precision using a common date format in local timezone
848///
849/// This function extends common date formats to include milliseconds where appropriate.
850/// For formats that don't typically include milliseconds (like ShortDate), the milliseconds are ignored.
851///
852/// Examples:
853/// ```rust
854/// let ts_ms = time_format::now_ms().unwrap();
855///
856/// // Format as RFC 3339 with milliseconds in local time
857/// let local_time = time_format::format_common_ms_local(ts_ms, time_format::DateFormat::RFC3339).unwrap();
858/// // Example: "2025-05-20T09:30:45.123-05:00"
859/// ```
860pub fn format_common_ms_local(ts_ms: TimeStampMs, format: DateFormat) -> Result<String, Error> {
861    // For formats that can reasonably include milliseconds, add them
862    let format_str = match format {
863        DateFormat::RFC3339 => "%Y-%m-%dT%H:%M:%S.{ms}%z",
864        DateFormat::SQL => "%Y-%m-%d %H:%M:%S.{ms}",
865        DateFormat::DateTime => "%Y-%m-%d %H:%M:%S.{ms}",
866        DateFormat::LongTime => "%H:%M:%S.{ms}",
867        DateFormat::Custom(fmt) => fmt,
868        _ => format.get_format_string(), // Use standard format for others
869    };
870
871    match format {
872        DateFormat::RFC3339 => {
873            // Handle RFC3339 specially for timezone formatting
874            strftime_ms_local(format_str, ts_ms).map(|s| {
875                if s.ends_with('0') || s.chars().last().unwrap().is_ascii_digit() {
876                    let len = s.len();
877                    format!("{}:{}", &s[..len - 2], &s[len - 2..])
878                } else {
879                    s
880                }
881            })
882        }
883        DateFormat::HTTP => {
884            // HTTP dates are always in GMT/UTC, so redirect to the UTC version
885            format_common_ms_utc(ts_ms, format)
886        }
887        _ => strftime_ms_local(format_str, ts_ms),
888    }
889}
890
891#[cfg(test)]
892mod tests {
893    use super::*;
894
895    #[test]
896    fn test_components_utc() {
897        // Test a known timestamp: 2023-01-15 14:30:45 UTC
898        let ts = 1673793045;
899        let components = components_utc(ts).unwrap();
900
901        assert_eq!(components.year, 2023);
902        assert_eq!(components.month, 1);
903        assert_eq!(components.month_day, 15);
904        assert_eq!(components.hour, 14);
905        assert_eq!(components.min, 30);
906        assert_eq!(components.sec, 45);
907    }
908
909    #[test]
910    fn test_strftime_utc() {
911        // Test a known timestamp: 2023-01-15 14:30:45 UTC
912        let ts = 1673793045;
913        let formatted = strftime_utc("%Y-%m-%d %H:%M:%S", ts).unwrap();
914        assert_eq!(formatted, "2023-01-15 14:30:45");
915    }
916
917    #[test]
918    fn test_iso8601_utc() {
919        let ts = 1673793045;
920        let formatted = format_iso8601_utc(ts).unwrap();
921        assert_eq!(formatted, "2023-01-15T14:30:45Z");
922    }
923
924    #[test]
925    fn test_timestamp_ms() {
926        let ts_ms = TimeStampMs::new(1673793045, 678);
927        assert_eq!(ts_ms.seconds, 1673793045);
928        assert_eq!(ts_ms.milliseconds, 678);
929        assert_eq!(ts_ms.total_milliseconds(), 1673793045678);
930    }
931
932    #[test]
933    fn test_strftime_ms_utc() {
934        let ts_ms = TimeStampMs::new(1673793045, 678);
935        let formatted = strftime_ms_utc("%Y-%m-%d %H:%M:%S.{ms}", ts_ms).unwrap();
936        assert_eq!(formatted, "2023-01-15 14:30:45.678");
937    }
938
939    #[test]
940    fn test_iso8601_ms_utc() {
941        let ts_ms = TimeStampMs::new(1673793045, 678);
942        let formatted = format_iso8601_ms_utc(ts_ms).unwrap();
943        assert_eq!(formatted, "2023-01-15T14:30:45.678Z");
944    }
945
946    #[test]
947    fn test_validate_format() {
948        assert!(validate_format("%Y-%m-%d").is_ok());
949        assert!(validate_format("%Y-%m-%d %H:%M:%S").is_ok());
950        assert!(validate_format("").is_err());
951        assert!(validate_format("%").is_err());
952        assert!(validate_format("%Q").is_err()); // Invalid specifier
953        assert!(validate_format("test\0test").is_err()); // Null byte
954    }
955
956    #[test]
957    fn test_common_formats() {
958        let ts = 1673793045;
959
960        // Test various common formats
961        let sql = format_common_utc(ts, DateFormat::SQL).unwrap();
962        assert_eq!(sql, "2023-01-15 14:30:45");
963
964        let datetime = format_common_utc(ts, DateFormat::DateTime).unwrap();
965        assert_eq!(datetime, "2023-01-15 14:30:45");
966
967        let short_time = format_common_utc(ts, DateFormat::ShortTime).unwrap();
968        assert_eq!(short_time, "14:30");
969
970        let long_time = format_common_utc(ts, DateFormat::LongTime).unwrap();
971        assert_eq!(long_time, "14:30:45");
972    }
973
974    #[test]
975    fn test_from_system_time() {
976        use std::time::{Duration, UNIX_EPOCH};
977
978        let system_time = UNIX_EPOCH + Duration::from_secs(1673793045);
979        let ts = from_system_time(system_time).unwrap();
980        assert_eq!(ts, 1673793045);
981
982        let components = components_utc(ts).unwrap();
983        assert_eq!(components.year, 2023);
984        assert_eq!(components.month, 1);
985        assert_eq!(components.month_day, 15);
986    }
987
988    #[test]
989    fn test_from_system_time_ms() {
990        use std::time::{Duration, UNIX_EPOCH};
991
992        let system_time = UNIX_EPOCH + Duration::from_millis(1673793045678);
993        let ts_ms = from_system_time_ms(system_time).unwrap();
994        assert_eq!(ts_ms.seconds, 1673793045);
995        assert_eq!(ts_ms.milliseconds, 678);
996    }
997
998    #[test]
999    fn test_epoch() {
1000        // Test Unix epoch (January 1, 1970, 00:00:00 UTC)
1001        let components = components_utc(0).unwrap();
1002        assert_eq!(components.year, 1970);
1003        assert_eq!(components.month, 1);
1004        assert_eq!(components.month_day, 1);
1005        assert_eq!(components.hour, 0);
1006        assert_eq!(components.min, 0);
1007        assert_eq!(components.sec, 0);
1008    }
1009
1010    #[test]
1011    fn test_y2k() {
1012        // Test Y2K (January 1, 2000, 00:00:00 UTC)
1013        let ts = 946684800;
1014        let components = components_utc(ts).unwrap();
1015        assert_eq!(components.year, 2000);
1016        assert_eq!(components.month, 1);
1017        assert_eq!(components.month_day, 1);
1018    }
1019
1020    #[test]
1021    fn test_leap_year() {
1022        // Test February 29, 2020 (leap year)
1023        let ts = 1582934400;
1024        let components = components_utc(ts).unwrap();
1025        assert_eq!(components.year, 2020);
1026        assert_eq!(components.month, 2);
1027        assert_eq!(components.month_day, 29);
1028    }
1029}