DEV Community

muro
muro

Posted on

"YYYY-MM-DD" to Time in Common Lisp

Disclaimer:
The purpose of this post is to track my journey into Common Lisp. Though there might exist more optimal ways, this represents my personal path.


While querying an API, I encountered dates formatted as strings in the "YYYY-MM-DD" format (for instance, "2023-01-21"), prompting me to seek a method to transform these into a format conducive to date comparisons.

Let's break it down step by step to get to the final solution, shall we?

First up, we're going to tackle that date string by splitting it into its parts: the year (yup, the 'YYYY' bit), the month (the 'MM' bit), and the day (you guessed it, the 'DD' part).

So, we've got our example date string: '2023-01-21'. Now, let's snag the 'YYYY' part. According to the Common Lisp Hyperspec (CLHS), there's a handy function that lets us pull out a chunk of a sequence when we tell it where to start and stop. In our scenario, that sequence is our date string.

From the CLHS:
subseq creates a sequence that is a copy of the subsequence of sequence bounded by start and end.

So, we're going to apply subseq to our date string. Since the year part stretches from positions 0 to 4, we just feed those numbers into our function like this:

(subseq "2023-01-21" 0 4) ;;; returns "2023"
Enter fullscreen mode Exit fullscreen mode

Great, we're on the right track!

Now, it's key to note that our result, "2023", comes back as a string. To transform it into an integer, we can use the parse-integer function. This step will convert the string "2023" into an actual numeric value, making it ready for any date comparisons or calculations we might want to perform later on.
Let's now enclose our snippet with parse-integer to perform the conversion:

(parse-integer (subseq "2023-01-21" 0 4)) ;;; returns 2023 (now it's an integer)
Enter fullscreen mode Exit fullscreen mode

With our strategy in place, we're ready to define a function named parse-date. This function will extract and return three pieces of information from our date string: the year, the month, and the day:

(defun parse-date (date-string)
  "Extracts year, month, and day from a date string formatted as YYYY-MM-DD."
  (let ((year (parse-integer (subseq date-string 0 4)))
        (month (parse-integer (subseq date-string 5 7)))
        (day (parse-integer (subseq date-string 8 10))))
    (values year month day)))
Enter fullscreen mode Exit fullscreen mode

Now, let's transform the day, month, and year values into a universal time format, enabling us to perform comparisons between different dates.

Luckily, Common Lisp offers the encode-universal-time function, capable of transforming our values into universal time. As outlined in the CLHS, to use this function effectively, we should supply the seconds, minutes, hour, day, month, and year as arguments, precisely in that sequence.
Given that we lack the specifics for seconds, minutes, and hours, we'll substitute zeroes for these values instead.

;;; Let's convert 2023-01-21:
(encode-universal-time 0 0 0 21 1 23) ;; returns 3883233600
;;; Let's convert 2023-01-22:
(encode-universal-time 0 0 0 22 1 23) ;; returns 3883320000
Enter fullscreen mode Exit fullscreen mode

Let's create a function that turns a date string straight into universal time. We're going to use our parse-date function here. Remember, parse-date gives us three things: the year, the month, and the day. We'll grab these three using something called multiple-value-bind, and then use them to get everything set up for the encode-universal-time function:

(defun date-string-to-universal-time (date-string)
  "Converts a date string in the format 'YYYY-MM-DD' to universal time."
  (multiple-value-bind (year month day) (parse-date date-string)
    (encode-universal-time 0 ; second
                           0 ; minute
                           0 ; hour
                           day
                           month
                           year))) 
Enter fullscreen mode Exit fullscreen mode

Now, we're equipped to perform date comparisons right from the date string, let's write an example:

(> (date-string-to-universal-time "2023-01-21")
     (date-string-to-universal-time "2023-01-22"))
;; returns NIL
Enter fullscreen mode Exit fullscreen mode
(< (date-string-to-universal-time "2023-01-21")
     (date-string-to-universal-time "2023-01-22"))
;; returns T
Enter fullscreen mode Exit fullscreen mode

There you have it. See you next time!

Top comments (4)

Collapse
 
7stud profile image
7stud • Edited

The function encode-universal-time doesn't give me the same results as you. Your results (shown in the comments):

(encode-universal-time 0 0 0 21 1 23) ;; returns 3883233600
(encode-universal-time 0 0 0 22 1 23) ;; returns 3883320000
Enter fullscreen mode Exit fullscreen mode

My results (with SBCL 2.4.0):

CL-USER> (encode-universal-time 0 0 0 21 1 23)
3883273200
CL-USER> (encode-universal-time 0 0 0 22 1 23)
3883359600
Enter fullscreen mode Exit fullscreen mode

And, I get yet a third result when I use an online lisp compiler:

(format t "~d~%" (encode-universal-time 0 0 0 21 1 2023))  ;; => 3883248000
(format t "~d~%" (encode-universal-time 0 0 0 22 1 2023))  ;; => 3883334400
Enter fullscreen mode Exit fullscreen mode

I'm not sure why, but changing the year from 23 to 2023 produces the same results:

CL-USER> (encode-universal-time 0 0 0 21 1 2023)
3883273200
CL-USER> (encode-universal-time 0 0 0 22 1 2023)
3883359600
Enter fullscreen mode Exit fullscreen mode

There's nothing in the hyperspec about that. How does encode-universal-time know that 23 means 2023 and not 1923? Edit: The hyperspec says:

A decoded time is an ordered series of nine values that, taken together, represent a point in calendar time (ignoring leap seconds)
...

...

Year
An integer indicating the year A.D. However, if this integer is between 0 and 99, the "obvious" year is used; more precisely, that year is assumed that is equal to the integer modulo 100 and within fifty years of the current year (inclusive backwards and exclusive forwards). Thus, in the year 1978, year 28 is 1928 but year 27 is 2027. (Functions that return time in this format always return a full year number.)

Edit: I think the different results have to do with timezones. According to the hyperspec for encode-universal-time:

If time-zone is supplied, no adjustment for daylight savings time is performed.

That implies that there is some adjustment if you don't supply a timezone? I tried suppling a timezone of 0, which I think is GMT:

CL-USER> (encode-universal-time 0 0 0 21 1 2023 0)
3883248000

CL-USER> (encode-universal-time 0 0 0 22 1 2023 0)
3883334400
Enter fullscreen mode Exit fullscreen mode

With a timezone of 0, I got the same numbers using the online lisp interpreter.

Collapse
 
vindarel profile image
vindarel

Oh, on reddit we have the idea:

(parse-integer "2023-01-21" :end 4)
Enter fullscreen mode Exit fullscreen mode
Collapse
 
mrmuro profile image
muro

Thanks @vindarel !, should that simplify my parse-date function? something like:

(defun parse-date (date-string)
  "Extracts year, month, and day from a date string formatted as YYYY-MM-DD."
  (let ((year (parse-integer date-string :start 0 :end 4))
        (month (parse-integer date-string :start 5 :end 7))
        (day (parse-integer date-string :start 8 :end 10)))
    (values year month day))) 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
vindarel profile image
vindarel • Edited

Well done. To add a handy library in:

(local-time:parse-timestring "2042-11-13")
;; => @2042-11-13T01:00:00.000000+01:00
Enter fullscreen mode Exit fullscreen mode