1use super::compat::{self, time_t, tm};
4use super::tm::{
5 empty_tm, get_time_sec, init_tm_unknown, local_time_tzoffset, local_tzoffset, time_to_tm,
6 time_to_tm_local, tm_to_time_t, TzHhmm,
7};
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') && tz_hhmm == 0 {
344 munged.push_str("UTC");
345 rest = &rest[1..];
346 } else if suppress_tz_name && rest.starts_with('Z') {
347 rest = &rest[1..];
348 } else {
349 munged.push('%');
350 }
351 }
352 strftime_c(&munged, tm)
353}
354
355fn strftime_c(fmt: &str, tm: &tm) -> String {
356 let mut buf = vec![0u8; 4096];
357 let cfmt = match CString::new(fmt) {
358 Ok(c) => c,
359 Err(_) => match CString::new("%Y") {
361 Ok(c) => c,
362 Err(_) => return String::new(),
363 },
364 };
365 unsafe {
366 let n = compat::strftime(
367 buf.as_mut_ptr() as *mut std::ffi::c_char,
368 buf.len(),
369 cfmt.as_ptr(),
370 tm,
371 );
372 if n == 0 && !fmt.is_empty() {
373 let mut munged = fmt.to_string();
374 munged.push(' ');
375 if let Ok(c2) = CString::new(munged.as_str()) {
376 let n2 = compat::strftime(
377 buf.as_mut_ptr() as *mut std::ffi::c_char,
378 buf.len(),
379 c2.as_ptr(),
380 tm,
381 );
382 if n2 > 0 {
383 return String::from_utf8_lossy(&buf[..n2 - 1]).into_owned();
384 }
385 }
386 }
387 String::from_utf8_lossy(&buf[..n]).into_owned()
388 }
389}
390
391pub fn show_date(time: u64, mut tz: TzHhmm, mode: &mut DateMode) -> String {
392 if mode.ty == DateModeType::Unix {
393 return format!("{time}");
394 }
395 let mut tmbuf = init_tm_unknown();
396 let mut human_tm = empty_tm();
397 let mut human_tz: TzHhmm = -1;
398
399 if mode.ty == DateModeType::Human {
400 let now = get_time_sec();
401 unsafe {
402 human_tz = local_time_tzoffset(now as time_t, &mut human_tm);
403 }
404 }
405
406 if mode.local {
407 tz = local_tzoffset(time);
408 }
409
410 if mode.ty == DateModeType::Raw {
411 return format!("{time} {:+05}", tz);
412 }
413
414 if mode.ty == DateModeType::Relative {
415 return show_date_relative(time, get_time_sec());
416 }
417
418 let mut tz = tz;
419 let ok = if mode.local {
420 unsafe { time_to_tm_local(time, &mut tmbuf).is_some() }
421 } else {
422 unsafe { time_to_tm(time, tz, &mut tmbuf).is_some() }
423 };
424 if !ok {
425 unsafe {
426 time_to_tm(0, 0, &mut tmbuf);
427 }
428 tz = 0;
429 }
430
431 match mode.ty {
432 DateModeType::Short => format!(
433 "{:04}-{:02}-{:02}",
434 tmbuf.tm_year + 1900,
435 tmbuf.tm_mon + 1,
436 tmbuf.tm_mday
437 ),
438 DateModeType::Iso8601 => format!(
439 "{:04}-{:02}-{:02} {:02}:{:02}:{:02} {:+05}",
440 tmbuf.tm_year + 1900,
441 tmbuf.tm_mon + 1,
442 tmbuf.tm_mday,
443 tmbuf.tm_hour,
444 tmbuf.tm_min,
445 tmbuf.tm_sec,
446 tz
447 ),
448 DateModeType::Iso8601Strict => {
449 let mut s = format!(
450 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
451 tmbuf.tm_year + 1900,
452 tmbuf.tm_mon + 1,
453 tmbuf.tm_mday,
454 tmbuf.tm_hour,
455 tmbuf.tm_min,
456 tmbuf.tm_sec
457 );
458 if tz == 0 {
459 s.push('Z');
460 } else {
461 let sign = if tz >= 0 { '+' } else { '-' };
462 let a = tz.abs();
463 s.push(sign);
464 s.push_str(&format!("{:02}:{:02}", a / 100, a % 100));
465 }
466 s
467 }
468 DateModeType::Rfc2822 => format!(
469 "{}, {} {} {} {:02}:{:02}:{:02} {:+05}",
470 &WEEKDAY_NAMES[tmbuf.tm_wday as usize][..3],
471 tmbuf.tm_mday,
472 &MONTH_NAMES[tmbuf.tm_mon as usize][..3],
473 tmbuf.tm_year + 1900,
474 tmbuf.tm_hour,
475 tmbuf.tm_min,
476 tmbuf.tm_sec,
477 tz
478 ),
479 DateModeType::Strftime => {
480 let fmt = mode.strftime_fmt.as_deref().unwrap_or("");
481 strbuf_addftime(&tmbuf, tz, fmt, !mode.local)
482 }
483 _ => show_date_normal(time, &tmbuf, tz, &human_tm, human_tz, mode.local),
484 }
485}