Elisp: Parsing Date Time

By Xah Lee. Date: . Last updated: .

This page shows you how to parse date time string.

If you are only interested in printing current date time in various formats, see: Emacs Lisp Date Time Formats.

Problem

Write a elisp function. The function will take a string argument that's any of common date time format, example

and output a canonical form 2011-09-02T11:14:11+0200.

Solution

Using parse-time-string

You can use parse-time-string, from the file parse-time.el. With feature name (require 'parse-time).

parse-time-string is a compiled Lisp function in `parse-time.el'.

(parse-time-string STRING)

Parse the time-string STRING into (SEC MIN HOUR DAY MON YEAR DOW DST TZ).
The values are identical to those of `decode-time', but any values that are
unknown are returned as nil.

Supported Formats of parse-time-string

;; testing for supported formats for “parse-time-string”
;; As of 2016-07-05 GNU Emacs 25.0.90.1
(require 'parse-time)

;; unixy format
(equal
 (parse-time-string "Date: Mon, 01 Aug 2011 12:24:51 -0400")
 '(51 24 12 1 8 2011 1 nil -14400))
;; yes

;; format reminder
;; (SEC MIN HOUR DAY MON YEAR DOW DST TZ)

;; unixy format
(equal
 (parse-time-string "Local: Mon, Aug 1 2011 9:24 am")
 '(0 24 9 1 8 2011 1 nil nil))
;; yes

;; with English month names
(equal
 (parse-time-string "2007, August 1")
 '(nil nil nil 1 8 2007 nil nil nil))
;; yes

(equal
 (parse-time-string "August 1, 2007")
 '(nil nil nil 1 8 2007 nil nil nil))
;; yes

(equal
 (parse-time-string "august 1, 2007")
 '(nil nil nil 1 8 2007 nil nil nil))
;; yes. Lowercase ok.

(equal
 (parse-time-string "August 1st, 2007")
 '(nil nil nil nil 8 2007 nil nil nil))
 ;; no. The date is nil.

(equal
 (parse-time-string "aug 1, 2007")
 '(nil nil nil 1 8 2007 nil nil nil))
 ;; yes. Month abbr OK.

(equal
 (parse-time-string "1 aug, 2007")
 '(nil nil nil 1 8 2007 nil nil nil))
 ;; yes
;; testing for supported formats for “parse-time-string”
;; As of 2016-07-05 GNU Emacs 25.0.90.1
(require 'parse-time)

;; test USA convention-like formats

(equal
 (parse-time-string "8/1/2007")
 '(nil nil nil 8 nil 2001 nil nil nil))
 ;; no. Takes the 8 as date, 1 as nil

;; (SEC MIN HOUR DAY MON YEAR DOW DST TZ)

(equal
 (parse-time-string "08/01/2007")
 '(nil nil nil 8 nil 2001 nil nil nil))
 ;; no. Takes the 8 as date, 1 as nil

(equal
 (parse-time-string "8,1,2007")
 '(nil nil nil 8 nil 2001 nil nil nil))
 ;; no
;; testing for supported formats for “parse-time-string”
;; As of 2016-07-05 GNU Emacs 25.0.90.1
(require 'parse-time)

;; test ISO 8601 formats

(equal
 (parse-time-string "2007-08-01")
 '(nil nil nil 1 8 2007 nil nil nil))
 ;; yes

;; (SEC MIN HOUR DAY MON YEAR DOW DST TZ)

(equal
 (parse-time-string "2007")
 '(nil nil nil nil nil 2007 nil nil nil))
 ;; yes

(equal
 (parse-time-string "2007-08")
 '(nil nil nil nil nil nil nil nil nil))
 ;; no. got nothing

(equal
 (parse-time-string "2011-08-01T11:55:37-07:00")
 '(nil nil nil nil nil nil nil nil nil))
 ;; no. got nothing

“parse-time-string” does not understand the following formats:

Datetime Parser Function

The simplest solution is just do a regex match on the form. I don't need the time info, so it makes the problem slightly simpler. Here's my code:

(defun xah-fix-datetime-stamp (@input-string &optional @begin-end)
  "Change timestamp under cursor into a yyyy-mm-dd format.
If there's a text selection, use that as input, else use current line.

Any “day of week”, or “time” info, or any other parts of the string, are discarded.
For example:
 「TUESDAY, FEB 15, 2011 05:16 ET」 ⇒ 「2011-02-15」
 「November 28, 1994」              ⇒ 「1994-11-28」
 「Nov. 28, 1994」                  ⇒ 「1994-11-28」
 「11/28/1994」                     ⇒ 「1994-11-28」
 「1994/11/28」                     ⇒ 「1994-11-28」

When called in lisp program, the optional second argument “*begin-end” is a vector of region boundary. (it can also be a list)
If “*begin-end” is non-nil, the region is taken as input (and “*input-string” is ignored).

URL `http://ergoemacs.org/emacs/elisp_parse_time.html'
Version 2015-04-14"

(interactive
   (list nil (vector (line-beginning-position) (line-end-position))))

  (let (
        ($str (if @begin-end (buffer-substring-no-properties (elt @begin-end 0) (elt @begin-end 1)) @input-string))
        ($work-on-region-p (if @begin-end t nil)))
    (require 'parse-time)

    (setq $str (replace-regexp-in-string "^ *\\(.+\\) *$" "\\1" $str)) ; remove white spaces

    (setq $str
          (cond
           ;; USA convention of mm/dd/yyyy
           ((string-match "\\([0-9][0-9]\\)/\\([0-9][0-9]\\)/\\([0-9][0-9][0-9][0-9]\\)" $str)
            (concat (match-string 3 $str) "-" (match-string 1 $str) "-" (match-string 2 $str)))
           ;; USA convention of m/dd/yyyy
           ((string-match "\\([0-9]\\)/\\([0-9][0-9]\\)/\\([0-9][0-9][0-9][0-9]\\)" $str)
            (concat (match-string 3 $str) "-0" (match-string 1 $str) "-" (match-string 2 $str)))

           ;; USA convention of mm/dd/yy
           ((string-match "\\([0-9][0-9]\\)/\\([0-9][0-9]\\)/\\([0-9][0-9]\\)" $str)
            (concat (format-time-string "%C") (match-string 3 $str) "-" (match-string 1 $str) "-" (match-string 2 $str)))
           ;; USA convention of m/dd/yy
           ((string-match "\\([0-9]\\)/\\([0-9][0-9]\\)/\\([0-9][0-9]\\)" $str)
            (concat (format-time-string "%C") (match-string 3 $str) "-0" (match-string 1 $str) "-" (match-string 2 $str)))

           ;; yyyy/mm/dd
           ((string-match "\\([0-9][0-9][0-9][0-9]\\)/\\([0-9][0-9]\\)/\\([0-9][0-9]\\)" $str)
            (concat (match-string 1 $str) "-" (match-string 2 $str) "-" (match-string 3 $str)))

           ;; some ISO 8601. yyyy-mm-ddThh:mm
           ((string-match "\\([0-9][0-9][0-9][0-9]\\)-\\([0-9][0-9]\\)-\\([0-9][0-9]\\)T[0-9][0-9]:[0-9][0-9]" $str)
            (concat (match-string 1 $str) "-" (match-string 2 $str) "-" (match-string 3 $str)))
           ;; some ISO 8601. yyyy-mm-dd
           ((string-match "\\([0-9][0-9][0-9][0-9]\\)-\\([0-9][0-9]\\)-\\([0-9][0-9]\\)" $str)
            (concat (match-string 1 $str) "-" (match-string 2 $str) "-" (match-string 3 $str)))
           ;; some ISO 8601. yyyy-mm
           ((string-match "\\([0-9][0-9][0-9][0-9]\\)-\\([0-9][0-9]\\)" $str)
            (concat (match-string 1 $str) "-" (match-string 2 $str)))

           ;; else
           (t
            (progn
              (setq $str (replace-regexp-in-string "January " "Jan. " $str))
              (setq $str (replace-regexp-in-string "February " "Feb. " $str))
              (setq $str (replace-regexp-in-string "March " "Mar. " $str))
              (setq $str (replace-regexp-in-string "April " "Apr. " $str))
              (setq $str (replace-regexp-in-string "May " "May. " $str))
              (setq $str (replace-regexp-in-string "June " "Jun. " $str))
              (setq $str (replace-regexp-in-string "July " "Jul. " $str))
              (setq $str (replace-regexp-in-string "August " "Aug. " $str))
              (setq $str (replace-regexp-in-string "September " "Sep. " $str))
              (setq $str (replace-regexp-in-string "October " "Oct. " $str))
              (setq $str (replace-regexp-in-string "November " "Nov. " $str))
              (setq $str (replace-regexp-in-string "December " "Dec. " $str))

              (setq $str (replace-regexp-in-string "\\([0-9]+\\)st" "\\1" $str))
              (setq $str (replace-regexp-in-string "\\([0-9]+\\)nd" "\\1" $str))
              (setq $str (replace-regexp-in-string "\\([0-9]+\\)rd" "\\1" $str))
              (setq $str (replace-regexp-in-string "\\([0-9]\\)th" "\\1" $str))

              (let (dateList $year $month $date $yyyy $mm $dd )
                (setq dateList (parse-time-string $str))
                (setq $year (nth 5 dateList))
                (setq $month (nth 4 dateList))
                (setq $date (nth 3 dateList))

                (setq $yyyy (number-to-string $year))
                (setq $mm (if $month (format "%02d" $month) "" ))
                (setq $dd (if $date (format "%02d" $date) "" ))
                (concat $yyyy "-" $mm "-" $dd))))))

    (if $work-on-region-p
        (progn (delete-region  (elt @begin-end 0) (elt @begin-end 1))
               (insert $str))
      $str )))

This code looks big but is very easy to understand. The function takes a string, and returns a string.

The whole code is just one giant conditional test.

(cond
 (TEST2 BODY)
 (TEST2 BODY)
 …
 )

In the code, the first few tests are regex match of forms like nn/nn/nnnn where each “n” is a digit. When any of these match, then basically i got what i want, and the code exits.

When none of these match, then it goes to the end of the test (t BODY), where the “t” there is always true, and run a big chunk of BODY. In the BODY, first i replace each full spelling of month names by their abbrev using replace-regexp-in-string, example

(setq $str (replace-regexp-in-string "January " "Jan. " $str))

This is done because in emacs 22 the parse-time-string doesn't understand fully spelled month names. (this has been fixed in 23.2.1 or earlier.)

Then, i also replace {1st, 2nd, nth} etc by {1, 2, n}, because emacs's parse-time-string doesn't understand those. Then, i simply feed it to parse-time-string and get a parsed date time as a list. After that, just extract the elements from the list and reformat the way i want using format.

Liket it? Put $5 at patreon. Or Buy Xah Emacs Tutorial. Thanks.