mod internal;
use std::{
ffi::{CStr, CString},
time::{SystemTime, UNIX_EPOCH},
};
use internal::*;
pub fn strtotime(
date_time: &str,
base_timestamp: Option<i64>,
timezone: &Timezone,
) -> Result<i64, String> {
if date_time.is_empty() {
return Err("Empty date_time string.".into());
}
let Ok(date_time_c_str) = CString::new(date_time) else {
return Err("Malformed date_time string.".into());
};
unsafe {
let mut error = std::mem::MaybeUninit::uninit();
let parsed_time = timelib_strtotime(
date_time_c_str.as_ptr(),
date_time_c_str.to_bytes().len(),
error.as_mut_ptr(),
timelib_builtin_db(),
Some(timelib_tz_get_wrapper_cached),
);
let err_count = (*error.assume_init()).error_count;
timelib_error_container_dtor(error.assume_init());
if err_count != 0 {
timelib_time_dtor(parsed_time);
return Err("Invalid date_time string.".into());
}
let base = timelib_time_ctor();
(*base).tz_info = timezone.tzi;
(*base).zone_type = TIMELIB_ZONETYPE_ID;
timelib_unixtime2local(base, base_timestamp.unwrap_or_else(rust_now_sec));
timelib_fill_holes(parsed_time, base, TIMELIB_NO_CLONE as i32);
timelib_update_ts(parsed_time, timezone.tzi);
let result = (*parsed_time).sse;
timelib_time_dtor(parsed_time);
timelib_time_dtor(base);
Ok(result)
}
}
fn rust_now_sec() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64
}
#[derive(Debug, PartialEq)]
pub struct Timezone {
tzi: *mut timelib_tzinfo,
}
impl Drop for Timezone {
fn drop(&mut self) {
unsafe {
timelib_tzinfo_dtor(self.tzi);
}
}
}
impl Timezone {
pub fn parse(timezone: &str) -> Result<Timezone, String> {
let Ok(tz_c_str) = CString::new(timezone) else {
return Err("Malformed timezone string.".into());
};
let mut error_code: i32 = 0;
let error_code_ptr = &mut error_code as *mut i32;
unsafe {
let tzi = timelib_parse_tzfile(tz_c_str.as_ptr(), timelib_builtin_db(), error_code_ptr);
if tzi.is_null() {
return Err(format!("Invalid timezone. Err: {error_code}."));
}
Ok(Self { tzi })
}
}
pub fn db_version() -> String {
let cstr = unsafe { CStr::from_ptr((*timelib_builtin_db()).version) };
String::from_utf8_lossy(cstr.to_bytes()).to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strtotime_empty_input() {
let tz = Timezone::parse("UTC").unwrap();
let result = strtotime("", None, &tz);
assert_eq!(Err("Empty date_time string.".to_string()), result);
}
#[test]
fn strtotime_invalid_date_time() {
let tz = Timezone::parse("UTC").unwrap();
let result = strtotime("derp", None, &tz);
assert_eq!(Err("Invalid date_time string.".to_string()), result);
}
#[test]
fn strtotime_invalid_date_time_string() {
let tz = Timezone::parse("UTC").unwrap();
let result = strtotime("today\0", None, &tz);
assert_eq!(Err("Malformed date_time string.".to_string()), result);
}
#[test]
fn strtotime_valid_date_time_fixed() {
let tz = Timezone::parse("UTC").unwrap();
let result = strtotime("jun 4 2022", None, &tz);
assert_eq!(Ok(1654300800), result);
}
#[test]
fn strtotime_valid_date_time_with_timezone_fixed() {
let tz = Timezone::parse("UTC").unwrap();
let result = strtotime("2006-05-12 13:00:00 America/New_York", None, &tz);
assert_eq!(Ok(1147453200), result);
let result = strtotime("2006-05-12 13:00:00 America/New_York", None, &tz);
assert_eq!(Ok(1147453200), result);
}
#[test]
fn strtotime_valid_date_time_fixed_timezone() {
let tz = Timezone::parse("America/Chicago").unwrap();
let result = strtotime("jun 4 2022", None, &tz);
assert_eq!(Ok(1654318800), result);
}
const SEC_PER_DAY: i64 = 86_400;
#[test]
fn strtotime_valid_date_time_relative() {
let tz = Timezone::parse("UTC").unwrap();
let result = strtotime("tomorrow", None, &tz);
assert!(result.is_ok());
let result = result.unwrap();
let now = rust_now_sec();
assert!(now <= result);
assert!(now + SEC_PER_DAY >= result);
}
#[test]
fn strtotime_valid_date_time_relative_base() {
let tz = Timezone::parse("UTC").unwrap();
let today = 1654318823; let tomorrow = 1654387200; let result = strtotime("tomorrow", Some(today), &tz);
assert_eq!(Ok(tomorrow), result);
}
#[test]
fn strtotime_valid_date_time_relative_base_timezone() {
let tz = Timezone::parse("America/Chicago").unwrap();
let today = 1654318823; let tomorrow = 1654405200; let result = strtotime("tomorrow", Some(today), &tz);
assert_eq!(Ok(tomorrow), result);
}
#[test]
fn timezone_invalid_timezone() {
let result = Timezone::parse("pizza");
assert_eq!(Err("Invalid timezone. Err: 6.".to_string()), result);
}
#[test]
fn timezone_invalid_timezone_string() {
let result = Timezone::parse("UTC\0");
assert_eq!(Err("Malformed timezone string.".to_string()), result);
}
#[test]
fn timezone_valid_timezone() {
let result = Timezone::parse("America/Chicago");
assert!(result.is_ok());
}
#[test]
fn timezone_db_version() {
assert_eq!("2025.2", Timezone::db_version());
}
}