format-seconds

2025-06-22

No Description

Upstream URL

License

Not determined

README
format-seconds

<a href="https://liberapay.com/contrapunctus/donate"> <img alt="Donate using Liberapay" src="https://img.shields.io/liberapay/receives/contrapunctus.svg?logo=liberapay"> </a>

1Explanation

:PROPERTIES::CUSTOM_ID: explanation:END: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.

1.1Comparison with prior art

:PROPERTIES::CUSTOM_ID: differences-from-emacs-lisp:END: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 -

  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.

1.2The month unit

:PROPERTIES::CUSTOM_ID: month-unit:END:The month unit is rife with issues. It all depends on how you define it.

1.2.1A month is 30 days

:PROPERTIES::CUSTOM_ID: month-is-30-days:END: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.

1.2.2A month is 1/12th of a year

:PROPERTIES::CUSTOM_ID: month-is-1-12th-of-year:END: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

1.2.3Remove months entirely

:PROPERTIES::CUSTOM_ID: remove-months-entirely:END: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)))

1.3Possible future improvements

:PROPERTIES::CUSTOM_ID: possible-future-improvements:END:
  1. Support for microseconds
  2. Support for using format's ~R directive instead of ~D.
  3. Support for multiple languages.
  4. Optimization

1.4Contributions and contact

:PROPERTIES::CUSTOM_ID: contributions-contact:END:Feedback and MRs are very welcome. ὤ2

Get in touch with the author and other Lisp users the Lisp Jabber/XMPP channel - xmpp:lisp@conference.a3.pm?join (web chat)

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

1.5License

:PROPERTIES::CUSTOM_ID: license:END: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).

1.6Thanks

:PROPERTIES::CUSTOM_ID: thanks:END:acdw, hdasch, Zash, gilberth, and moonchild, for discussing the behavior of the library.

2Tutorial

:PROPERTIES::CUSTOM_ID: tutorial:END:

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"

Dependencies (0)

    Dependents (0)

      • GitHub
      • Quicklisp