Tempus

Easy handling of time periods aka slots, like business days, holidays, schedules, and cron-like recurring slots.
Why Estructura? Elevating Code Quality
Tempus and related libraries in the ecosystem rely heavily on Estructura, an advanced data-structuring and domain-modelling framework for Elixir.
Building complex domain libraries (especially time-arithmetic and schedule processing engines) requires strict data guarantees, type-safety, and minimal boilerplate. Estructura dramatically improves code quality across several crucial dimensions:
1. Strict Validation & Type Coercion at Boundaries
Instead of manually writing defensive functions or risking invalid internal state, Estructura allows defining strict field schemas with type coercion and validation contracts. Inputs are normalized at construction time, ensuring data structures are guaranteed valid throughout the pipeline.
defmodule BusinessHours do
use Estructura
defstruct [
open_time: {Time, :from_iso8601!},
close_time: {Time, :from_iso8601!}
]
# Enforces runtime contracts and validates constraints
def validate(%__MODULE__{open_time: o, close_time: c}) do
if Time.compare(c, o) == :gt, do: :ok, else: {:error, :invalid_hours}
end
end
# Coerces ISO8601 string inputs automatically into %Time{} structs
{:ok, hours} = BusinessHours.new(open_time: "09:00:00", close_time: "17:00:00")
2. Guard & Pattern Matching Primitives
Estructura generates compile-time guard helpers and pattern-matching primitives (is_...), avoiding ad-hoc map key checks or defensive is_map/1 guards across module boundaries.
3. Ergonomic Immutability & Nested Access (Access Protocol & Lenses)
Manipulating deeply nested data structures in idiomatic Elixir can become verbose. Estructura generates full Access protocol implementations and lens transformers (get_in, put_in, update_in), making complex nested updates concise and type-safe.
# Update nested values safely using generated Access lenses
update_in(schedule, [:business_hours, :open_time], &Time.add(&1, 3600))
4. Automated Property-Based Testing Integration
Estructura integrates directly with StreamData, automatically deriving generators for data structures. This allows instant property-based testing of domain models without manually constructing complex test generators.
# Automatically generate random valid structs for property tests
property "slots are always valid" do
check all slot <- BusinessHours.generator() do
assert Time.compare(slot.close_time, slot.open_time) == :gt
end
end
Key Concepts in Tempus
Tempus models time periods using half-open intervals $[from, to)$ (from_open: false, to_open: true by default):
- Half-Open Intervals: A standard slot $[00:00:00, 00:00:00\text{ next day})$ covers the whole day up to, but not including, midnight of the next day. This simplifies slot joining, adjacency checks, and avoids 1-microsecond arithmetic hacks.
- Infinite / Unbound Limits: A
nilbound represents infinity ($-\infty$ or $+\infty$) and is always marked as open (true). ~ISigil: Concise sigil syntax for constructing slots with custom boundary openness:~I[2023-04-10 10:00:00Z → 2023-04-10 12:00:00Z)— closedfrom, opento.~I(2023-04-10 10:00:00Z → 2023-04-10 12:00:00Z]— openfrom, closedto.
Installation
Add tempus to your list of dependencies in mix.exs:
def deps do
[
{:tempus, "~> 0.16"}
]
end
Usage Examples
1. Creating Slots with ~I Sigil
import Tempus.Sigils
# Whole day slot [2023-04-10 00:00:00Z -> 2023-04-11 00:00:00Z)
day_slot = ~I(2023-04-10)d
# Explicit datetime slot
slot = ~I[2023-04-10 09:00:00Z → 2023-04-10 17:00:00Z)
2. Schedule Arithmetic & Busy/Free Time
alias Tempus.{Slot, Slots}
import Tempus.Sigils
# Define holiday and weekend slots
holidays = [~D[2020-08-06], ~D[2020-08-13]] |> Tempus.slots()
weekends = [~D[2020-08-08], ~D[2020-08-09]] |> Tempus.slots()
schedule = Slots.merge(holidays, weekends)
# Calculate 5 business days ahead starting from Aug 5th
business_days = Tempus.days_ahead(schedule, ~D[2020-08-05], 5)
Changelog
0.16.0— [📚] half-open slot interval refactoring ([from, to)),nilopen infinity bounds,v1.18.0, deprecation ofis_covered/20.15.0— [🎉]Tempus.parse_cron/2and low-levelTempus.Crontab0.14.1— [🐜]recurrent/3has now concerned about DST0.14.0— [📚] refactoring of guard usage and docs update0.13.3— [🐜] avoid crash on incorrect input of slots0.13.2— [🎉]Tempus.Slots.Stream.recurrent/3to introduce cron-like streams0.13.0— [🎉] different calendars experimental support0.12.1— [🎉] timezones and more guards exported0.11.0— [📚] better coverage0.10.2— [🐜]add/4and tests for it: fixed0.10.1— [🐜]split/4and tests for it: fixed0.10.0— complete rewrite of implementations, 3–10× faster,Slotsform an Abelian group now