Date & Time Module¶
The date_time standard library module provides relational predicates for constructing, decomposing, and manipulating dates and times. A date, time, datetime or duration is an ordinary term — a cell such as date(2026, 3, 16), which Python sees as the tuple ('date', 2026, 3, 16). The predicates compute with Python's datetime module internally, but they take and give back terms, never datetime objects.
The implementation lives in clausal/modules/py/datetime.py.
Import¶
-import_from(date_time, [now, today, date, time, datetime,
timedelta, date_add, date_sub, date_diff,
days_between, datetime_string,
date_of, weekday, date_between, timestamp,
datetime_string_iso, date_string_iso,
date_max, date_min, ordinal])
Or via module import:
The terms¶
| Term | Python value it stands for |
|---|---|
date(Year, Month, Day) |
datetime.date |
time(Hour, Minute, Second, Microsecond) |
datetime.time |
datetime(Year, Month, Day, Hour, Minute, Second, Microsecond) |
naive datetime.datetime (a tz-aware one carries a ninth argument, the UTC offset in seconds) |
timedelta(Days, Seconds, Microseconds) |
datetime.timedelta |
date(Y, M, D) is a term, not a goal: write D is date(2026, 3, 16) to build
one and date(Y, M, DAY) is D to take one apart — both are plain
unification. Importing date makes the ground form validated: an impossible
date raises domain_error(date, …) instead of building a bogus term:
Wrap it in catch/3 if a caller wants failure instead.
To call a Python datetime method, convert explicitly with
to_python, which turns a date term into the
datetime object:
-import_from(date_time, [date])
-import_from(clausal, [to_python])
iso_date(Y, M, D, S) <- (
DT is date(Y, M, D),
S is ++to_python(DT).isoformat()
)
S is the Python str '2026-03-16' — an atom. From Python,
clausal.to_python(('date', 2026, 3, 16)) is datetime.date(2026, 3, 16),
and clausal.to_clausal goes the other way.
Prefer the declarative predicates over ++ escapes
++() drops into arbitrary Python and is reserved for last-resort interop. The common date operations all have clean, relational equivalents — reach for these first:
Instead of ++ … |
Use |
|---|---|
S is ++to_python(DT).isoformat() |
datetime_string(DT, S, "%Y-%m-%d") |
N is ++to_python(TD).days |
timedelta(N, _, TD) |
D is ++to_python(DT).date() |
date_of(DT, D) |
date_diff(A, B, TD), timedelta(N, _, TD) |
days_between(A, B, N) |
M is ++max(…) / ++min(…) |
date_max(D1, D2, M) / date_min(D1, D2, M) |
N is ++to_python(D).toordinal() |
ordinal(D, N) |
Text results (datetime_string/3, datetime_string_iso/2,
date_string_iso/2) are strings, and a format or ISO text argument is
written as a string: datetime_string(date(2026, 3, 16), S, "%Y-%m-%d")
gives S = "2026-03-16".
Ordering & comparison¶
date, time, and datetime terms are ordered chronologically by the
comparison operators — <, >, <=, >=:
-import_from(date_time, [date])
earlier(A, B) <- (A is date(2020, 1, 1), B is date(2021, 1, 1), A < B) # succeeds
The same terms sort chronologically through sort/2, msort/2,
min_list/2, and max_list/2.
Only terms of the same kind should be compared. Comparing a date with a
number throws error(type_error(orderable, 5), (<)/2), and min_list/2 /
max_list/2 raise type_error(orderable, List) for such a mix. catch/3
intercepts it and binds the error term:
Comparing a date with a datetime does not raise today and does not
compare chronologically (date(2030, 1, 1) < DT holds for a datetime in
2020): convert with date_of/2 first.
Predicates¶
now/1, now_utc/1, today/1¶
now(DT) # DT = datetime(2026, 3, 16, 14, 30, 5, 123456) (local, naive)
now_utc(DT) # DT = datetime(2026, 3, 16, 13, 30, 5, 123456, 0) (UTC offset 0)
today(D) # D = date(2026, 3, 16)
date/3 — the date term¶
date(Year, Month, Day) is the date itself; unification constructs and decomposes it:
# Construct (unification with the validated term)
D is date(2026, 3, 16) # D = date(2026, 3, 16)
# Decompose
date(Y, M, DAY) is D # Y = 2026, M = 3, DAY = 16
time/4 — Bidirectional¶
time(Hour, Minute, Second, TimeObj):
datetime/7 — Bidirectional¶
datetime(Year, Month, Day, Hour, Minute, Second, DtObj):
A component of the right type but no such value raises a domain error carrying the term, as date/3 does: time(25, 0, 0, T) is domain_error(time, time(25, 0, 0)) and datetime(2026, 13, 1, 0, 0, 0, DT) is domain_error(datetime, datetime(2026, 13, 1, 0, 0, 0)).
timedelta/3 — Bidirectional¶
timedelta(Days, Seconds, TdObj):
timedelta(7, 0, TD) # TD = timedelta(7, 0, 0)
timedelta(D, S, TD) # decompose TD into days and seconds
date_add/3, date_sub/3¶
date_add(DATE, DELTA, RESULT) # RESULT = DATE + DELTA
date_sub(DATE, DELTA, RESULT) # RESULT = DATE - DELTA
date_diff/3¶
days_between/3¶
days_between(DateA, DateB, N) — the whole-day count of DateA - DateB as a plain integer, so the common "days between two dates" need is a single goal instead of date_diff(A, B, TD), timedelta(N, _, TD):
datetime_string/3¶
datetime_string(DT, S, "%Y-%m-%d") # format: S = "2026-03-16"
datetime_string(DT, "2026-03-16", "%Y-%m-%d") # parse: DT = datetime(2026, 3, 16, 0, 0, 0, 0)
date_of/2 — Bidirectional¶
date_of(DateTime, Date) — the declarative form of ++to_python(DT).date(). Forward, it extracts the calendar date from a datetime; in reverse (with DateTime unbound) it builds the midnight datetime of a date:
date_of(DT, D) # D = the calendar date of DT (datetime → date)
date_of(DT, D) # DT = midnight of D (date → datetime; DT unbound)
date_max/3, date_min/3¶
date_max(D1, D2, M) / date_min(D1, D2, M) — M is the later (resp. earlier) of two dates or datetimes, with no ++ escape. A date/datetime mix is not comparable and raises a type_error on the second argument (date_max(date(2026,3,16), datetime(2026,3,16,9,0,0,0), M) is type_error(date, datetime(2026,3,16,9,0,0,0)): the first argument fixes the type); a naive with an aware datetime raises domain_error(naive_datetime, DT) (or aware_datetime). date_diff/3, days_between/3 and date_between/3 raise the same:
date_max(D1, D2, M) # M = the later of two dates — no ++max(D1, D2)
date_min(D1, D2, M) # M = the earlier of two dates
ordinal/2 — Bidirectional¶
ordinal(Date, N) — the proleptic-Gregorian day number (Python's toordinal()). Forward binds N (a datetime contributes its calendar day's ordinal); in reverse (Date unbound) it builds the date term for day N (an N outside 1..3652059 raises domain_error(ordinal, N)), so enumerating every calendar day in [CS, CE] is ordinal(CS, A), ordinal(CE, B), numlist(A, B, Ns) mapped back through the inverse mode:
ordinal(D, N) # date(2026, 3, 16) → N = 739691
ordinal(D, N) # N = 739691 → D = date(2026, 3, 16) (D unbound)
weekday/2¶
timestamp/2 — Bidirectional¶
timestamp(DateTime, Stamp) — convert between a datetime term and a POSIX epoch float:
timestamp(DT, TS) # forward: TS = DT.timestamp() (float epoch)
timestamp(DT, 0) # inverse: DT = the local datetime of epoch 0
Forward (DateTime bound to a datetime term): Stamp = the float seconds since the epoch. With Stamp already bound this acts as a check. Inverse (DateTime unbound, Stamp a number): DateTime is the local-time datetime for that stamp. A plain date has no timestamp, so the forward direction requires a datetime — use datetime/7 to construct one first.
datetime_string_iso/2, date_string_iso/2 — Bidirectional ISO-8601¶
Bidirectional ISO-8601 string conversion without a format argument:
datetime_string_iso(DT, S) # S = "2026-03-16T14:30:00" / inverse parses it
date_string_iso(D, S) # S = "2026-03-16" / inverse parses it
datetime_string_iso/2 forward requires a datetime term and produces the full ISO-8601 string; inverse (datetime unbound, string bound) parses it. date_string_iso/2 forward requires a date term (a datetime is rejected — use date_of/2 first if needed) and produces YYYY-MM-DD; inverse parses it. Text that does not parse raises syntax_error(invalid_datetime) / syntax_error(invalid_date) (ruled 2026-10-02); text that parses but names no date ("2026-13-01") raises domain_error(iso_datetime, S) / domain_error(iso_date, S); a wrong kind of term raises type_error (date_string_iso(DT, S) with a datetime is type_error(date, datetime(...))), and both arguments unbound raises instantiation_error (see Wrong-type arguments raise).
date_between/3 — Nondeterministic¶
date_between(Start, End, D) — generates each date in the range [Start, End]:
-import_from(date_time, [date, date_between])
week_dates(START, END, D) <- date_between(START, END, D)
march_16_to_18(D) <- date_between(date(2026, 3, 16), date(2026, 3, 18), D)
# D = date(2026, 3, 16); date(2026, 3, 17); date(2026, 3, 18)
This is nondeterministic — it succeeds once for each date in the range via backtracking.
Test coverage
Tests are in tests/test_date_time.py.
- now/now_utc/today: current timestamps
- date/3: construct, decompose, invalid values
- time/4: construct, decompose
- datetime/7: construct, decompose
- timedelta/3: construct, decompose
- date_add/date_sub/date_diff: arithmetic
- days_between: direct integer day count
- datetime_string: bidirectional strftime/strptime
- date_of: datetime ↔ date (both modes)
- weekday: weekday computation
- date_between: date range enumeration
- timestamp: bidirectional datetime ↔ POSIX epoch
- datetime_string_iso/date_string_iso: bidirectional ISO-8601 helpers
See also: I/O — writing and formatting output · Python Interop — ++() escape for additional datetime operations.