| name | calgebra |
| description | Set algebra for calendars. Use when working with time intervals, finding free time, detecting conflicts, composing calendars, filtering events by duration or properties, computing metrics, or building recurring patterns. Use when this capability is needed. |
| metadata | {"author":"ashenfad"} |
calgebra
Set algebra for calendars. Compose lazily, query efficiently.
Quick Start
from calgebra import at_tz, to_dataframe, total_duration, HOUR
from calgebra.gcal import calendars, transparency
from datetime import date
token = access_token
tz = "US/Pacific"
at = at_tz(tz)
cals = calendars(token)
primary = next(c for c in cals if c.primary)
events = list(primary[at("2025-01-20"):at("2025-01-27")])
df = to_dataframe(events, tz=tz)
busy = primary & (transparency == "opaque")
busy_events = list(busy[at("2025-01-20"):at("2025-01-27")])
Imports matter: transparency comes from calgebra.gcal, NOT from
calgebra. Other field helpers like hours, minutes, field come
from calgebra.
at_tz() is required for all timeline slicing. You cannot pass bare
date objects to slice bounds. Dates are only accepted by metrics.
Core Concepts
Intervals are time ranges [start, end) with exclusive end bounds (Unix timestamps):
from calgebra import Interval, at_tz
at = at_tz("US/Pacific")
meeting = Interval.from_datetimes(start=at(2025, 1, 15, 14, 0), end=at(2025, 1, 15, 15, 0))
meeting.duration
Timelines are lazy interval sources. Compose with operators, slice to execute:
from calgebra import timeline, union, intersection
busy = alice_cal | bob_cal
busy = union(alice_cal, bob_cal, charlie_cal)
at = at_tz("US/Pacific")
events = list(busy[at("2025-01-01"):at("2025-01-31")])
at_tz() creates timezone-aware datetimes for slicing. Always pair with "US/Pacific":
at = at_tz("US/Pacific")
at("2025-01-01")
at(2025, 1, 15, 14, 30)
at(date(2025, 1, 1))
Operators
| Op | Meaning | Example |
|---|
| | Union | alice | bob — anyone busy |
& | Intersection | cal_a & cal_b — both busy |
- | Difference | workhours - meetings — free time |
~ | Complement | ~busy — all gaps |
Functional forms: union(*timelines), intersection(*timelines).
Filtering
from calgebra import hours, minutes, field, one_of, has_any, has_all
long_meetings = calendar & (hours >= 2)
short = calendar & (minutes < 30)
priority = field("priority")
high = timeline & (priority >= 8)
category = field("category")
work = timeline & one_of(category, {"work", "planning"})
tags = field("tags")
urgent = timeline & has_any(tags, {"urgent", "critical"})
both = timeline & has_all(tags, {"work", "urgent"})
Important: Use & between timelines and filters. | only works
between timelines.
DataFrame Conversion (Preferred for Displaying Events)
Use to_dataframe to present events to the user:
from calgebra import to_dataframe
events = list(calendar[at("2025-01-01"):at("2025-02-01")])
df = to_dataframe(events, tz="US/Pacific")
df = to_dataframe(events, include=["day", "time", "duration", "summary"])
df = to_dataframe(events, exclude=["uid", "dtstamp"])
df = to_dataframe(events, raw=True)
Default columns: day (date string), time (time string), duration (formatted),
then type-specific fields (summary, location, etc.).
With raw=True: day → datetime, time → datetime, duration → int (seconds).
Recurring Patterns
from calgebra import day_of_week, time_of_day, recurring, HOUR, MINUTE
tz = "US/Pacific"
weekdays = day_of_week(["monday", "tuesday", "wednesday", "thursday", "friday"], tz=tz)
work_hours = time_of_day(start=9*HOUR, duration=8*HOUR, tz=tz)
business_hours = weekdays & work_hours
biweekly = recurring(freq="weekly", interval=2, day="monday", start=9*HOUR, duration=HOUR, tz=tz)
first_monday = recurring(freq="monthly", week=1, day="monday", start=10*HOUR, duration=HOUR, tz=tz)
last_friday = recurring(freq="monthly", week=-1, day="friday", tz=tz)
payroll = recurring(freq="monthly", day_of_month=[1, 15], tz=tz)
Transformations
from calgebra import buffer, merge_within, flatten, HOUR, MINUTE
blocked = buffer(flights, before=2*HOUR)
busy = buffer(meetings, before=15*MINUTE, after=15*MINUTE)
incidents = merge_within(alarms, gap=15*MINUTE)
coalesced = flatten(cal_a | cal_b)
Metrics
All metric functions share this signature:
metric(timeline, start, end, period="full", tz="UTC", group_by=None)
- start/end:
date, datetime, or Unix int. Dates are interpreted as midnight in tz.
- period:
"full", "hour", "day", "week" (ISO Mon–Sun), "month", "year"
- tz: Always pass
"US/Pacific" (or an explicit IANA timezone).
- group_by (optional): Collapses windows by cyclic key. Cannot be used with
period="full" or "year".
from calgebra import total_duration, count_intervals, coverage_ratio
from datetime import date
tz = "US/Pacific"
daily = total_duration(meetings, date(2025, 11, 1), date(2025, 12, 1),
period="day", tz=tz)
daily_cov = coverage_ratio(calendar, date(2025, 11, 1), date(2025, 12, 1),
period="day", tz=tz)
Cyclic histograms (group_by):
Copy these exact period+group_by pairs — no other combinations work:
by_hour = total_duration(cal, date(2025, 1, 1), date(2025, 4, 1),
period="hour", group_by="hour_of_day", tz=tz)
by_dow = count_intervals(cal, date(2025, 1, 1), date(2025, 4, 1),
period="day", group_by="day_of_week", tz=tz)
by_dom = coverage_ratio(cal, date(2025, 1, 1), date(2025, 4, 1),
period="day", group_by="day_of_month", tz=tz)
by_woy = total_duration(cal, date(2025, 1, 1), date(2025, 4, 1),
period="week", group_by="week_of_year", tz=tz)
by_moy = count_intervals(cal, date(2025, 1, 1), date(2026, 1, 1),
period="month", group_by="month_of_year", tz=tz)
iCalendar (.ics) Files
from calgebra import file_to_timeline, timeline_to_file
cal = file_to_timeline("calendar.ics")
events = list(cal[at("2025-01-01"):at("2025-02-01")])
timeline_to_file(filtered_events, "output.ics")
Reverse Iteration
from itertools import islice
recent_first = list(calendar[start:end:-1])
last_5 = list(islice(calendar[start:end:-1], 5))
most_recent = next(calendar[start:end:-1], None)
Common Patterns
Find free time:
from calgebra.gcal import calendars, transparency
from calgebra import day_of_week, time_of_day, HOUR, at_tz
token = access_token
tz = "US/Pacific"
at = at_tz(tz)
cals = calendars(token)
primary = next(c for c in cals if c.primary)
weekdays = day_of_week(["monday", "tuesday", "wednesday", "thursday", "friday"], tz=tz)
work_hours = time_of_day(start=9*HOUR, duration=8*HOUR, tz=tz)
business_hours = weekdays & work_hours
busy = primary & (transparency == "opaque")
free = business_hours - busy
slots = list(free[at("2025-01-20"):at("2025-01-24")])
Detect conflicts:
has_conflict = any((my_calendar & proposed_time)[start:end])
Cross-timezone overlap:
pacific = weekdays & time_of_day(start=9*HOUR, duration=8*HOUR, tz="US/Pacific")
london = weekdays & time_of_day(start=9*HOUR, duration=8*HOUR, tz="Europe/London")
overlap = pacific & london
Key Points
- Composition is lazy, slicing executes
- Exclusive end bounds
[start, end) everywhere
- Always use
at_tz("US/Pacific") for slice bounds — never bare dates
transparency is imported from calgebra.gcal, not calgebra
& works between timelines and filters; | only between timelines
- Recurring patterns require finite bounds when slicing
Source: ashenfad/calgebra — distributed by TomeVault.