1mod internal;
2
3use std::{
4 ffi::{CStr, CString},
5 time::{SystemTime, UNIX_EPOCH},
6};
7
8use internal::*;
9
10pub fn strtotime(
26 date_time: &str,
27 base_timestamp: Option<i64>,
28 timezone: &Timezone,
29) -> Result<i64, String> {
30 if date_time.is_empty() {
31 return Err("Empty date_time string.".into());
32 }
33
34 let Ok(date_time_c_str) = CString::new(date_time) else {
35 return Err("Malformed date_time string.".into());
36 };
37
38 unsafe {
39 let mut error = std::mem::MaybeUninit::uninit();
40 let parsed_time = timelib_strtotime(
41 date_time_c_str.as_ptr(),
42 date_time_c_str.to_bytes().len(),
43 error.as_mut_ptr(),
44 timelib_builtin_db(),
45 Some(timelib_tz_get_wrapper_cached),
46 );
47 let err_count = (*error.assume_init()).error_count;
48 timelib_error_container_dtor(error.assume_init());
49 if err_count != 0 {
50 timelib_time_dtor(parsed_time);
51 return Err("Invalid date_time string.".into());
53 }
54
55 let base = timelib_time_ctor();
56 (*base).tz_info = timezone.tzi;
57 (*base).zone_type = TIMELIB_ZONETYPE_ID;
58 timelib_unixtime2local(base, base_timestamp.unwrap_or_else(rust_now_sec));
59
60 timelib_fill_holes(parsed_time, base, TIMELIB_NO_CLONE as i32);
61 timelib_update_ts(parsed_time, timezone.tzi);
62 let result = (*parsed_time).sse;
63 timelib_time_dtor(parsed_time);
64 timelib_time_dtor(base);
65
66 Ok(result)
67 }
68}
69
70fn rust_now_sec() -> i64 {
71 SystemTime::now()
72 .duration_since(UNIX_EPOCH)
73 .unwrap()
74 .as_secs() as i64
75}
76
77#[derive(Debug, PartialEq)]
79pub struct Timezone {
80 tzi: *mut timelib_tzinfo,
81}
82
83impl Drop for Timezone {
84 fn drop(&mut self) {
85 unsafe {
86 timelib_tzinfo_dtor(self.tzi);
87 }
88 }
89}
90
91impl Timezone {
92 pub fn parse(timezone: &str) -> Result<Timezone, String> {
105 let Ok(tz_c_str) = CString::new(timezone) else {
106 return Err("Malformed timezone string.".into());
107 };
108 let mut error_code: i32 = 0;
109 let error_code_ptr = &mut error_code as *mut i32;
110 unsafe {
111 let tzi = timelib_parse_tzfile(tz_c_str.as_ptr(), timelib_builtin_db(), error_code_ptr);
112 if tzi.is_null() {
113 return Err(format!("Invalid timezone. Err: {error_code}."));
114 }
115 Ok(Self { tzi })
116 }
117 }
118
119 pub fn db_version() -> String {
121 let cstr = unsafe { CStr::from_ptr((*timelib_builtin_db()).version) };
122 String::from_utf8_lossy(cstr.to_bytes()).to_string()
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn strtotime_empty_input() {
132 let tz = Timezone::parse("UTC").unwrap();
133 let result = strtotime("", None, &tz);
134 assert_eq!(Err("Empty date_time string.".to_string()), result);
135 }
136
137 #[test]
138 fn strtotime_invalid_date_time() {
139 let tz = Timezone::parse("UTC").unwrap();
140 let result = strtotime("derp", None, &tz);
141 assert_eq!(Err("Invalid date_time string.".to_string()), result);
142 }
143
144 #[test]
145 fn strtotime_invalid_date_time_string() {
146 let tz = Timezone::parse("UTC").unwrap();
147 let result = strtotime("today\0", None, &tz);
148 assert_eq!(Err("Malformed date_time string.".to_string()), result);
149 }
150
151 #[test]
152 fn strtotime_valid_date_time_fixed() {
153 let tz = Timezone::parse("UTC").unwrap();
154 let result = strtotime("jun 4 2022", None, &tz);
155 assert_eq!(Ok(1654300800), result);
156 }
157
158 #[test]
159 fn strtotime_valid_date_time_with_timezone_fixed() {
160 let tz = Timezone::parse("UTC").unwrap();
161 let result = strtotime("2006-05-12 13:00:00 America/New_York", None, &tz);
162 assert_eq!(Ok(1147453200), result);
163 let result = strtotime("2006-05-12 13:00:00 America/New_York", None, &tz);
165 assert_eq!(Ok(1147453200), result);
166 }
167
168 #[test]
169 fn strtotime_valid_date_time_fixed_timezone() {
170 let tz = Timezone::parse("America/Chicago").unwrap();
171 let result = strtotime("jun 4 2022", None, &tz);
172 assert_eq!(Ok(1654318800), result);
173 }
174
175 const SEC_PER_DAY: i64 = 86_400;
176
177 #[test]
178 fn strtotime_valid_date_time_relative() {
179 let tz = Timezone::parse("UTC").unwrap();
180 let result = strtotime("tomorrow", None, &tz);
181 assert!(result.is_ok());
182 let result = result.unwrap();
183 let now = rust_now_sec();
184 assert!(now <= result);
185 assert!(now + SEC_PER_DAY >= result);
186 }
187
188 #[test]
189 fn strtotime_valid_date_time_relative_base() {
190 let tz = Timezone::parse("UTC").unwrap();
191 let today = 1654318823; let tomorrow = 1654387200; let result = strtotime("tomorrow", Some(today), &tz);
194 assert_eq!(Ok(tomorrow), result);
195 }
196
197 #[test]
198 fn strtotime_valid_date_time_relative_base_timezone() {
199 let tz = Timezone::parse("America/Chicago").unwrap();
200 let today = 1654318823; let tomorrow = 1654405200; let result = strtotime("tomorrow", Some(today), &tz);
203 assert_eq!(Ok(tomorrow), result);
204 }
205
206 #[test]
207 fn timezone_invalid_timezone() {
208 let result = Timezone::parse("pizza");
209 assert_eq!(Err("Invalid timezone. Err: 6.".to_string()), result);
210 }
211
212 #[test]
213 fn timezone_invalid_timezone_string() {
214 let result = Timezone::parse("UTC\0");
215 assert_eq!(Err("Malformed timezone string.".to_string()), result);
216 }
217
218 #[test]
219 fn timezone_valid_timezone() {
220 let result = Timezone::parse("America/Chicago");
221 assert!(result.is_ok());
222 }
223
224 #[test]
225 fn timezone_db_version() {
226 assert_eq!("2025.1", Timezone::db_version());
227 }
228}