EmacsConf backstage: converting timezones
| emacsconf, emacs2023-09-07: It looks like I can use Etc/GMT-2 to mean GMT+2 - note the reversed sign.
EmacsConf is a virtual conference with speakers from all over the world. We like to plan the schedule so that the speakers can come for live Q&A sessions without having to wake up too early or stay up too late.
Timezones are tricky for me. Sometimes I mess up timezone names (like the time I misspelled Tbilisi and ended up with UTC conversion) or get the timezone conversion wrong because of daylight savings time, and it's annoying to go to a website to convert the timezones.
Fortunately, the tzc package provides a way to convert times
from one timeone to another in Emacs, and it includes a list of
timezones in tzc-time-zones
loaded from /usr/share/zoneinfo
.
Here's how I use it to make organizing EmacsConf easier.
Setting the timeone with completion
To reduce data entry errors, I use completion when setting the timezone.

emacsconf-timezone-set
(defun emacsconf-timezone-set (timezone) "Set the timezone for the current Org entry." (interactive (list (progn (require 'tzc) (completing-read "Timezone: " tzc-time-zones)))) (org-entry-put (point) "TIMEZONE" timezone))
Sometimes speakers specify their timezone as an offset from GMT or
UTC, such as GMT+2
. It turns out that I can use timezones like
Etc/GMT-2
to capture that, although it's important to note that the sign for Etc/GMT timezones is reversed (so Etc/GMT-2 = GMT+2).
Converting timezones
In Toronto, we switch from daylight savings time to standard time
sometime in November, so I need to make sure that my time conversions
for speaker availability uses the date of the conference
(emacsconf-date
, 2023-12-02 this year).
emacsconf-convert-from-timezone
makes it easy to convert times on
emacsconf-date
so that I don't have to keep re-entering the date
part.

emacsconf-convert-from-timezone
(defun emacsconf-convert-from-timezone (timezone time) (interactive (list (progn (require 'tzc) (if (org-entry-get (point) "TIMEZONE") (completing-read (format "From zone (%s): " (org-entry-get (point) "TIMEZONE")) tzc-time-zones nil nil nil nil (org-entry-get (point) "TIMEZONE")) (completing-read "From zone: " tzc-time-zones nil t))) (read-string "Time: "))) (let* ((from-offset (format-time-string "%z" (date-to-time emacsconf-date) timezone)) (time (date-to-time (concat emacsconf-date "T" (string-pad time 5 ?0 t) ":00.000" from-offset)))) (message "%s = %s" (format-time-string "%b %d %H:%M %z" time timezone) (format-time-string "%b %d %H:%M %z" time emacsconf-timezone))))
Validating schedule constraints
Once I get the availability into a standard format, I can use that to
validate that sessions are scheduled during the times that speakers
have indicated that they're available. So far, I've been using text
like >= 10:00 EST
at the beginning of the talk's AVAILABILITY
property, since that's easy to parse and validate. I can use that to
colour invalid talks red in an SVG, and I can make a list of invalid
talks as well.

How does that work? First, we get the time constraint out of the
AVAILABILITY
property with emacsconf-schedule-get-time-constraint
.
emacsconf-schedule-get-time-constraint
(defun emacsconf-schedule-get-time-constraint (o) (unless (string-match "after the event" (or (plist-get o :q-and-a) "")) (let ((avail (or (plist-get o :availability) "")) hours) (when (string-match "\\([<>]\\)=? *\\([0-9]+:[0-9]+\\) *EST" avail) (if (string= (match-string 1 avail) ">") (list (match-string 2 avail) nil) (list nil (match-string 2 avail)))))))
Then we can return a warning if a talk is scheduled outside those time constraints.
emacsconf-schedule-check-time
(defun emacsconf-schedule-check-time (label o &optional from-time to-time) "FROM-TIME and TO-TIME should be strings like HH:MM in EST. Both start and end time are tested." (let* ((start-time (format-time-string "%H:%M" (plist-get o :start-time))) (end-time (format-time-string "%H:%M" (plist-get o :end-time))) result) (setq result (or (and (null o) (format "%s: Not found" label)) (and from-time (string< start-time from-time) (format "%s: Starts at %s before %s" label start-time from-time)) (and to-time (string< to-time end-time) (format "%s: Ends at %s after %s" label end-time to-time)))) (when result (plist-put o :invalid result)) result))
So then we can check all the talks as scheduled, and set the
:invalid
property if it's outside the availability constraints.
emacsconf-schedule-validate-time-constraints
(defun emacsconf-schedule-validate-time-constraints (&optional info) (interactive) (let* ((info (or info (emacsconf-get-talk-info))) (results (delq nil (append (mapcar (lambda (o) (apply #'emacsconf-schedule-check-time (car o) (emacsconf-search-talk-info (car o) info) (cdr o))) emacsconf-time-constraints) (mapcar (lambda (o) (let (result (constraint (emacsconf-schedule-get-time-constraint o))) (when constraint (setq result (apply #'emacsconf-schedule-check-time (plist-get o :slug) o constraint)) (when result (plist-put o :invalid result)) result))) info))))) (if (called-interactively-p 'any) (message "%s" (string-join results "\n")) results)))
I'll cover making the schedule SVG in another blog post. It's handy to have a quick way to check availability in both text and graphical format.
Translating schedules into local times
When we e-mail speakers their schedules, we also include a translation
to their local time if we know it. This is handled by
emacsconf-timezone-strings
, which we can use in mail templates and
on wiki pages. Here's an example of how the function works:
(string-join (emacsconf-timezone-strings '(:scheduled "<2023-12-02 Sat 09:00-09:05>") '("America/Toronto" "America/Vancouver")) "\n")
Saturday, Dec 2 2023, ~9:00 AM - 9:00 AM EST (America/Toronto) Saturday, Dec 2 2023, ~6:00 AM - 6:00 AM PST (America/Vancouver)
and here's the code for the function:
emacsconf-timezone-strings
(defun emacsconf-timezone-strings (o &optional timezones) (mapcar (lambda (tz) (emacsconf-timezone-string o tz)) (or timezones emacsconf-timezones)))
So that's how we work with timezones in EmacsConf!