Port of Elisp format-seconds to Common Lisp
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
contrapunctus 4c34aa72cc Add thanks 2 months ago
README.org Add thanks 2 months ago
UNLICENSE Create README, add licenses, write comparisons 2 months ago
WTFPL Create README, add licenses, write comparisons 2 months ago
format-seconds-tests.asd Implement plural units; write tests 2 months ago
format-seconds.asd Use helper function, extend units 2 months ago
format-seconds.org Ensure tests use default units 2 months ago

README.org

format-seconds

Donate using Liberapay

Explanation

format-seconds is a Common Lisp system to format seconds as an integer into human-readable duration strings, based on a control string similar to format. It tries to provide the equivalent of format-seconds from Emacs Lisp, but adopts Common Lisp conventions wherever possible.

 * (format-seconds nil
                "I've been waiting ~Y, ~W, ~D, and oh...~M to fix what he did to me."
                33869640)
 "I've been waiting 1 year, 3 weeks, 6 days, and oh...14 minutes to fix what he did to me."

To see more of what it can do, have a look at the tutorial.

Comparison with prior art

This library has the following differences compared to Elisp's format-seconds -

  1. As with Common Lisp's format, there is an additional destination parameter, and control string directives begin with ~ rather than %,
  2. Durations may be formatted as months (30 days) and weeks (7 days). The Elisp version supports years and days, and nothing in between.
  3. Directives can contain any prefix parameters acceptable to format's ~D directive.
  4. The %z directive is planned, but not yet implemented.

Differences from local-time-duration:human-readable-duration">local-time-duration:human-readable-duration -

  1. Durations may be formatted as years (365 days) or months (30 days). The largest unit supported by local-time-duration:human-readable-duration is weeks.
  2. The user controls the output string and the units used, and whether or not unit names are emitted.
  3. Consumes integer seconds rather than local-time-duration:duration instances.

Differences from humanize-duration -

  1. Durations may be formatted as years (365 days) or months (30 days). The largest unit supported by humanize-duration is weeks.
  2. Control over output string is provided by a format-like control string, rather than a list.
  3. Consumes integer seconds rather than local-time-duration:duration instances.

The month unit

The month unit is rife with issues. It all depends on how you define it.

A month is 30 days

You could define a month as 30 days, which is the default. That works well for most situations.

(let* ((minute 60)
       (hour   (* 60 minute))
       (day    (* 24 hour))
       (week   (* 7 day))
       (year   (* 365 day)))
  (list
   (format-seconds nil "~O, ~W, ~D" (+ year
                                       (* 3 week)
                                       (* 4 day)
                                       (* 23 hour)
                                       (* 59 minute)
                                       59))
   (format-seconds nil "~O, ~W, ~D" (+ year (* 3 week)))))
;; =>
("13 months, 0 weeks, 0 days"
 "12 months, 3 weeks, 5 days")

Why is that 13 months, seeing as we supplied 12? Where do those mysterious 5 days come from?

The problem is that if a month is defined as 30 days and a year as 365 days, a year has 12 months and 5 days. 5 days + 3 weeks (21 days) + 4 days = 30 days, resulting in an extra month. The same 5 days manifest in the second example.

A month is 1/12th of a year

Alternatively, you could define a month as 1/12th of a year -

(setq *units*
      (let* ((second (make-unit "second" "s" 1))
             (minute (make-unit "minute" "m" 60))
             (hour   (make-unit "hour" "h" (* 60 60)))
             (day    (make-unit "day"  "d" (* 24 60 60)))
             (week   (make-unit "week" "w" (* 7 24 60 60)))
             (year   (make-unit "year" "y" (* 365 24 60 60)))
             (month  (make-unit "month" "o" (/ (seconds year) 12))))
        (list year month week day hour minute second)))

The earlier examples now return -

("12 months, 3 weeks, 4 days"
 "12 months, 3 weeks, 0 days")

Any test code using this definition of months must define months in input durations as (/ year 12), or you'll get unexpected results.

(let ((day (* 24 60 60)))
  (format-seconds nil "~Y, ~O" (+ (* 2 365 day)
                                  (* 2 30 day))))
"2 years, 1 month" ;; ouch

(let* ((day   (* 24 60 60))
       (year  (* 365 day))
       (month (/ year 12)))
  (format-seconds nil "~Y, ~O" (+ (* 2 year) (* 2 month))))
"2 years, 2 months" ;; whew

Remove months entirely

A third option could be to remove support for months entirely.

(setq *units*
      (loop for (name directive seconds) in
            `(("year" "y"   ,(* 365 24 60 60))
              ("week" "w"   ,(* 7 24 60 60))
              ("day" "d"    ,(* 24 60 60))
              ("hour" "h"   ,(* 60 60))
              ("minute" "m" 60)
              ("second" "s" 1))
            collect (make-unit name directive seconds)))

Possible future improvements

  1. Support for using format's ~R directive instead of ~D.
  2. Support for multiple languages.
  3. Optimization

Contributions and contact

Feedback and MRs are very welcome. 🙂

Get in touch with the author and other Emacs users in the Emacs Jabber/XMPP channel - xmpp:emacs@salas.suchat.org?join (web chat)

(For help in getting started with Jabber, click here)

License

I'd like for all software to be liberated - transparent, trustable, and accessible for anyone to use, study, or improve.

I'd like anyone using my software to credit me for the work.

I'd like to receive financial support for my efforts, so I can spend all my time doing what I find meaningful.

But I don't want to make demands or threats (e.g. via legal conditions) to accomplish all that, nor restrict my services to only those who can pay.

Thus, format-seconds is released under your choice of Unlicense or the WTFPL.

(See files UNLICENSE and WTFPL).

Thanks

acdw, hdasch, Zash, gilberth, and moonchild, for discussing the behavior of the library.

Tutorial

Let's set up our session. Enter the following forms into your REPL. (The * represents the REPL prompt and is not meant to be entered.)

 * (ql:quickload :format-seconds)
 * (use-package :format-seconds)
 * (defvar *year* (* 365 24 60 60))
 * (defvar *day* (* 24 60 60))

The destination parameter works like it does in format. For instance, pass T as destination to print to *standard-output* -

 * (format-seconds t "~Y" *year*)
 1 year
 NIL

Or pass NIL as destination to return a string. Note the pluralization based on the provided duration.

 * (format-seconds nil "~Y" 0)
 "0 years"
 * (format-seconds nil "~Y" *year*)
 "1 year"
 * (format-seconds nil "~Y" (* 2 *year*))
 "2 years"

To omit unit strings, you can use lower-case directives -

 * (format-seconds nil "~yy ~om" (* 60 24 60 60))
 "0y 2m"

Any prefix parameters acceptable to format's ~D directive can be used. Let's left-pad the durations with zeroes -

 * (format-seconds nil "~2,'0h:~2,'0m:~2,'0s" (* 2 *day*))
 "48:00:00"