【分享】Elisp 生成人性化的时间格式

在有些情况下,我们需要的不是准确的时间,而是更加人性化的时间。即当前的时间(或指定一个时间)距离过去的特定时间经过了多久,就像论坛每个帖子的“活跃” 一栏。

用法

使用 user-friendly-time 函数。第一个参数是过去的某个时间。第二个参数为 t 是得到更精确的时间,即当前时间单位向下再精确一个单位。第三个参数默认是当前的时间,用于得到过去时间到现在时间的差,也可以指定一个特定的时间。

例子

(user-friendly-time (date-to-time "2021-05-02 15:28:33"))

(user-friendly-time
 (date-to-time "2021-05-02 15:28:33") t) ;; 最后一个参数默认是当前时间。

(user-friendly-time
 (date-to-time "2021-05-02 15:28:33") t
 (date-to-time "2021-05-02 15:30:00")) ;; => 1 minute 27 seconds

(user-friendly-time
 (date-to-time "2021-05-01 15:28:33") nil
 (date-to-time "2021-05-01 15:30:00")) ;; => 1 minute

代码

有类似需求的,欢迎取用。

点击展开代码
(defvar uft-unit-secs
  `(("second" . 1)
    ("minute" . 60)
    ("hour" . ,(* 60 60))
    ("day" . ,(* 60 60 24))
    ("month" . ,(* 60 60 24 30.5))
    ("year" . ,(* 60 60 24 365.5))))

(defun uft--grammar-correct (string)
  "When the number in STRING is '1', make time unit
a singular form.  Otherwise, keep the original format."
  (when string
    (let* ((lst (split-string string " "))
           (unit-num (car lst))
           (unit-name (cadr lst)))
      (unless (string= unit-num "1")
        (setq unit-name (concat unit-name "s")))
      (concat unit-num " " unit-name))))

(defun uft--accurater (unit-name interval accurater)
  "Accurate to the lower level time unit."
  (pcase unit-name
    ("second" (if accurater
                  (uft--grammar-correct
                   (concat (number-to-string interval) " " unit-name))
                "just now"))
    (_
     (let* ((units uft-unit-secs)
            (unit-secs (floor (cdr (assoc unit-name units))))
            (unit-num (/ interval unit-secs))
            (unit-str (uft--grammar-correct
                       (format "%s %s" unit-num unit-name)))
            (lower-unit
             (nth (1- (seq-position units (assoc unit-name units))) units))
            (lower-unit-name (car lower-unit))
            (lower-unit-secs (cdr lower-unit))
            (lower-unit-num (floor (/ (% interval unit-secs) lower-unit-secs)))
            (lower-unit-str (uft--grammar-correct
                             (unless (= lower-unit-num 0)
                               (format "%s %s" lower-unit-num
                                       lower-unit-name)))))
       (if (and accurater lower-unit-str)
           (concat unit-str " " lower-unit-str)
         unit-str)))))

(defun user-friendly-time (specified-time &optional accurater latest-time)
  "Return a user-friendly string of SPECIFIED-TIME.

SPECIFIED-TIME is a time value to convert to float.
See ‘format-time-string’ for the various forms of a time value.

If the optional argument ACCURATER is non-nil, return the accurater
format of string.  The optional argument LATEST-TIME is current-time
by default."
  (let* ((latest-time (or latest-time (current-time)))
         (passed-sec (floor (time-to-seconds specified-time)))
         (curr-sec (floor (time-to-seconds latest-time)))
         interval)
    (if (> passed-sec curr-sec)
        (error "the specified-time shouldn't be later than current time!")
      (setq interval (- curr-sec passed-sec))
      (pcase interval
        ((pred (> 60))
         (uft--accurater "second" interval accurater))
        ((and (pred (<= 60))
              (pred (> (* 60 60))))
         (uft--accurater "minute" interval accurater))
        ((and (pred (<= (* 60 60)))
              (pred (> (* 60 60 24))))
         (uft--accurater "hour" interval accurater))
        ((and (pred (<= (* 60 60 24)))
              (pred (> (* 60 60 24 30.5))))
         (uft--accurater "day" interval accurater))
        ((and (pred (<= (* 60 60 24 30.5)))
              (pred (> (* 60 60 24 365.5))))
         (uft--accurater "month" interval accurater))
        ((pred (<= (* 60 60 24 365.5)))
         (uft--accurater "year" interval accurater))))))

欢迎大佬指点一下代码,能否写得更简洁?

2赞

elisp 的没有写过,不过之前做 ipython 的 autotime 扩展写过一个精确到纳秒(应该很简单就能转过来?

K = 1000
G = K * K * K
M = 60
H = M * M
D = H * 24
Y = 365
YD = D * Y


def duration(ns: int) -> str:
    """Calculate human readable time string."""
    return ", ".join(
        map(
            lambda nu: f"{nu[0]}{nu[1]}",
            filter(
                lambda nu: nu[0] > 0,
                zip(
                    map(  # noqa: WPS317
                        lambda d, m: (ns // d) % m,
                        (YD * G, D * G, H * G, M * G, G, K * K, K, 1),
                        (sys.maxsize, Y, 24, 60, 60, K, K, K),
                    ),
                    ("y", "d", "h", "m", "s", "ms", "µs", "ns"),
                ),
            ),
        )
    )
1赞
(defun duration (t1 &optional t2)
  "Calculate human readable time string.

T1 start time
T2 end time, default (current-time)

The original Python version is provided by @nasy
https://emacs-china.org/t/elisp/17173/2"
  (let* ((ns (car (time-convert
                   (time-subtract (or t2 (current-time)) t1)
                   1000000000)))
         (K 1000)
         (G (* K K K))
         (M 60)
         (H (* M M))
         (D (* H 24))
         (Y 365)
         (YD (* D Y))
         (units '("y" "d" "h" "m" "s" "ms" "µs" "ns"))
         (times (mapcar (pcase-lambda (`(,d ,m))
                          (% (/ ns d) m))
                        (list (list (* YD G) 9223372036854775807)
                              (list (* D G)  Y)
                              (list (* H G)  24)
                              (list (* M G)  60)
                              (list G        60)
                              (list (* K K)  K)
                              (list K        K)
                              (list 1        K))))
         result)
    (mapconcat #'identity 
               (remove nil
                 (cl-mapcar (lambda (t u)
                              (when (not (zerop t))
                                (format "%s%s" t u)))
                            times units))
               ", ")))
(duration (date-to-time "2021-05-01 00:00:00 123"))
;; => "1d, 21h, 35m, 14s, 398ms, 59µs"
4赞

cool~ 学到了

好评(紫薯布丁

第二版,增加显示精度参数:

(defun duration2 (t1 &optional t2 accurate)
  "Calculate human readable time string.

T1 start time
T2 end time, default (current-time)
ACCURATE smallest unit, default \"ns\".

The original Python version is provided by @nasy
https://emacs-china.org/t/elisp/17173/2"
  (let* ((ns (car (time-convert
                   (time-subtract (or t2 (current-time)) t1)
                   1000000000)))
         (K 1000)
         (G (* K K K))
         (M 60)
         (H (* M M))
         (D (* H 24))
         (Y 365)
         (YD (* D Y))
         (units '("y" "d" "h" "m" "s" "ms" "µs" "ns"))
         (aclen (1- (length (member (or accurate "ns") units))))
         (times (mapcar (pcase-lambda (`(,d ,m))
                          (% (/ ns d) m))
                        (butlast
                         (list (list (* YD G) 9223372036854775807)
                               (list (* D G)  Y)
                               (list (* H G)  24)
                               (list (* M G)  60)
                               (list G        60)
                               (list (* K K)  K)
                               (list K        K)
                               (list 1        K))
                         aclen)))
         result)
    (mapconcat #'identity 
               (remove nil
                       (cl-mapcar (lambda (t u)
                                    (when (not (zerop t))
                                      (format "%s%s" t u)))
                                  times units))
               ", ")))
(duration2 (date-to-time "2021-05-01 00:00:00 123"))
;; => "2d, 9h, 4m, 41s, 494ms, 915µs"

(duration2 (date-to-time "2021-05-01 00:00:00 123") nil "s")
;; => "2d, 9h, 4m, 49s"
1赞
(defun user-friendly-time (specified-time &optional accurater latest-time)
  (let* ((string (format-seconds "%y year %d day %h hour %m minute %s second%z"
                                 (time-subtract
                                  (or latest-time (current-time))
                                  specified-time)))
         (n (if accurater 4 2)))
    (mapconcat #'identity  (seq-take (split-string string) n) " ")))
2赞

啊,我好蠢,我怎么没有想到直接 format 时间差。