Skip to main content

zsh/
datetime.rs

1//! Date/time utilities - port of Modules/datetime.c
2//!
3//! Provides strftime builtin and EPOCHSECONDS/EPOCHREALTIME/epochtime parameters.
4
5use chrono::{DateTime, Local, NaiveDateTime, TimeZone, Utc};
6use std::time::{Duration, SystemTime, UNIX_EPOCH};
7
8/// Get current time as epoch seconds
9pub fn epoch_seconds() -> i64 {
10    SystemTime::now()
11        .duration_since(UNIX_EPOCH)
12        .unwrap_or(Duration::ZERO)
13        .as_secs() as i64
14}
15
16/// Get current time as high-resolution epoch time (float)
17pub fn epoch_realtime() -> f64 {
18    let now = SystemTime::now()
19        .duration_since(UNIX_EPOCH)
20        .unwrap_or(Duration::ZERO);
21    now.as_secs() as f64 + now.subsec_nanos() as f64 * 1e-9
22}
23
24/// Get current time as [seconds, nanoseconds] array
25pub fn epoch_time() -> (i64, i64) {
26    let now = SystemTime::now()
27        .duration_since(UNIX_EPOCH)
28        .unwrap_or(Duration::ZERO);
29    (now.as_secs() as i64, now.subsec_nanos() as i64)
30}
31
32/// Format time using strftime-style format
33pub fn strftime(
34    format: &str,
35    timestamp: Option<i64>,
36    nanoseconds: Option<i64>,
37) -> Result<String, String> {
38    let (secs, nanos) = if let Some(ts) = timestamp {
39        (ts, nanoseconds.unwrap_or(0))
40    } else {
41        let (s, n) = epoch_time();
42        (s, n)
43    };
44
45    let dt: DateTime<Local> = match Local.timestamp_opt(secs, nanos as u32) {
46        chrono::LocalResult::Single(dt) => dt,
47        chrono::LocalResult::Ambiguous(dt, _) => dt,
48        chrono::LocalResult::None => return Err("unable to convert to time".to_string()),
49    };
50
51    let mut result = format.to_string();
52
53    result = result.replace("%%", "\x00");
54    result = result.replace("%Y", &dt.format("%Y").to_string());
55    result = result.replace("%y", &dt.format("%y").to_string());
56    result = result.replace("%m", &dt.format("%m").to_string());
57    result = result.replace("%d", &dt.format("%d").to_string());
58    result = result.replace("%H", &dt.format("%H").to_string());
59    result = result.replace("%M", &dt.format("%M").to_string());
60    result = result.replace("%S", &dt.format("%S").to_string());
61    result = result.replace("%j", &dt.format("%j").to_string());
62    result = result.replace("%w", &dt.format("%w").to_string());
63    result = result.replace("%u", &dt.format("%u").to_string());
64    result = result.replace("%U", &dt.format("%U").to_string());
65    result = result.replace("%W", &dt.format("%W").to_string());
66    result = result.replace("%a", &dt.format("%a").to_string());
67    result = result.replace("%A", &dt.format("%A").to_string());
68    result = result.replace("%b", &dt.format("%b").to_string());
69    result = result.replace("%B", &dt.format("%B").to_string());
70    result = result.replace("%c", &dt.format("%c").to_string());
71    result = result.replace("%x", &dt.format("%x").to_string());
72    result = result.replace("%X", &dt.format("%X").to_string());
73    result = result.replace("%p", &dt.format("%p").to_string());
74    result = result.replace("%P", &dt.format("%P").to_string());
75    result = result.replace("%Z", &dt.format("%Z").to_string());
76    result = result.replace("%z", &dt.format("%z").to_string());
77    result = result.replace("%e", &dt.format("%e").to_string());
78    result = result.replace("%k", &dt.format("%k").to_string());
79    result = result.replace("%l", &dt.format("%l").to_string());
80    result = result.replace("%n", "\n");
81    result = result.replace("%t", "\t");
82    result = result.replace("%s", &secs.to_string());
83
84    result = result.replace("%N", &format!("{:09}", nanos));
85    result = result.replace("%.N", &format!(".{:09}", nanos));
86    result = result.replace("%3N", &format!("{:03}", nanos / 1_000_000));
87    result = result.replace("%6N", &format!("{:06}", nanos / 1_000));
88    result = result.replace("%9N", &format!("{:09}", nanos));
89
90    result = result.replace('\x00', "%");
91
92    Ok(result)
93}
94
95/// Parse a time string using strptime-style format
96pub fn strptime(format: &str, input: &str) -> Result<i64, String> {
97    let dt = NaiveDateTime::parse_from_str(input, format)
98        .map_err(|e| format!("format not matched: {}", e))?;
99
100    let local = Local.from_local_datetime(&dt);
101    match local {
102        chrono::LocalResult::Single(dt) => Ok(dt.timestamp()),
103        chrono::LocalResult::Ambiguous(dt, _) => Ok(dt.timestamp()),
104        chrono::LocalResult::None => Err("unable to convert to time".to_string()),
105    }
106}
107
108/// Options for strftime builtin
109#[derive(Debug, Default)]
110pub struct StrftimeOptions {
111    pub no_newline: bool,
112    pub quiet: bool,
113    pub reverse: bool,
114    pub scalar: Option<String>,
115}
116
117/// Execute the strftime builtin
118pub fn builtin_strftime(args: &[&str], options: &StrftimeOptions) -> (i32, String) {
119    if args.is_empty() {
120        return (1, "strftime: format expected\n".to_string());
121    }
122
123    let format = args[0];
124
125    if options.reverse {
126        if args.len() < 2 {
127            return (1, "strftime: timestring expected\n".to_string());
128        }
129
130        match strptime(format, args[1]) {
131            Ok(timestamp) => {
132                if options.scalar.is_some() {
133                    (0, timestamp.to_string())
134                } else {
135                    (0, format!("{}\n", timestamp))
136                }
137            }
138            Err(e) => {
139                if options.quiet {
140                    (1, String::new())
141                } else {
142                    (1, format!("strftime: {}\n", e))
143                }
144            }
145        }
146    } else {
147        let timestamp = if args.len() > 1 {
148            match args[1].parse::<i64>() {
149                Ok(ts) => Some(ts),
150                Err(_) => {
151                    return (
152                        1,
153                        format!("strftime: {}: invalid decimal number\n", args[1]),
154                    )
155                }
156            }
157        } else {
158            None
159        };
160
161        let nanoseconds = if args.len() > 2 {
162            match args[2].parse::<i64>() {
163                Ok(ns) if ns >= 0 && ns <= 999_999_999 => Some(ns),
164                Ok(_) => {
165                    return (
166                        1,
167                        format!("strftime: {}: invalid nanosecond value\n", args[2]),
168                    )
169                }
170                Err(_) => {
171                    return (
172                        1,
173                        format!("strftime: {}: invalid decimal number\n", args[2]),
174                    )
175                }
176            }
177        } else {
178            None
179        };
180
181        match strftime(format, timestamp, nanoseconds) {
182            Ok(result) => {
183                let output = if options.no_newline || options.scalar.is_some() {
184                    result
185                } else {
186                    format!("{}\n", result)
187                };
188                (0, output)
189            }
190            Err(e) => (1, format!("strftime: {}\n", e)),
191        }
192    }
193}
194
195/// Format a duration in human-readable form
196pub fn format_duration(seconds: u64) -> String {
197    let days = seconds / 86400;
198    let hours = (seconds % 86400) / 3600;
199    let mins = (seconds % 3600) / 60;
200    let secs = seconds % 60;
201
202    if days > 0 {
203        format!("{}d {}h {}m {}s", days, hours, mins, secs)
204    } else if hours > 0 {
205        format!("{}h {}m {}s", hours, mins, secs)
206    } else if mins > 0 {
207        format!("{}m {}s", mins, secs)
208    } else {
209        format!("{}s", secs)
210    }
211}
212
213/// Get current date/time info as a hashmap (for TZ-aware operations)
214pub fn get_datetime_info() -> std::collections::HashMap<String, String> {
215    let now = Local::now();
216    let mut info = std::collections::HashMap::new();
217
218    info.insert("year".to_string(), now.format("%Y").to_string());
219    info.insert("month".to_string(), now.format("%m").to_string());
220    info.insert("day".to_string(), now.format("%d").to_string());
221    info.insert("hour".to_string(), now.format("%H").to_string());
222    info.insert("minute".to_string(), now.format("%M").to_string());
223    info.insert("second".to_string(), now.format("%S").to_string());
224    info.insert("weekday".to_string(), now.format("%A").to_string());
225    info.insert("monthname".to_string(), now.format("%B").to_string());
226    info.insert("timezone".to_string(), now.format("%Z").to_string());
227    info.insert("offset".to_string(), now.format("%z").to_string());
228    info.insert("epoch".to_string(), now.timestamp().to_string());
229    info.insert(
230        "iso8601".to_string(),
231        now.format("%Y-%m-%dT%H:%M:%S%z").to_string(),
232    );
233
234    info
235}
236
237/// Convert between timezones
238pub fn convert_timezone(timestamp: i64, to_utc: bool) -> i64 {
239    if to_utc {
240        let dt: DateTime<Local> = Local
241            .timestamp_opt(timestamp, 0)
242            .single()
243            .unwrap_or_else(|| Local::now());
244        dt.with_timezone(&Utc).timestamp()
245    } else {
246        let dt: DateTime<Utc> = Utc
247            .timestamp_opt(timestamp, 0)
248            .single()
249            .unwrap_or_else(Utc::now);
250        dt.with_timezone(&Local).timestamp()
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn test_epoch_seconds() {
260        let secs = epoch_seconds();
261        assert!(secs > 1700000000);
262    }
263
264    #[test]
265    fn test_epoch_realtime() {
266        let rt = epoch_realtime();
267        assert!(rt > 1700000000.0);
268
269        let (secs, _) = epoch_time();
270        assert!((rt - secs as f64).abs() < 1.0);
271    }
272
273    #[test]
274    fn test_epoch_time() {
275        let (secs, nanos) = epoch_time();
276        assert!(secs > 1700000000);
277        assert!(nanos >= 0 && nanos < 1_000_000_000);
278    }
279
280    #[test]
281    fn test_strftime_basic() {
282        let result = strftime("%Y-%m-%d", Some(1700000000), None).unwrap();
283        assert!(result.contains("-"));
284
285        let result = strftime("%%", None, None).unwrap();
286        assert_eq!(result, "%");
287    }
288
289    #[test]
290    fn test_strftime_nanoseconds() {
291        let result = strftime("%N", Some(1700000000), Some(123456789)).unwrap();
292        assert_eq!(result, "123456789");
293
294        let result = strftime("%3N", Some(1700000000), Some(123456789)).unwrap();
295        assert_eq!(result, "123");
296    }
297
298    #[test]
299    fn test_strftime_epoch() {
300        let result = strftime("%s", Some(1700000000), None).unwrap();
301        assert_eq!(result, "1700000000");
302    }
303
304    #[test]
305    fn test_format_duration() {
306        assert_eq!(format_duration(45), "45s");
307        assert_eq!(format_duration(90), "1m 30s");
308        assert_eq!(format_duration(3661), "1h 1m 1s");
309        assert_eq!(format_duration(90061), "1d 1h 1m 1s");
310    }
311
312    #[test]
313    fn test_builtin_strftime() {
314        let (status, output) = builtin_strftime(&["%s"], &StrftimeOptions::default());
315        assert_eq!(status, 0);
316        assert!(!output.is_empty());
317
318        let (status, _) = builtin_strftime(&[], &StrftimeOptions::default());
319        assert_eq!(status, 1);
320    }
321
322    #[test]
323    fn test_builtin_strftime_with_timestamp() {
324        let (status, output) = builtin_strftime(&["%s", "1700000000"], &StrftimeOptions::default());
325        assert_eq!(status, 0);
326        assert!(output.contains("1700000000"));
327    }
328
329    #[test]
330    fn test_get_datetime_info() {
331        let info = get_datetime_info();
332        assert!(info.contains_key("year"));
333        assert!(info.contains_key("epoch"));
334        assert!(info.contains_key("iso8601"));
335    }
336}