format-seconds
2025-06-22
No Description
<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'sformat-seconds
-- As with Common Lisp's
format
, there is an additionaldestination
parameter, and control string directives begin with~
rather than%
, - Durations may be formatted as months (30 days) and weeks (7 days). The Elisp version supports years and days, and nothing in between.
- Directives can contain any prefix parameters acceptable to
format
's~D
directive. - The
%z
directive is planned, but not yet implemented.
Differences from local-time-duration:human-readable-duration
-
- 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. - The user controls the output string and the units used, and whether or not unit names are emitted.
- Consumes integer seconds rather than
local-time-duration:duration
instances.
Differences from humanize-duration -
- Durations may be formatted as years (365 days) or months (30 days). The largest unit supported by
humanize-duration
is weeks. - Control over output string is provided by a
format
-like control string, rather than a list. - 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:- Support for microseconds
- Support for using
format
's~R
directive instead of~D
. - Support for multiple languages.
- Optimization
1.4Contributions and contact
:PROPERTIES::CUSTOM_ID: contributions-contact:END:Feedback and MRs are very welcome. ὤ2Get 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"