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}