#![doc = include_str!("../README.md")]
use std::{
convert::TryInto,
ffi::CString,
fmt,
mem::MaybeUninit,
os::raw::{c_char, c_int},
};
#[cfg(not(target_env = "msvc"))]
use std::os::raw::c_long;
#[allow(non_camel_case_types)]
type time_t = i64;
pub type TimeStamp = i64;
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct TimeStampMs {
pub seconds: i64,
pub milliseconds: u16,
}
impl TimeStampMs {
pub fn new(seconds: i64, milliseconds: u16) -> Self {
let milliseconds = milliseconds % 1000;
Self {
seconds,
milliseconds,
}
}
pub fn from_timestamp(ts: TimeStamp) -> Self {
Self {
seconds: ts,
milliseconds: 0,
}
}
pub fn total_milliseconds(&self) -> i64 {
self.seconds * 1000 + self.milliseconds as i64
}
}
#[cfg(not(target_env = "msvc"))]
#[repr(C)]
#[derive(Debug, Copy, Clone)]
struct tm {
pub tm_sec: c_int,
pub tm_min: c_int,
pub tm_hour: c_int,
pub tm_mday: c_int,
pub tm_mon: c_int,
pub tm_year: c_int,
pub tm_wday: c_int,
pub tm_yday: c_int,
pub tm_isdst: c_int,
pub tm_gmtoff: c_long,
pub tm_zone: *mut c_char,
}
#[cfg(target_env = "msvc")]
#[repr(C)]
#[derive(Debug, Copy, Clone)]
struct tm {
pub tm_sec: c_int,
pub tm_min: c_int,
pub tm_hour: c_int,
pub tm_mday: c_int,
pub tm_mon: c_int,
pub tm_year: c_int,
pub tm_wday: c_int,
pub tm_yday: c_int,
pub tm_isdst: c_int,
}
#[cfg(not(target_env = "msvc"))]
extern "C" {
fn gmtime_r(ts: *const time_t, tm: *mut tm) -> *mut tm;
fn localtime_r(ts: *const time_t, tm: *mut tm) -> *mut tm;
fn strftime(s: *mut c_char, maxsize: usize, format: *const c_char, timeptr: *const tm)
-> usize;
}
#[cfg(target_env = "msvc")]
extern "C" {
fn _gmtime64_s(tm: *mut tm, ts: *const time_t) -> c_int;
fn _localtime64_s(tm: *mut tm, ts: *const time_t) -> c_int;
fn strftime(s: *mut c_char, maxsize: usize, format: *const c_char, timeptr: *const tm)
-> usize;
}
#[cfg(not(target_env = "msvc"))]
unsafe fn safe_gmtime(ts: *const time_t, tm: *mut tm) -> bool {
!gmtime_r(ts, tm).is_null()
}
#[cfg(target_env = "msvc")]
unsafe fn safe_gmtime(ts: *const time_t, tm: *mut tm) -> bool {
_gmtime64_s(tm, ts) == 0
}
#[cfg(not(target_env = "msvc"))]
unsafe fn safe_localtime(ts: *const time_t, tm: *mut tm) -> bool {
!localtime_r(ts, tm).is_null()
}
#[cfg(target_env = "msvc")]
unsafe fn safe_localtime(ts: *const time_t, tm: *mut tm) -> bool {
_localtime64_s(tm, ts) == 0
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Error {
TimeError,
InvalidTimestamp,
FormatError,
InvalidFormatString,
Utf8Error,
NullByteError,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::TimeError => write!(f, "Time processing error"),
Error::InvalidTimestamp => write!(f, "Invalid timestamp value"),
Error::FormatError => write!(f, "Time formatting error"),
Error::InvalidFormatString => write!(f, "Invalid format string"),
Error::Utf8Error => write!(f, "UTF-8 conversion error"),
Error::NullByteError => write!(f, "String contains null bytes"),
}
}
}
impl std::error::Error for Error {}
pub fn validate_format(format: impl AsRef<str>) -> Result<(), Error> {
let format = format.as_ref();
if format.is_empty() {
return Err(Error::InvalidFormatString);
}
if format.contains('\0') {
return Err(Error::NullByteError);
}
let mut chars = format.chars().peekable();
while let Some(c) = chars.next() {
if c == '%' {
match chars.next() {
Some('a') | Some('A') | Some('b') | Some('B') | Some('c') | Some('C')
| Some('d') | Some('D') | Some('e') | Some('F') | Some('g') | Some('G')
| Some('h') | Some('H') | Some('I') | Some('j') | Some('k') | Some('l')
| Some('m') | Some('M') | Some('n') | Some('p') | Some('P') | Some('r')
| Some('R') | Some('s') | Some('S') | Some('t') | Some('T') | Some('u')
| Some('U') | Some('V') | Some('w') | Some('W') | Some('x') | Some('X')
| Some('y') | Some('Y') | Some('z') | Some('Z') | Some('%') | Some('E')
| Some('O') | Some('+') => {
continue;
}
Some(_c) => {
return Err(Error::InvalidFormatString);
}
None => {
return Err(Error::InvalidFormatString);
}
}
}
}
let ms_braces = format.match_indices('{').count();
let ms_closing_braces = format.match_indices('}').count();
if ms_braces != ms_closing_braces {
return Err(Error::InvalidFormatString);
}
Ok(())
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Components {
pub sec: u8,
pub min: u8,
pub hour: u8,
pub month_day: u8,
pub month: u8,
pub year: i16,
pub week_day: u8,
pub year_day: u16,
}
pub fn components_utc(ts_seconds: TimeStamp) -> Result<Components, Error> {
let mut tm = MaybeUninit::<tm>::uninit();
if !unsafe { safe_gmtime(&ts_seconds, tm.as_mut_ptr()) } {
return Err(Error::TimeError);
}
let tm = unsafe { tm.assume_init() };
Ok(Components {
sec: tm.tm_sec as _,
min: tm.tm_min as _,
hour: tm.tm_hour as _,
month_day: tm.tm_mday as _,
month: (1 + tm.tm_mon) as _,
year: (1900 + tm.tm_year) as _,
week_day: tm.tm_wday as _,
year_day: tm.tm_yday as _,
})
}
pub fn components_local(ts_seconds: TimeStamp) -> Result<Components, Error> {
let mut tm = MaybeUninit::<tm>::uninit();
if !unsafe { safe_localtime(&ts_seconds, tm.as_mut_ptr()) } {
return Err(Error::TimeError);
}
let tm = unsafe { tm.assume_init() };
Ok(Components {
sec: tm.tm_sec as _,
min: tm.tm_min as _,
hour: tm.tm_hour as _,
month_day: tm.tm_mday as _,
month: (1 + tm.tm_mon) as _,
year: (1900 + tm.tm_year) as _,
week_day: tm.tm_wday as _,
year_day: tm.tm_yday as _,
})
}
pub fn from_system_time(time: std::time::SystemTime) -> Result<TimeStamp, Error> {
time.duration_since(std::time::UNIX_EPOCH)
.map_err(|_| Error::TimeError)?
.as_secs()
.try_into()
.map_err(|_| Error::InvalidTimestamp)
}
pub fn now() -> Result<TimeStamp, Error> {
from_system_time(std::time::SystemTime::now())
}
pub fn from_system_time_ms(time: std::time::SystemTime) -> Result<TimeStampMs, Error> {
let duration = time
.duration_since(std::time::UNIX_EPOCH)
.map_err(|_| Error::TimeError)?;
let seconds = duration
.as_secs()
.try_into()
.map_err(|_| Error::InvalidTimestamp)?;
let millis = duration.subsec_millis() as u16;
Ok(TimeStampMs::new(seconds, millis))
}
pub fn now_ms() -> Result<TimeStampMs, Error> {
from_system_time_ms(std::time::SystemTime::now())
}
pub fn strftime_utc(format: impl AsRef<str>, ts_seconds: TimeStamp) -> Result<String, Error> {
let format = format.as_ref();
validate_format(format)?;
let mut tm = MaybeUninit::<tm>::uninit();
if !unsafe { safe_gmtime(&ts_seconds, tm.as_mut_ptr()) } {
return Err(Error::TimeError);
}
let tm = unsafe { tm.assume_init() };
format_time_with_tm(format, &tm)
}
pub fn strftime_local(format: impl AsRef<str>, ts_seconds: TimeStamp) -> Result<String, Error> {
let format = format.as_ref();
validate_format(format)?;
let mut tm = MaybeUninit::<tm>::uninit();
if !unsafe { safe_localtime(&ts_seconds, tm.as_mut_ptr()) } {
return Err(Error::TimeError);
}
let tm = unsafe { tm.assume_init() };
format_time_with_tm(format, &tm)
}
fn format_time_with_tm(format: &str, tm: &tm) -> Result<String, Error> {
let format_len = format.len();
let format = CString::new(format).map_err(|_| Error::NullByteError)?;
let mut buf_size = format_len;
let mut buf: Vec<u8> = vec![0; buf_size];
let mut len = unsafe {
strftime(
buf.as_mut_ptr() as *mut c_char,
buf_size,
format.as_ptr() as *const c_char,
tm,
)
};
if len == 0 {
buf_size *= 10;
buf.resize(buf_size, 0);
len = unsafe {
strftime(
buf.as_mut_ptr() as *mut c_char,
buf_size,
format.as_ptr() as *const c_char,
tm,
)
};
if len == 0 {
return Err(Error::InvalidFormatString);
}
}
while len == 0 {
buf_size *= 2;
buf.resize(buf_size, 0);
len = unsafe {
strftime(
buf.as_mut_ptr() as *mut c_char,
buf_size,
format.as_ptr() as *const c_char,
tm,
)
};
}
buf.truncate(len);
String::from_utf8(buf).map_err(|_| Error::Utf8Error)
}
pub fn strftime_ms_utc(format: impl AsRef<str>, ts_ms: TimeStampMs) -> Result<String, Error> {
let format_str = format.as_ref();
validate_format(format_str)?;
let mut tm = MaybeUninit::<tm>::uninit();
if !unsafe { safe_gmtime(&ts_ms.seconds, tm.as_mut_ptr()) } {
return Err(Error::TimeError);
}
let tm = unsafe { tm.assume_init() };
let seconds_formatted = format_time_with_tm(format_str, &tm)?;
if format_str.contains("{ms}") {
let ms_str = format!("{:03}", ts_ms.milliseconds);
Ok(seconds_formatted.replace("{ms}", &ms_str))
} else {
Ok(seconds_formatted)
}
}
pub fn strftime_ms_local(format: impl AsRef<str>, ts_ms: TimeStampMs) -> Result<String, Error> {
let format_str = format.as_ref();
validate_format(format_str)?;
let mut tm = MaybeUninit::<tm>::uninit();
if !unsafe { safe_localtime(&ts_ms.seconds, tm.as_mut_ptr()) } {
return Err(Error::TimeError);
}
let tm = unsafe { tm.assume_init() };
let seconds_formatted = format_time_with_tm(format_str, &tm)?;
if format_str.contains("{ms}") {
let ms_str = format!("{:03}", ts_ms.milliseconds);
Ok(seconds_formatted.replace("{ms}", &ms_str))
} else {
Ok(seconds_formatted)
}
}
pub fn format_iso8601_utc(ts: TimeStamp) -> Result<String, Error> {
strftime_utc("%Y-%m-%dT%H:%M:%SZ", ts)
}
pub fn format_iso8601_ms_utc(ts_ms: TimeStampMs) -> Result<String, Error> {
strftime_ms_utc("%Y-%m-%dT%H:%M:%S.{ms}Z", ts_ms)
}
pub fn format_iso8601_local(ts: TimeStamp) -> Result<String, Error> {
strftime_local("%Y-%m-%dT%H:%M:%S%z", ts).map(|s| {
if s.len() > 5 && (s.ends_with('0') || s.chars().last().unwrap().is_ascii_digit()) {
let len = s.len();
format!("{}:{}", &s[..len - 2], &s[len - 2..])
} else {
s
}
})
}
pub fn format_iso8601_ms_local(ts_ms: TimeStampMs) -> Result<String, Error> {
strftime_ms_local("%Y-%m-%dT%H:%M:%S.{ms}%z", ts_ms).map(|s| {
let len = s.len();
if len > 5 && (s.ends_with('0') || s.chars().last().unwrap().is_ascii_digit()) {
format!("{}:{}", &s[..len - 2], &s[len - 2..])
} else {
s
}
})
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum DateFormat {
RFC3339,
RFC2822,
HTTP,
SQL,
US,
European,
ShortDate,
LongDate,
ShortTime,
LongTime,
DateTime,
Custom(&'static str),
}
impl DateFormat {
fn get_format_string(&self) -> &'static str {
match self {
Self::RFC3339 => "%Y-%m-%dT%H:%M:%S%z",
Self::RFC2822 => "%a, %d %b %Y %H:%M:%S %z",
Self::HTTP => "%a, %d %b %Y %H:%M:%S GMT",
Self::SQL => "%Y-%m-%d %H:%M:%S",
Self::US => "%m/%d/%Y %I:%M:%S %p",
Self::European => "%d/%m/%Y %H:%M:%S",
Self::ShortDate => "%m/%d/%y",
Self::LongDate => "%A, %B %d, %Y",
Self::ShortTime => "%H:%M",
Self::LongTime => "%H:%M:%S",
Self::DateTime => "%Y-%m-%d %H:%M:%S",
Self::Custom(fmt) => fmt,
}
}
}
pub fn format_common_utc(ts: TimeStamp, format: DateFormat) -> Result<String, Error> {
let format_str = format.get_format_string();
match format {
DateFormat::RFC3339 => {
strftime_utc(format_str, ts).map(|s| {
if s.ends_with('0') || s.chars().last().unwrap().is_ascii_digit() {
let len = s.len();
format!("{}:{}", &s[..len - 2], &s[len - 2..])
} else {
s
}
})
}
_ => strftime_utc(format_str, ts),
}
}
pub fn format_common_local(ts: TimeStamp, format: DateFormat) -> Result<String, Error> {
let format_str = format.get_format_string();
match format {
DateFormat::RFC3339 => {
strftime_local(format_str, ts).map(|s| {
if s.ends_with('0') || s.chars().last().unwrap().is_ascii_digit() {
let len = s.len();
format!("{}:{}", &s[..len - 2], &s[len - 2..])
} else {
s
}
})
}
DateFormat::HTTP => {
format_common_utc(ts, format)
}
_ => strftime_local(format_str, ts),
}
}
pub fn format_common_ms_utc(ts_ms: TimeStampMs, format: DateFormat) -> Result<String, Error> {
let format_str = match format {
DateFormat::RFC3339 => "%Y-%m-%dT%H:%M:%S.{ms}%z",
DateFormat::SQL => "%Y-%m-%d %H:%M:%S.{ms}",
DateFormat::DateTime => "%Y-%m-%d %H:%M:%S.{ms}",
DateFormat::LongTime => "%H:%M:%S.{ms}",
DateFormat::Custom(fmt) => fmt,
_ => format.get_format_string(), };
match format {
DateFormat::RFC3339 => {
strftime_ms_utc(format_str, ts_ms).map(|s| {
if s.ends_with('0') || s.chars().last().unwrap().is_ascii_digit() {
let len = s.len();
format!("{}:{}", &s[..len - 2], &s[len - 2..])
} else {
s
}
})
}
_ => strftime_ms_utc(format_str, ts_ms),
}
}
pub fn format_common_ms_local(ts_ms: TimeStampMs, format: DateFormat) -> Result<String, Error> {
let format_str = match format {
DateFormat::RFC3339 => "%Y-%m-%dT%H:%M:%S.{ms}%z",
DateFormat::SQL => "%Y-%m-%d %H:%M:%S.{ms}",
DateFormat::DateTime => "%Y-%m-%d %H:%M:%S.{ms}",
DateFormat::LongTime => "%H:%M:%S.{ms}",
DateFormat::Custom(fmt) => fmt,
_ => format.get_format_string(), };
match format {
DateFormat::RFC3339 => {
strftime_ms_local(format_str, ts_ms).map(|s| {
if s.ends_with('0') || s.chars().last().unwrap().is_ascii_digit() {
let len = s.len();
format!("{}:{}", &s[..len - 2], &s[len - 2..])
} else {
s
}
})
}
DateFormat::HTTP => {
format_common_ms_utc(ts_ms, format)
}
_ => strftime_ms_local(format_str, ts_ms),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_components_utc() {
let ts = 1673793045;
let components = components_utc(ts).unwrap();
assert_eq!(components.year, 2023);
assert_eq!(components.month, 1);
assert_eq!(components.month_day, 15);
assert_eq!(components.hour, 14);
assert_eq!(components.min, 30);
assert_eq!(components.sec, 45);
}
#[test]
fn test_strftime_utc() {
let ts = 1673793045;
let formatted = strftime_utc("%Y-%m-%d %H:%M:%S", ts).unwrap();
assert_eq!(formatted, "2023-01-15 14:30:45");
}
#[test]
fn test_iso8601_utc() {
let ts = 1673793045;
let formatted = format_iso8601_utc(ts).unwrap();
assert_eq!(formatted, "2023-01-15T14:30:45Z");
}
#[test]
fn test_timestamp_ms() {
let ts_ms = TimeStampMs::new(1673793045, 678);
assert_eq!(ts_ms.seconds, 1673793045);
assert_eq!(ts_ms.milliseconds, 678);
assert_eq!(ts_ms.total_milliseconds(), 1673793045678);
}
#[test]
fn test_strftime_ms_utc() {
let ts_ms = TimeStampMs::new(1673793045, 678);
let formatted = strftime_ms_utc("%Y-%m-%d %H:%M:%S.{ms}", ts_ms).unwrap();
assert_eq!(formatted, "2023-01-15 14:30:45.678");
}
#[test]
fn test_iso8601_ms_utc() {
let ts_ms = TimeStampMs::new(1673793045, 678);
let formatted = format_iso8601_ms_utc(ts_ms).unwrap();
assert_eq!(formatted, "2023-01-15T14:30:45.678Z");
}
#[test]
fn test_validate_format() {
assert!(validate_format("%Y-%m-%d").is_ok());
assert!(validate_format("%Y-%m-%d %H:%M:%S").is_ok());
assert!(validate_format("").is_err());
assert!(validate_format("%").is_err());
assert!(validate_format("%Q").is_err()); assert!(validate_format("test\0test").is_err()); }
#[test]
fn test_common_formats() {
let ts = 1673793045;
let sql = format_common_utc(ts, DateFormat::SQL).unwrap();
assert_eq!(sql, "2023-01-15 14:30:45");
let datetime = format_common_utc(ts, DateFormat::DateTime).unwrap();
assert_eq!(datetime, "2023-01-15 14:30:45");
let short_time = format_common_utc(ts, DateFormat::ShortTime).unwrap();
assert_eq!(short_time, "14:30");
let long_time = format_common_utc(ts, DateFormat::LongTime).unwrap();
assert_eq!(long_time, "14:30:45");
}
#[test]
fn test_from_system_time() {
use std::time::{Duration, UNIX_EPOCH};
let system_time = UNIX_EPOCH + Duration::from_secs(1673793045);
let ts = from_system_time(system_time).unwrap();
assert_eq!(ts, 1673793045);
let components = components_utc(ts).unwrap();
assert_eq!(components.year, 2023);
assert_eq!(components.month, 1);
assert_eq!(components.month_day, 15);
}
#[test]
fn test_from_system_time_ms() {
use std::time::{Duration, UNIX_EPOCH};
let system_time = UNIX_EPOCH + Duration::from_millis(1673793045678);
let ts_ms = from_system_time_ms(system_time).unwrap();
assert_eq!(ts_ms.seconds, 1673793045);
assert_eq!(ts_ms.milliseconds, 678);
}
#[test]
fn test_epoch() {
let components = components_utc(0).unwrap();
assert_eq!(components.year, 1970);
assert_eq!(components.month, 1);
assert_eq!(components.month_day, 1);
assert_eq!(components.hour, 0);
assert_eq!(components.min, 0);
assert_eq!(components.sec, 0);
}
#[test]
fn test_y2k() {
let ts = 946684800;
let components = components_utc(ts).unwrap();
assert_eq!(components.year, 2000);
assert_eq!(components.month, 1);
assert_eq!(components.month_day, 1);
}
#[test]
fn test_leap_year() {
let ts = 1582934400;
let components = components_utc(ts).unwrap();
assert_eq!(components.year, 2020);
assert_eq!(components.month, 2);
assert_eq!(components.month_day, 29);
}
}