- Common Lisp 100%
| src | ||
| tests | ||
| format-seconds-tests.asd | ||
| format-seconds.asd | ||
| README.org | ||
| UNLICENSE | ||
| WTFPL | ||
format-seconds
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 -
- As with Common Lisp's
format, there is an additionaldestinationparameter, 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~Ddirective. - The
%zdirective 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-durationis 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:durationinstances.
Differences from humanize-duration -
- Durations may be formatted as years (365 days) or months (30 days). The largest unit supported by
humanize-durationis 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:durationinstances.
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
- Support for microseconds
- Support for using
format's~Rdirective instead of~D. - Support for multiple languages.
- Optimization
Contributions and contact
Feedback and MRs are very welcome. 🙂
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)
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.
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"