Dateutil¶
https://dateutil.readthedocs.io/en/stable/
The dateutil module provides powerful extensions to the standard datetime module, available in Python.
Features¶
- Computing of relative deltas (next month, next year, next monday, last week of month, etc);
- Computing of relative deltas between two given date and/or datetime objects;
- Computing of dates based on very flexible recurrence rules, using a superset of the iCalendar specification. Parsing of RFC strings is supported as well.
- Generic parsing of dates in almost any string format;
- Timezone (tzinfo) implementations for tzfile(5) format files (/etc/localtime, /usr/share/zoneinfo, etc), TZ environment string (in all known formats), iCalendar format files, given ranges (with help from relative deltas), local machine timezone, fixed offset timezone, UTC timezone, and Windows registry-based time zones.
- Internal up-to-date world timezone information based on Olson?s database.
- Computing of Easter Sunday dates for any given year, using Western, Orthodox or Julian algorithms.
CL alternatives¶
Python to CL examples¶
https://dateutil.readthedocs.io/en/stable/examples.html
Similary to Python let’s start with importing required libraries:
(ql:quickload :local-time)
(ql:quickload :local-time-duration)
(use-package :local-time)
Store some values:
(defparameter *now* (now))
(defparameter *today* (today))
(list *now* *today*)
Next month
(timestamp+ *now* 1 :month)
;; or
(adjust-timestamp *now* (offset :month 1))
@2020-04-24T10:48:48\.317722Z
Next month, plus one week
(adjust-timestamp *now* (offset :month 1) (offset :day 7))
@2020-04-30T10:48:48\.317722Z
Next month, plus one week, at 10am.
(adjust-timestamp *now* (offset :month 1) (offset :day 7) (set :hour 10))
@2020-04-30T10:48:48\.317722Z
Setting specific time fields similar to absolute relativedelta:
(adjust-timestamp *now* (set :year 1) (set :month 1))
@0001-01-24T10:48:48\.317722Z
Get the relative delta
(ltd:timestamp-difference (encode-timestamp 0 0 0 0 1 1 2018) *now*)
#<LOCAL-TIME-DURATION:DURATION [-813/-38928/-317722000] -116 weeks -1 days -10 hours -48 minutes -48 seconds -317722000 nsecs>
One month before one year.
(adjust-timestamp *now* (offset :year 1) (offset :month -1))
@2021-02-24T10:48:48\.317722Z
How does it handle months with different numbers of days? Notice that adding one month will never cross the month boundary.
(adjust-timestamp (encode-timestamp 0 0 0 0 27 1 2003) (offset :month 1))
@2003-02-27T00:00:00\.000000Z
(adjust-timestamp (encode-timestamp 0 0 0 0 31 1 2003) (offset :month 1))
@2003-02-28T00:00:00\.000000Z
(adjust-timestamp (encode-timestamp 0 0 0 0 31 1 2003) (offset :month 2))
@2003-03-31T00:00:00\.000000Z
The logic for years is the same, even on leap years.
(adjust-timestamp (encode-timestamp 0 0 0 0 28 2 2000) (offset :year 1))
@2001-02-28T00:00:00\.000000Z
(adjust-timestamp (encode-timestamp 0 0 0 0 29 2 2000) (offset :year 1))
@2001-02-28T00:00:00\.000000Z
(adjust-timestamp (encode-timestamp 0 0 0 0 28 2 1999) (offset :year 1))
@2000-02-28T00:00:00\.000000Z
(adjust-timestamp (encode-timestamp 0 0 0 0 1 3 1999) (offset :year 1))
(adjust-timestamp (encode-timestamp 0 0 0 0 28 2 2001) (offset :year -1))
@2000-02-28T00:00:00\.000000Z
(adjust-timestamp (encode-timestamp 0 0 0 0 1 3 2001) (offset :year -1))
@2000-03-01T00:00:00\.000000Z
Next Friday
(adjust-timestamp *today* (offset :day-of-week :friday))
@2020-03-27T00:00:00\.000000Z
Last Friday of the month
(defun set-day-of-week (time day-of-week)
"Adjust the timestamp to be the specifed day of the week, selects corresponding preceeding date if timestamp's day of the week do not match the requirement."
(let ((adjusted (adjust-timestamp time (offset :day-of-week day-of-week))))
(if (timestamp>= time adjusted)
adjusted
(adjust-timestamp adjusted (offset :day -7)))))
(set-day-of-week (timestamp-maximize-part *today* :day) :friday)
@2020-03-27T23:59:59\.999999Z
Next Wednesday (it’s today!)
(defun next-day-of-week (time day-of-week)
"Adjust the timestamp to be the next specifed day of the week, selects corresponding future date if timestamp's day of the week do not match the requirement."
(let ((adjusted (adjust-timestamp time (offset :day-of-week day-of-week))))
(if (timestamp>= adjusted time)
adjusted
(adjust-timestamp adjusted (offset :day 7)))))
(let ((*today* (encode-timestamp 0 0 0 0 3 1 2018)))
(next-day-of-week *today* :wednesday))
@2018-01-03T00:00:00\.000000Z
Next wednesday, but not today.
(let ((*today* (encode-timestamp 0 0 0 0 3 1 2018)))
(next-day-of-week (adjust-timestamp *today* (offset :day 1)) :wednesday))
@2018-01-10T00:00:00\.000000Z
Following ISO year week number notation find the first day of the 15th week of 1997.
(set-day-of-week
(adjust-timestamp
(next-day-of-week
(encode-timestamp 0 0 0 0 1 1 1997)
:thursday)
(offset :day (* 7 14)))
:monday)
@1997-04-07T00:00:00\.000000Z
How long ago has the millennium changed?
(ltd:timestamp-difference *now* (encode-timestamp 0 0 0 0 1 1 2001))
#<LOCAL-TIME-DURATION:DURATION [7022/38928/317722000] 1003 weeks 1 day 10 hours 48 minutes 48 seconds 317722000 nsecs>
It works with dates too.
(ltd:timestamp-difference *today* (encode-timestamp 0 0 0 0 1 1 2001))
#<LOCAL-TIME-DURATION:DURATION [7022/0/0] 1003 weeks 1 day>
Obtain a date using the yearday:
(adjust-timestamp (timestamp-minimize-part *now* :day) (offset :day 260))
@2020-11-16T00:00:00\.000000Z
Leap year vs non-leap year:
(let ((leap (encode-timestamp 0 0 0 0 1 1 2000))
(non-leap (encode-timestamp 0 0 0 0 1 1 2002)))
(list (adjust-timestamp (timestamp-minimize-part leap :day) (offset :day 260))
(adjust-timestamp (timestamp-minimize-part non-leap :day) (offset :day 260))))