EmacsConf backstage: converting timezones

| emacsconf, emacs

2023-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.

output-2023-09-06-10:06:28.gif
Figure 1: 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.

output-2023-09-06-10:09:17.gif
Figure 2: Converting from a timezone

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.

output-2023-09-06-10:58:41.gif
Figure 3: Validating time constraints in a draft schedule

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!

You can comment with Disqus or you can e-mail me at sacha@sachachua.com.