Easy way to input date time in Common Lisp
Most date time libraries require a strict input format when parsing string to date time object. As libraries, it maybe a reasonable design. But when using command line tool which asks for a date time input, I definitely don't want to input a RFC 3399 date time like '2024-12-19T15:20:00'. Command line tools need to accept date time input in a more tolerant format.
Recently I wrote a command line tool, which takes date time as an argument. I'd like to share how it handles date time input. It is too simple to become a library. I just explain the idea, and paste the code here.
The whole idea is, find a sensible way to let user specify the most concerned parts, and complement other parts from a default timestamp.
Suppose at current moment the time is "2024-12-18T09:05:00+08:00", all valid formats it accepts and their means are:
10 -> 2024-12-18T10:00:00+08:00 13:20 -> 2024-12-18T13:20:00+08:00 17:01:38 -> 2024-12-18T17:01:38+08:00 21 14 -> 2024-12-21T14:00:00+08:00 5 13:20 -> 2024-12-05T13:20:00+08:00 8-3 8:30 -> 2024-08-03T08:30:00+08:00 2019-12-21 14:15:20 -> 2019-12-21T14:15:20+08:00
- It splits the input string at space: 1) if there is only one part, take it as 'time'. 2) if there are two splits, the first one is 'date', and the second one is 'time'.
- Components of 'date' are taken in the order: day, month, and year. Which means that, if there is only one number, it is taken as 'day'.
- Components or time are taken in the order: hour, minute, and second. So if there is only one number, it is taken as 'hour'.
That's it. Time to show the code::
(defmacro with-gensyms (syms &body body) `(let ,(loop for s in syms collect `(,s (gensym))) ,@body)) (defmacro tagged-parts (str sep tags min max &key from-left) (with-gensyms (gstr gsep gtags gmin gmax gparts gparts-len gtags-len) `(let ((,gstr ,str) (,gsep ,sep) (,gtags ,tags) (,gmin ,min) (,gmax ,max)) (let* ((,gparts (uiop:split-string ,gstr :separator ,gsep)) (,gparts-len (length ,gparts))) (unless (<= ,gmin ,gparts-len ,gmax) (error "str ~a separated into ~d parts which are not between ~d and ~d" ,gstr ,gparts-len ,gmin, gmax)) (let ((,gtags-len (length ,gtags))) (unless (>= ,gtags-len ,gparts-len) (error "~d tags for ~d parts are not enough" ,gtags-len ,gparts-len)) (loop for part in ,gparts ,@(if from-left `(for tag-i from 0 upto (1- ,gtags-len)) `(for tag-i from (- ,gtags-len ,gparts-len) upto (1- ,gtags-len))) append (list (aref ,gtags tag-i) (parse-integer part)))))))) (defun parse-date-time (str) (flet ((parse-date (date) (tagged-parts date '(#\-) #(:year :month :day) 1 3)) (parse-time (time) (tagged-parts time '(#\:) #(:hour :minute :second) 1 3 :from-left t))) (let ((parts (remove-if #'(lambda (part) (zerop (length part))) (uiop:split-string str)))) (case (length parts) (2 (append (parse-date (car parts)) (parse-time (cadr parts)))) (1 (parse-time (car parts))) (otherwise (error "Invalid datetime: ~a" str))))))
The function 'parse-date-time' splits input string and tags each part as we described earlier. For example:
(parse-date-time "10-12 13") ;; outputs (:month 10 :day 12 :hour 13)
Then it should be straightforward to complement the missing parts using a date time library you choose. Here is an example using 'local-time' library:
(defun string->timestamp (timestring) (let ((tagged-parts (parse-date-time timestring))) (local-time:with-decoded-timestamp (:hour hour :day day :month month :year year) (local-time:now) (local-time:encode-timestamp 0 (getf tagged-parts :second 0) (getf tagged-parts :minute 0) (getf tagged-parts :hour hour) (getf tagged-parts :day day) (getf tagged-parts :month month) (getf tagged-parts :year year)))))
About 50 lines of code, we have a sensible way to accept date time input in command line.
The code in this blog is in Public Domain, feel free to do whatever you need.
---------------------
Published on 2024-12-19
The content for this site is licensed under: