Next: Introduction, Previous: (dir), Up: (dir) [Contents]
Welcome to the PERIODS library. The intention of this code is to provide a convenient set of utilities for manipulating times, distances between times, and both contiguous and discontiguous ranges of time. By combining these facilities in various ways, almost any type of time expression is possible.
• Introduction | ||
• Installation | ||
• Fixed time | ||
• Time durations | ||
• Relative time | ||
• Time ranges | ||
• Time periods |
Next: Installation, Previous: Top, Up: Top [Contents]
Consider you are writing a calendaring application which must
support the idea of recurring tasks. Often, people using a calendar
have very certain ideas of what type of recurrence they want—but it
is not always easy to calculate. Say they are entering their pay
days; this could either fall bi-weekly, the second Friday of every
month, or every 15 days, but the following Monday if that day falls on
a weekend. How is your code to calculate these moments in time,
without resorting to contortionist uses of encode-universal-time
and decode-universal-time
?
By way of a brief introduction to the following sections, here is how each of the above time expressions would be calculated. The first three occurrences of each are shown, starting from ‘Sun, 18 Nov 2007’. This code generates a list of dates occurring bi-weekly, or every 14 days, until next year:
;; The date is written with (local-time:enable-read-macros)
(list-times @2007-11-18 (duration :days 14) (next-year))
;; ⇒
@2007-12-02T01:00:00.000000+01:00
@2007-12-16T01:00:00.000000+01:00
@2007-12-30T01:00:00.000000+01:00
@2008-01-13T01:00:00.000000+01:00
…
And this code, using the system :period-series
, generates an unbounded series occurring bi-weekly,
or every 14 days:
(scan-times @2007-11-18 (duration :days 14))
⇒ #Z(@2007-12-02 @2007-12-16 @2007-12-30 …)
This next example is a bit more involved, as it unifies the idea
of duration
-based time stepping (monthly) with
relative-time
. The result is an unbounded sequence
representing the second Friday of every month.
(mapping ((time (scan-times (previous-time @2007-11-18
(relative-time :day 1))
(duration :months 1))))
(next-time (next-time time (relative-time :day-of-week 5)
:accept-anchor t)
(relative-time :day-of-week 5)))
⇒ #Z(@2007-12-14 @2008-01-11 @2008-02-08 …)
To read this example briefly: Beginning with the first day
of the current month, advance forward one month ad infinitum.
For each new month, scan forward to the first Friday (accepting the
first day if it is Friday, with :accept-anchor
). Then scan to
the Friday after that.
In the next example, we want to step through time every 15 days, but select the following Monday if the 15th day falls on a weekend.
(mapping ((time (scan-times @2007-11-03 (duration :days 15))))
(if (falls-on-weekend-p time)
(next-time time (relative-time :day-of-week 1))
time))
⇒ #Z(@2007-11-19 @2007-12-03 @2007-12-18 …)
The starting time for this example is set to ‘3 Nov 2007’, so that Monday-shifting could be demonstrated. Note how the shift does not disrupt the regular 15-day cycle, it merely causes the paycheck to be delivered one day late in that instance.
Hopefully this gives an idea of the power and flexibility of the periods library, especially if used in combination with the SERIES library1, since times are naturally expressable as an unbounded series supporting lazy calculation.
Next: Fixed time, Previous: Introduction, Up: Top [Contents]
The PERIODS library depends on two required dependencies:
If you also wish to use the SERIES library with PERIODS, it must be installed as well, version 2.2.9 or later.
The recommended way to install the library is to use Quicklisp, which installs the dependencies automatically.
Here is how to load the basic PERIODS library:
* (ql:quickload :periods) |
Loading the series-enabled PERIODS functions:
* (ql:quickload :period-series) |
Next: Time durations, Previous: Installation, Up: Top [Contents]
The most basic element of time used by the PERIODS library is the
fixed-time
structure. At present, this is just a type alias
for local-time
structures2, so all of the
operations possible on a local-time
are applicable to a
fixed-time
. In addition, fixed-time
’s may be
constructed by a convenience function of the same name, which allows
for quickly creating time structures anchored in the current year.
Return a fixed point in time relative to the time of the call. args
is a
property list giving a specific precision for the return value.
If the keyword argument :now
is given, all else is ignored; this is
equivalent to calling local-time:now
.
Otherwise, any keyword arguments given override their corresponding elements in the current time. Further, any elements smaller in resolution than the finest specified element are reduced to 0 or 1, according to their position.
For example, assuming the current time is "@2007-11-17T23:02:00.000", compare these outputs:
(fixed-time :month 4) ;; ⇒ @2007-04-01T00:00:00.000
(fixed-time :day 10) ;; ⇒ @2007-11-10T00:00:00.000
(fixed-time :hour 15) ;; ⇒ @2007-11-17T15:00:00.000
This behavior makes it very easy to return a fixed time for "april of this
year", etc. If you wish to determine the date of the previous April, while
preserving the current day of the month, hour of the day, etc., then see the
function previous-time
.
• Helper functions |
Previous: Fixed time, Up: Fixed time [Contents]
There are a few helper functions for performing common operations on
fixed-time
’s:
Return the corresponding detail associated with a given
fixed-time
. All results are of type fixnum
.
Return the day of the week associated with a given fixed-time
.
The result is a fixnum
with 0 representing Sunday, through 6 on Saturday.
Return t
if the given fixed-time
occurs on a Saturday or Sunday.
Return the current year as a fixnum
.
Return t
if year
falls on a leap year.
Returns the number of days in the given month of the specified year.
Reduce a fixed time to be no finer than resolution
.
For example, if the date is 2007-04-20, and the resolution is :month, the date is floored to 2007-04-01. Anything smaller than the resolution is reduced to zero (or 1, if it is a day or month being reduced).
Examine the property list step-by
and return the smallest unit of time
specified.
For example, given the following property list:
(:DAY 10 :HOUR 5 :MINUTE 2)
The result is :minute
.
Given a fixed-time
, add the supplied duration
.
Example (reader notation requires calling LOCAL-TIME:ENABLE-READ-MACROS):
(add-time @2007-05-20T12:10:10.000 (duration :hours 50))
;; ⇒ @2007-05-22T14:10:10.000
note:
This function always adds the largest increments first, so:
(add-time @2003-01-09 (duration :years 1 :days 20)) ;; ⇒ @2004-02-29
If days has been added before years, the result would have been "@2004-03-01".
Compute the duration existing between fixed-times left
and right
.
The order of left or right is ignored; the returned duration
, if added to
the earlier value, will result in the later.
A complexity of this process which might surprise some is that larger
quantities are added by add-time
before smaller quantities. For example, what
is the difference between 2003-02-10 and 2004-03-01? If you add years before
days, the difference is 1 year and 20 days. If you were to add days before
years, however, the difference would be 1 year and 21 days. The question, do
you advance to 2004 and then calculate between 2-10 and 3-01, or do you move
from 2-10 to 3-01, and then increment the year? This library chooses to add
years before days, since this follows human reckoning a bit closer (i.e., a
person would likely flip to the 2004 calendar and then start counting off
days, rather than the other way around). This difference in reckoning can be
tricky, however, so bear this in mind.
Next: Relative time, Previous: Fixed time, Up: Top [Contents]
The basic element is the duration
structure. It has the slots years, months, days, hours, minutes, seconds
down to nanoseconds.
Create a duration with the duration
function. It accepts a key argument for each slot:
(duration :days 3)
;; =>
#S(DURATION
:YEARS 0
:MONTHS 0
:DAYS 3
:HOURS 0
:MINUTES 0
:SECONDS 0
:MILLISECONDS 0
:MICROSECONDS 0
:NANOSECONDS 0)
Create a duration
object.
One thing to note about duration: there is no way to determine the total length of a duration in terms of any specific time quantity, without first binding that duration to a fixed point in time (after all, how many days are in a month if you don’t know which month it is?) Therefore, those looking for a function like "duration-seconds" are really wanting to work with ranges, not just durations.
Access each duration slot with its accessor: duration-days
and so on.
Evaluate body where var
is bound to a time starting at start
+
duration
, separated by duration
, until and excluding end
.
A ’do’ style version of the functional map-times
macro.
The disadvantage to do-times
is that there is no way to ask for a reversed
time sequence, or specify an inclusive endpoint.
Return nil
.
Example:
;; when now is @2023-11-14T09:05:00
(do-times (time (now)
(duration :hours 1)
(next-day))
(print time))
;; =>
@2023-11-14T10:04:31.475324+01:00
@2023-11-14T11:04:31.475324+01:00
@2023-11-14T12:04:31.475324+01:00
[…]
@2023-11-14T23:04:31.475324+01:00
NIL
Map over a set of times separated by duration
, calling callable
with the
start of each.
Example:
(map-times #'print (now) (duration :hours 1) (next-day))
Add one duration to another.
Subtract one duration from another.
Multiply one duration to another.
Next: Time ranges, Previous: Time durations, Up: Top [Contents]
A relative time allows to work with ranges.
A relative-time
structure has the slots year, month, week, day-of-week, day, hour, minute, second
down to nanosecond.
Compute the first time after fixed-time
which matches relative-time
.
This function finds the first moment after fixed-time
which honors every
element in relative-time:
(next-time @2007-05-20 (relative-time :month 3)) ;; ⇒ @2008-03-20
The relative time constructor arguments may also be symbolic:
(relative-time :month :this)
(relative-time :month :next)
(relative-time :month :prev)
To find the date two weeks after next February, a combination of next-time
and add-time
must be used, since "next February" is a relative time concept,
while "two weeks" is a duration concept:
(add-time (next-time @2007-05-20 (relative-time :month 2))
(duration :days 14))
note:
The keyword arguments to relative-time
are always singular; those to
duration
are always plural.
The following form resolves to the first sunday of the given year:
(next-time (previous-time @2007-05-20
(relative-time :month 1 :day 1))
(relative-time :week-day 0))
This form finds the first Friday the 13th after today:
(next-time @2007-05-20 (relative-time :day 13 :day-of-week 5))
note:
When adding times, next-time
always seeks the next time that fully
honors your request. If asked for Feb 29, the year of the resulting time will
fall in a leap year. If asked for Thu, Apr 29, it returns the next occurrence
of Apr 29 which falls on a Friday. Example:
(next-time @2007-11-01
(relative-time :month 4 :day 29 :day-of-week 4))
;; ⇒ @2010-04-29T00:00:00.000
This function is the reverse of ‘NEXT-TIME’. Please look there for more.
There are helper functions to get dates relative to the current date:
and also:
up to next-sunday
, previous-
versions,
and also sunday-week-begin, monday-week-begin, day-begin, hour-begin
down to microsecond, and end-
versions,
and more.
Next: Time periods, Previous: Relative time, Up: Top [Contents]
Create a time-range
.
Params: the time-range
struct slots (:begin, :end, :duration…).
Example:
(time-range :begin (next-day) :end (next-sunday-week))
;; =>
#S(PERIODS:TIME-RANGE
:FIXED-BEGIN NIL
:BEGIN @2023-11-14T00:00:00.000000+01:00
:BEGIN-INCLUSIVE-P T
:FIXED-END NIL
:END @2023-11-19T17:51:58.625785+01:00
:END-INCLUSIVE-P NIL
:DURATION NIL
:ANCHOR NIL)
Use its accessor functions: time-range-begin
etc.
Return t
if fixed-time
is with this time-range
.
Example:
(time-within-range-p (local-time:now) (time-range :begin (periods:previous-day) :end (periods:next-day)))
Return t
if fixed-time
is with this time-range
.
Example:
(time-within-range-p (local-time:now) (time-range :begin (previous-day) :end (next-day)))
down to second-range
.
Previous: Time ranges, Up: Top [Contents]
Define a context where (1) min-symbol
and max-symbol
are locally
bound variables with nil
default values and (2) update
names a
lexically bound function which takes a timestamp and updates the
variables min-symbol
and max-symbol
so that they respectively hold the
earliest and latest timestamp after successive invocations. That
function finally returns its input value. For example, the following
code builds a time-range
instance from a list of dated transactions.
(with-timestamp-range (earliest latest)
(dolist (tt transaction)
(update-range (transaction-date tt)))
(time-range :begin earliest :end latest :end-inclusive-p t))
A custom name can be used to nest invocations:
(with-timestamp-range (earliest latest global-update-range)
(dolist (jj journals)
(with-timestamp-range (<< >>)
(dolist (tt (journal-xact jj))
(gloal-update-range
(update-range (transaction-date tt))))
(format t "Journal earliest / latest: ~A / ~A~%" << >>)))
(format t "Global earliest / latest: ~A / ~A~%" earliest latest))
See http://series.sourceforge.net/. To use the series-enabled functions, load the PERIOD-SERIES ASDF system.