timelib/
lib.rs

1mod internal;
2
3use std::{
4    ffi::{CStr, CString},
5    time::{SystemTime, UNIX_EPOCH},
6};
7
8use internal::*;
9
10/// Returns a timestamp (in seconds since the epoch) or an error (string).
11///
12/// # Arguments
13///
14/// * `date_time` - A string that holds the relative date you wish to compute.
15/// * `base_timestamp` - An optional timestamp (in seconds) to use as your base (defaults to the current timestamp).
16/// * `timezone` - An address of a Timezone object.
17///
18/// # Examples
19///
20/// ```
21/// let tz = timelib::Timezone::parse("America/Chicago").expect("Error parsing timezone!");
22/// timelib::strtotime("tomorrow", None, &tz);
23/// timelib::strtotime("next tuesday", Some(1654318823), &tz);
24/// ```
25pub 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            // TODO expose error message(s)
52            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/// A Timezone wrapper.
78#[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    /// Parses a String into a Timezone instance.
93    ///
94    /// # Arguments
95    ///
96    /// * `timezone` - A String with your IANA Timezone name.
97    ///
98    /// # Examples
99    ///
100    /// ```
101    /// let tz = timelib::Timezone::parse("UTC");
102    /// let tz = timelib::Timezone::parse("America/Chicago");
103    /// ```
104    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    /// Returns the underlying timezone database version.
120    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        // Get again - should use underlying TZ cache.
164        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; // Saturday, June 4, 2022 5:00:23 AM GMT
192        let tomorrow = 1654387200; // Sunday, June 5, 2022 12:00:00 AM GMT
193        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; // Saturday, June 4, 2022 12:00:23 AM GMT-05:00 DST
201        let tomorrow = 1654405200; // Sunday, June 5, 2022 12:00:00 AM GMT-05:00 DST
202        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}