1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
//! A wrapper library for the glibc strftime function
//!
//! Examples
//! ========
//!
//! Format the current date and time in Brussels in French:
//!
//! ```
//! use std::env;
//!
//! env::set_var("LC_ALL", "fr_BE.UTF-8");
//! env::set_var("TZ", "Europe/Brussels");
//!
//! libc_strftime::tz_set();
//! libc_strftime::set_locale();
//!
//! let now = libc_strftime::epoch(); // most likely a u64
//! let local = libc_strftime::strftime_local("%c", now);
//! println!("On est: {}", local); // On est: mer 07 aoû 2019 06:19:56 CEST
//! ```

use std::ffi::CString;
use std::mem;

mod c {
    extern "C" {
        #[cfg(unix)]
        pub(crate) fn tzset();
        #[cfg(windows)]
        pub(crate) fn _tzset();
        pub(crate) fn strftime(
            s: *mut libc::c_char,
            max: libc::size_t,
            format: *const libc::c_char,
            tm: *const libc::tm,
        ) -> usize;
        pub(crate) fn time(tloc: *const libc::time_t) -> libc::time_t;
        #[cfg(unix)]
        pub(crate) fn localtime_r(t: *const libc::time_t, tm: *mut libc::tm);
        #[cfg(windows)]
        pub(crate) fn _localtime64_s(tm: *mut libc::tm, t: *const libc::time_t);
        #[cfg(unix)]
        pub(crate) fn gmtime_r(t: *const libc::time_t, tm: *mut libc::tm);
        #[cfg(windows)]
        pub(crate) fn _gmtime64_s(tm: *mut libc::tm, t: *const libc::time_t);
    }
}

/// Get a tm struct in local timezone
pub fn get_local_tm_from_epoch(epoch: libc::time_t) -> libc::tm {
    unsafe {
        let mut now: libc::tm = mem::zeroed();
        #[cfg(unix)]
        c::localtime_r(&epoch, &mut now);
        #[cfg(windows)]
        c::_localtime64_s(&mut now, &epoch);
        now
    }
}

/// Get a tm struct in GMT
pub fn get_gmt_tm_from_epoch(epoch: libc::time_t) -> libc::tm {
    unsafe {
        let mut now: libc::tm = mem::zeroed();
        #[cfg(unix)]
        c::gmtime_r(&epoch, &mut now);
        #[cfg(windows)]
        c::_gmtime64_s(&mut now, &epoch);
        now
    }
}

/// Call strftime() using a tm struct provided in input
fn strftime(format: &str, tm: &libc::tm) -> String {
    let f = CString::new(format).unwrap();
    let buf = [0_u8; 100];
    let l: usize = unsafe { c::strftime(buf.as_ptr() as _, buf.len(), f.as_ptr() as *const _, tm) };
    std::string::String::from_utf8_lossy(&buf[..l]).to_string()
}

/// Call strftime() using the local timezone and returns a String
pub fn strftime_local(format: &str, epoch: libc::time_t) -> String {
    let tm = get_local_tm_from_epoch(epoch);
    strftime(format, &tm)
}

/// Call strftime() using GMT and returns a String
pub fn strftime_gmt(format: &str, epoch: libc::time_t) -> String {
    let tm = get_gmt_tm_from_epoch(epoch);
    strftime(format, &tm)
}

/// Call setlocale() which will initialize the locale based on the environment variables
pub fn set_locale() {
    unsafe {
        libc::setlocale(libc::LC_ALL, b"\0".as_ptr() as _);
    }
}

/// Call tzset() which will initialize the local timezone based on the environment variables
pub fn tz_set() {
    unsafe {
        #[cfg(unix)]
        c::tzset();
        #[cfg(windows)]
        c::_tzset();
    }
}

/// Retrieve the current time in epoch format (number of seconds since 1970 in UTC)
pub fn epoch() -> libc::time_t {
    unsafe { c::time(std::ptr::null()) }
}

#[cfg(test)]
mod tests {
    use crate::*;

    const EPOCH: libc::time_t = 1_565_151_596;

    #[test]
    #[cfg(unix)]
    fn format_time_and_date_in_gmt_and_cest() {
        use std::env;

        env::set_var("LC_ALL", "en_US.UTF-8");
        env::set_var("TZ", "Europe/Brussels");

        tz_set();
        set_locale();

        let gmt = strftime_gmt("%c", EPOCH);
        let local = strftime_local("%c", EPOCH);
        #[cfg(target_os = "linux")]
        assert_eq!(gmt, "Wed 07 Aug 2019 04:19:56 AM GMT");
        #[cfg(target_os = "macos")]
        assert_eq!(gmt, "Wed Aug  7 04:19:56 2019");
        #[cfg(target_os = "linux")]
        assert_eq!(local, "Wed 07 Aug 2019 06:19:56 AM CEST");
        #[cfg(target_os = "macos")]
        assert_eq!(local, "Wed Aug  7 06:19:56 2019");

        env::set_var("LC_ALL", "fr_BE.UTF-8");
        env::set_var("TZ", "Europe/Brussels");

        tz_set();
        set_locale();

        let gmt = strftime_gmt("%c", EPOCH);
        let local = strftime_local("%c", EPOCH);
        #[cfg(target_os = "linux")]
        assert_eq!(gmt, "mer 07 aoû 2019 04:19:56 GMT");
        #[cfg(target_os = "macos")]
        assert_eq!(gmt, "Mer  7 aoû 04:19:56 2019");
        #[cfg(target_os = "linux")]
        assert_eq!(local, "mer 07 aoû 2019 06:19:56 CEST");
        #[cfg(target_os = "macos")]
        assert_eq!(local, "Mer  7 aoû 06:19:56 2019");
    }

    // NOTE: I have no idea how to change the timezone or the language on
    //       Windows. It's supposed to be with the global environment variable
    //       TZ but I couldn't make it working... well, at least it returns
    //       something and it should probably work, right?
    #[test]
    #[cfg(windows)]
    fn format_time_and_date_on_windows() {
        tz_set();
        set_locale();

        let gmt = strftime_gmt("%c", EPOCH);
        let local = strftime_local("%c", EPOCH);
        #[cfg(target_os = "windows")]
        assert_eq!(gmt, "8/7/2019 4:19:56 AM");
        #[cfg(target_os = "windows")]
        assert_eq!(local, "8/7/2019 4:19:56 AM");
    }
}