1use super::tm::{
4 empty_tm, get_time_sec, init_tm_unknown, local_time_tzoffset, local_tzoffset, time_to_tm,
5 time_to_tm_local, tm_to_time_t, TzHhmm,
6};
7use libc::{time_t, tm};
8use std::ffi::CString;
9use std::io::IsTerminal;
10
11const MONTH_NAMES: [&str; 12] = [
12 "January",
13 "February",
14 "March",
15 "April",
16 "May",
17 "June",
18 "July",
19 "August",
20 "September",
21 "October",
22 "November",
23 "December",
24];
25
26const WEEKDAY_NAMES: [&str; 7] = [
27 "Sundays",
28 "Mondays",
29 "Tuesdays",
30 "Wednesdays",
31 "Thursdays",
32 "Fridays",
33 "Saturdays",
34];
35
36#[derive(Clone, Copy, PartialEq, Eq)]
37pub enum DateModeType {
38 Normal,
39 Human,
40 Relative,
41 Short,
42 Iso8601,
43 Iso8601Strict,
44 Rfc2822,
45 Strftime,
46 Raw,
47 Unix,
48}
49
50pub struct DateMode {
51 pub ty: DateModeType,
52 pub local: bool,
53 pub strftime_fmt: Option<String>,
54}
55
56impl DateMode {
57 pub fn from_type(ty: DateModeType) -> Self {
58 Self {
59 ty,
60 local: false,
61 strftime_fmt: None,
62 }
63 }
64}
65
66pub fn parse_date_format(format: &str) -> Result<DateMode, &'static str> {
67 let mut s = format;
68 if let Some(rest) = s.strip_prefix("auto:") {
69 s = if std::io::stdout().is_terminal() {
70 rest
71 } else {
72 "default"
73 };
74 }
75 if s == "local" {
76 s = "default-local";
77 }
78 let (ty, mut p) = parse_date_type(s)?;
79 let mut local = false;
80 if let Some(r) = p.strip_prefix("-local") {
81 local = true;
82 p = r;
83 }
84 let mut mode = DateMode {
85 ty,
86 local,
87 strftime_fmt: None,
88 };
89 if ty == DateModeType::Strftime {
90 let rest = p
91 .strip_prefix(':')
92 .ok_or("date format missing colon separator")?;
93 mode.strftime_fmt = Some(rest.to_string());
94 } else if !p.is_empty() {
95 return Err("unknown date format");
96 }
97 Ok(mode)
98}
99
100fn parse_date_type(s: &str) -> Result<(DateModeType, &str), &'static str> {
101 if let Some(r) = s.strip_prefix("relative") {
102 return Ok((DateModeType::Relative, r));
103 }
104 if let Some(r) = s
105 .strip_prefix("iso8601-strict")
106 .or_else(|| s.strip_prefix("iso-strict"))
107 {
108 return Ok((DateModeType::Iso8601Strict, r));
109 }
110 if let Some(r) = s.strip_prefix("iso8601").or_else(|| s.strip_prefix("iso")) {
111 return Ok((DateModeType::Iso8601, r));
112 }
113 if let Some(r) = s.strip_prefix("rfc2822").or_else(|| s.strip_prefix("rfc")) {
114 return Ok((DateModeType::Rfc2822, r));
115 }
116 if let Some(r) = s.strip_prefix("short") {
117 return Ok((DateModeType::Short, r));
118 }
119 if let Some(r) = s.strip_prefix("default") {
120 return Ok((DateModeType::Normal, r));
121 }
122 if let Some(r) = s.strip_prefix("human") {
123 return Ok((DateModeType::Human, r));
124 }
125 if let Some(r) = s.strip_prefix("raw") {
126 return Ok((DateModeType::Raw, r));
127 }
128 if let Some(r) = s.strip_prefix("unix") {
129 return Ok((DateModeType::Unix, r));
130 }
131 if let Some(r) = s.strip_prefix("format") {
132 return Ok((DateModeType::Strftime, r));
133 }
134 Err("unknown date format")
135}
136
137pub fn date_mode_release(mode: &mut DateMode) {
138 mode.strftime_fmt = None;
139}
140
141pub fn show_date_relative(time: u64, now_sec: i64) -> String {
142 let now = now_sec as i128;
143 let t = time as i128;
144 if now < t {
145 return "in the future".to_string();
146 }
147 let mut diff = (now - t) as u64;
148 if diff < 90 {
149 return if diff == 1 {
150 "1 second ago".to_string()
151 } else {
152 format!("{diff} seconds ago")
153 };
154 }
155 diff = (diff + 30) / 60;
156 if diff < 90 {
157 return if diff == 1 {
158 "1 minute ago".to_string()
159 } else {
160 format!("{diff} minutes ago")
161 };
162 }
163 diff = (diff + 30) / 60;
164 if diff < 36 {
165 return if diff == 1 {
166 "1 hour ago".to_string()
167 } else {
168 format!("{diff} hours ago")
169 };
170 }
171 diff = (diff + 12) / 24;
172 if diff < 14 {
173 return if diff == 1 {
174 "1 day ago".to_string()
175 } else {
176 format!("{diff} days ago")
177 };
178 }
179 if diff < 70 {
180 let w = (diff + 3) / 7;
181 return if w == 1 {
182 "1 week ago".to_string()
183 } else {
184 format!("{w} weeks ago")
185 };
186 }
187 if diff < 365 {
188 let m = (diff + 15) / 30;
189 return if m == 1 {
190 "1 month ago".to_string()
191 } else {
192 format!("{m} months ago")
193 };
194 }
195 if diff < 1825 {
196 let totalmonths = (diff * 12 * 2 + 365) / (365 * 2);
197 let years = totalmonths / 12;
198 let months = totalmonths % 12;
199 if months > 0 {
200 let ys = if years == 1 {
201 "1 year".to_string()
202 } else {
203 format!("{years} years")
204 };
205 return if months == 1 {
206 format!("{ys}, 1 month ago")
207 } else {
208 format!("{ys}, {months} months ago")
209 };
210 }
211 return if years == 1 {
212 "1 year ago".to_string()
213 } else {
214 format!("{years} years ago")
215 };
216 }
217 let y = (diff + 183) / 365;
218 if y == 1 {
219 "1 year ago".to_string()
220 } else {
221 format!("{y} years ago")
222 }
223}
224
225fn strbuf_rtrim(s: &mut String) {
226 while let Some(c) = s.pop() {
227 if !c.is_whitespace() {
228 s.push(c);
229 break;
230 }
231 }
232}
233
234fn show_date_normal(
235 time: u64,
236 tm: &tm,
237 tz: TzHhmm,
238 human_tm: &tm,
239 human_tz: TzHhmm,
240 local: bool,
241) -> String {
242 #[derive(Clone, Copy)]
243 struct Hide {
244 year: bool,
245 date: bool,
246 wday: bool,
247 time: bool,
248 seconds: bool,
249 tz: bool,
250 }
251 let mut hide = Hide {
252 year: false,
253 date: false,
254 wday: false,
255 time: false,
256 seconds: false,
257 tz: false,
258 };
259
260 hide.tz = local || tz == human_tz;
261 hide.year = tm.tm_year == human_tm.tm_year;
262 if hide.year && tm.tm_mon == human_tm.tm_mon {
263 if tm.tm_mday > human_tm.tm_mday {
264 } else if tm.tm_mday == human_tm.tm_mday {
266 hide.date = true;
267 hide.wday = true;
268 } else if tm.tm_mday + 5 > human_tm.tm_mday {
269 hide.date = true;
270 }
271 }
272
273 if hide.wday {
274 return show_date_relative(time, get_time_sec());
275 }
276
277 if human_tm.tm_year != 0 {
278 hide.seconds = true;
279 hide.tz |= !hide.date;
280 hide.wday = !hide.year;
281 hide.time = !hide.year;
282 }
283
284 let mut out = String::new();
285 if !hide.wday {
286 let w = WEEKDAY_NAMES[tm.tm_wday as usize].as_bytes();
287 out.push_str(std::str::from_utf8(&w[..3]).unwrap_or("Sun"));
288 out.push(' ');
289 }
290 if !hide.date {
291 let m = MONTH_NAMES[tm.tm_mon as usize].as_bytes();
292 out.push_str(std::str::from_utf8(&m[..3]).unwrap_or("Jan"));
293 out.push(' ');
294 out.push_str(&format!("{} ", tm.tm_mday));
295 }
296 if !hide.time {
297 out.push_str(&format!("{:02}:{:02}", tm.tm_hour, tm.tm_min));
298 if !hide.seconds {
299 out.push_str(&format!(":{:02}", tm.tm_sec));
300 }
301 } else {
302 strbuf_rtrim(&mut out);
303 }
304 if !hide.year {
305 out.push_str(&format!(" {}", tm.tm_year + 1900));
306 }
307 if !hide.tz {
308 out.push_str(&format!(" {:+05}", tz));
309 }
310 out
311}
312
313fn strbuf_expand_step(munged: &mut String, fmt: &mut &str) -> bool {
314 let Some(pct) = fmt.find('%') else {
315 munged.push_str(fmt);
316 *fmt = "";
317 return false;
318 };
319 munged.push_str(&fmt[..pct]);
320 *fmt = &fmt[pct + 1..];
321 true
322}
323
324pub fn strbuf_addftime(tm: &tm, tz_hhmm: TzHhmm, fmt: &str, suppress_tz_name: bool) -> String {
325 if fmt.is_empty() {
326 return String::new();
327 }
328 let mut munged = String::new();
329 let mut rest = fmt;
330 while strbuf_expand_step(&mut munged, &mut rest) {
331 if rest.starts_with('%') {
332 munged.push_str("%%");
333 rest = &rest[1..];
334 } else if rest.starts_with('s') {
335 let secs = tm_to_time_t(tm) as i64
336 - 3600 * (tz_hhmm / 100) as i64
337 - 60 * (tz_hhmm % 100) as i64;
338 munged.push_str(&format!("{secs}"));
339 rest = &rest[1..];
340 } else if rest.starts_with('z') {
341 munged.push_str(&format!("{:+05}", tz_hhmm));
342 rest = &rest[1..];
343 } else if suppress_tz_name && rest.starts_with('Z') {
344 rest = &rest[1..];
345 } else {
346 munged.push('%');
347 }
348 }
349 strftime_c(&munged, tm)
350}
351
352fn strftime_c(fmt: &str, tm: &tm) -> String {
353 let mut buf = vec![0u8; 4096];
354 let cfmt = match CString::new(fmt) {
355 Ok(c) => c,
356 Err(_) => CString::new("%Y").unwrap(),
357 };
358 unsafe {
359 let n = libc::strftime(
360 buf.as_mut_ptr() as *mut libc::c_char,
361 buf.len(),
362 cfmt.as_ptr(),
363 tm,
364 );
365 if n == 0 && !fmt.is_empty() {
366 let mut munged = fmt.to_string();
367 munged.push(' ');
368 let c2 = CString::new(munged.as_str()).unwrap();
369 let n2 = libc::strftime(
370 buf.as_mut_ptr() as *mut libc::c_char,
371 buf.len(),
372 c2.as_ptr(),
373 tm,
374 );
375 if n2 > 0 {
376 return String::from_utf8_lossy(&buf[..n2 - 1]).into_owned();
377 }
378 }
379 String::from_utf8_lossy(&buf[..n]).into_owned()
380 }
381}
382
383pub fn show_date(time: u64, mut tz: TzHhmm, mode: &mut DateMode) -> String {
384 if mode.ty == DateModeType::Unix {
385 return format!("{time}");
386 }
387 let mut tmbuf = init_tm_unknown();
388 let mut human_tm = empty_tm();
389 let mut human_tz: TzHhmm = -1;
390
391 if mode.ty == DateModeType::Human {
392 let now = get_time_sec();
393 unsafe {
394 human_tz = local_time_tzoffset(now as time_t, &mut human_tm);
395 }
396 }
397
398 if mode.local {
399 tz = local_tzoffset(time);
400 }
401
402 if mode.ty == DateModeType::Raw {
403 return format!("{time} {:+05}", tz);
404 }
405
406 if mode.ty == DateModeType::Relative {
407 return show_date_relative(time, get_time_sec());
408 }
409
410 let mut tz = tz;
411 let ok = if mode.local {
412 unsafe { time_to_tm_local(time, &mut tmbuf).is_some() }
413 } else {
414 unsafe { time_to_tm(time, tz, &mut tmbuf).is_some() }
415 };
416 if !ok {
417 unsafe {
418 time_to_tm(0, 0, &mut tmbuf);
419 }
420 tz = 0;
421 }
422
423 match mode.ty {
424 DateModeType::Short => format!(
425 "{:04}-{:02}-{:02}",
426 tmbuf.tm_year + 1900,
427 tmbuf.tm_mon + 1,
428 tmbuf.tm_mday
429 ),
430 DateModeType::Iso8601 => format!(
431 "{:04}-{:02}-{:02} {:02}:{:02}:{:02} {:+05}",
432 tmbuf.tm_year + 1900,
433 tmbuf.tm_mon + 1,
434 tmbuf.tm_mday,
435 tmbuf.tm_hour,
436 tmbuf.tm_min,
437 tmbuf.tm_sec,
438 tz
439 ),
440 DateModeType::Iso8601Strict => {
441 let mut s = format!(
442 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
443 tmbuf.tm_year + 1900,
444 tmbuf.tm_mon + 1,
445 tmbuf.tm_mday,
446 tmbuf.tm_hour,
447 tmbuf.tm_min,
448 tmbuf.tm_sec
449 );
450 if tz == 0 {
451 s.push('Z');
452 } else {
453 let sign = if tz >= 0 { '+' } else { '-' };
454 let a = tz.abs();
455 s.push(sign);
456 s.push_str(&format!("{:02}:{:02}", a / 100, a % 100));
457 }
458 s
459 }
460 DateModeType::Rfc2822 => format!(
461 "{}, {} {} {} {:02}:{:02}:{:02} {:+05}",
462 &WEEKDAY_NAMES[tmbuf.tm_wday as usize][..3],
463 tmbuf.tm_mday,
464 &MONTH_NAMES[tmbuf.tm_mon as usize][..3],
465 tmbuf.tm_year + 1900,
466 tmbuf.tm_hour,
467 tmbuf.tm_min,
468 tmbuf.tm_sec,
469 tz
470 ),
471 DateModeType::Strftime => {
472 let fmt = mode.strftime_fmt.as_deref().unwrap_or("");
473 strbuf_addftime(&tmbuf, tz, fmt, !mode.local)
474 }
475 _ => show_date_normal(time, &tmbuf, tz, &human_tm, human_tz, mode.local),
476 }
477}