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}