libc_strftime/
lib.rs

1//! A wrapper library for the glibc strftime function
2//!
3//! Examples
4//! ========
5//!
6//! Format the current date and time in Brussels in French:
7//!
8//! ```
9//! use std::env;
10//!
11//! env::set_var("LC_ALL", "fr_BE.UTF-8");
12//! env::set_var("TZ", "Europe/Brussels");
13//!
14//! libc_strftime::tz_set();
15//! libc_strftime::set_locale();
16//!
17//! let now = libc_strftime::epoch(); // most likely a u64
18//! let local = libc_strftime::strftime_local("%c", now);
19//! println!("On est: {}", local); // On est: mer 07 aoû 2019 06:19:56 CEST
20//! ```
21
22use std::ffi::CString;
23use std::mem;
24
25mod c {
26    extern "C" {
27        #[cfg(unix)]
28        pub(crate) fn tzset();
29        #[cfg(windows)]
30        pub(crate) fn _tzset();
31        pub(crate) fn strftime(
32            s: *mut libc::c_char,
33            max: libc::size_t,
34            format: *const libc::c_char,
35            tm: *const libc::tm,
36        ) -> usize;
37        pub(crate) fn time(tloc: *const libc::time_t) -> libc::time_t;
38        #[cfg(unix)]
39        pub(crate) fn localtime_r(t: *const libc::time_t, tm: *mut libc::tm);
40        #[cfg(windows)]
41        pub(crate) fn _localtime64_s(tm: *mut libc::tm, t: *const libc::time_t);
42        #[cfg(unix)]
43        pub(crate) fn gmtime_r(t: *const libc::time_t, tm: *mut libc::tm);
44        #[cfg(windows)]
45        pub(crate) fn _gmtime64_s(tm: *mut libc::tm, t: *const libc::time_t);
46    }
47}
48
49/// Get a tm struct in local timezone
50pub fn get_local_tm_from_epoch(epoch: libc::time_t) -> libc::tm {
51    unsafe {
52        let mut now: libc::tm = mem::zeroed();
53        #[cfg(unix)]
54        c::localtime_r(&epoch, &mut now);
55        #[cfg(windows)]
56        c::_localtime64_s(&mut now, &epoch);
57        now
58    }
59}
60
61/// Get a tm struct in GMT
62pub fn get_gmt_tm_from_epoch(epoch: libc::time_t) -> libc::tm {
63    unsafe {
64        let mut now: libc::tm = mem::zeroed();
65        #[cfg(unix)]
66        c::gmtime_r(&epoch, &mut now);
67        #[cfg(windows)]
68        c::_gmtime64_s(&mut now, &epoch);
69        now
70    }
71}
72
73/// Call strftime() using a tm struct provided in input
74fn strftime(format: &str, tm: &libc::tm) -> String {
75    let f = CString::new(format).unwrap();
76    let buf = [0_u8; 100];
77    let l: usize = unsafe { c::strftime(buf.as_ptr() as _, buf.len(), f.as_ptr() as *const _, tm) };
78    std::string::String::from_utf8_lossy(&buf[..l]).to_string()
79}
80
81/// Call strftime() using the local timezone and returns a String
82pub fn strftime_local(format: &str, epoch: libc::time_t) -> String {
83    let tm = get_local_tm_from_epoch(epoch);
84    strftime(format, &tm)
85}
86
87/// Call strftime() using GMT and returns a String
88pub fn strftime_gmt(format: &str, epoch: libc::time_t) -> String {
89    let tm = get_gmt_tm_from_epoch(epoch);
90    strftime(format, &tm)
91}
92
93/// Call setlocale() which will initialize the locale based on the environment variables
94pub fn set_locale() {
95    unsafe {
96        libc::setlocale(libc::LC_ALL, b"\0".as_ptr() as _);
97    }
98}
99
100/// Call tzset() which will initialize the local timezone based on the environment variables
101pub fn tz_set() {
102    unsafe {
103        #[cfg(unix)]
104        c::tzset();
105        #[cfg(windows)]
106        c::_tzset();
107    }
108}
109
110/// Retrieve the current time in epoch format (number of seconds since 1970 in UTC)
111pub fn epoch() -> libc::time_t {
112    unsafe { c::time(std::ptr::null()) }
113}
114
115#[cfg(test)]
116mod tests {
117    use crate::*;
118
119    const EPOCH: libc::time_t = 1_565_151_596;
120
121    #[test]
122    #[cfg(unix)]
123    fn format_time_and_date_in_gmt_and_cest() {
124        use std::env;
125
126        env::set_var("LC_ALL", "en_US.UTF-8");
127        env::set_var("TZ", "Europe/Brussels");
128
129        tz_set();
130        set_locale();
131
132        let gmt = strftime_gmt("%c", EPOCH);
133        let local = strftime_local("%c", EPOCH);
134        #[cfg(target_os = "linux")]
135        assert_eq!(gmt, "Wed 07 Aug 2019 04:19:56 AM GMT");
136        #[cfg(target_os = "macos")]
137        assert_eq!(gmt, "Wed Aug  7 04:19:56 2019");
138        #[cfg(target_os = "linux")]
139        assert_eq!(local, "Wed 07 Aug 2019 06:19:56 AM CEST");
140        #[cfg(target_os = "macos")]
141        assert_eq!(local, "Wed Aug  7 06:19:56 2019");
142
143        env::set_var("LC_ALL", "fr_BE.UTF-8");
144        env::set_var("TZ", "Europe/Brussels");
145
146        tz_set();
147        set_locale();
148
149        let gmt = strftime_gmt("%c", EPOCH);
150        let local = strftime_local("%c", EPOCH);
151        #[cfg(target_os = "linux")]
152        assert_eq!(gmt, "mer 07 aoû 2019 04:19:56 GMT");
153        #[cfg(target_os = "macos")]
154        assert_eq!(gmt, "Mer  7 aoû 04:19:56 2019");
155        #[cfg(target_os = "linux")]
156        assert_eq!(local, "mer 07 aoû 2019 06:19:56 CEST");
157        #[cfg(target_os = "macos")]
158        assert_eq!(local, "Mer  7 aoû 06:19:56 2019");
159    }
160
161    // NOTE: I have no idea how to change the timezone or the language on
162    //       Windows. It's supposed to be with the global environment variable
163    //       TZ but I couldn't make it working... well, at least it returns
164    //       something and it should probably work, right?
165    #[test]
166    #[cfg(windows)]
167    fn format_time_and_date_on_windows() {
168        tz_set();
169        set_locale();
170
171        let gmt = strftime_gmt("%c", EPOCH);
172        let local = strftime_local("%c", EPOCH);
173        #[cfg(target_os = "windows")]
174        assert_eq!(gmt, "8/7/2019 4:19:56 AM");
175        #[cfg(target_os = "windows")]
176        assert_eq!(local, "8/7/2019 4:19:56 AM");
177    }
178}