A Python library for date calculations using ISO8601 period/duration elements
Project description
DateExpr
A Python library for date calculations using ISO8601 period/duration elements. This library was created to support complex date/time calculations declaratively, rather than programmatically, allowing periods/intervals to be stored in files/databases for configuration purposes.
Installation
pip install dateexpr
Features
- ISO8601 Duration Support: Use standard period expressions like
P1Y,P3M,P1W,P1D,PT1H,PT30M,PT1S - Variable-Duration Period Handling: Correctly handles months and years that have different lengths
- Month-End Preservation:
Jan 31 + P1M = Feb 28/29(stays at month end) - Timezone Support: Full timezone support via
pytz - Fluent Interface: Chain operations for readable date calculations
- Predefined Aliases: Convenient shortcuts like
@today,@monthly,@quarter
Quick Start
from dateexpr import DateExpr
import pytz
# Create from ISO8601 date string
expr = DateExpr("2024-01-15")
# Add periods
next_month = expr.advanced("+P1M") # 2024-02-15
next_week = expr.advanced("+P1W") # 2024-01-22
tomorrow = expr.advanced("+P1D") # 2024-01-16
# Use predefined aliases
today = DateExpr("@today")
month_end = expr.advanced("@month-end") # 2024-01-31
# Month-end preservation
jan_31 = DateExpr("2024-01-31")
feb_end = jan_31.advanced("+P1M") # 2024-02-29 (leap year)
# Timezone handling
tz = pytz.timezone("America/Toronto")
expr = DateExpr("2024-01-15T10:30:00", tz=tz)
print(expr.iso8601()) # 2024-01-15T10:30:00-0500
print(expr.to_utc()) # UTC datetime
Period Syntax
ISO8601 Durations
| Pattern | Meaning |
|---|---|
P1Y |
1 year |
P3M |
3 months |
P1W |
1 week |
P1D |
1 day |
PT1H |
1 hour |
PT30M |
30 minutes |
PT1S |
1 second |
P1Y3M1DT1H30M |
Combined: 1 year, 3 months, 1 day, 1 hour, 30 minutes |
Prefixes
| Prefix | Meaning |
|---|---|
+P1M |
Add 1 month |
-P1M |
Subtract 1 month |
P0M |
Truncate to start of month |
P0D |
Truncate to start of day |
Predefined Aliases
Date/Time Navigation
| Alias | Equivalent | Description |
|---|---|---|
@today |
P0D |
Start of current day |
@tomorrow |
P0D,+P1D |
Start of next day |
@yesterday |
P1D |
Start of previous day |
@this_date |
P0D |
Start of current day |
@next_date |
P0D,+P1D |
Start of next day |
@this_week |
P0W |
Start of current week |
@next_week |
P0W,+P1W |
Start of next week |
@last_week |
P0W,-P1W |
Start of previous week |
@this_month |
P0M |
Start of current month |
@next_month |
P0M,+P1M |
Start of next month |
@last_month |
P1M |
Start of previous month |
@month-end |
P0M,+P1M,-P1D |
Last day of current month |
@month-mid |
P0M,+P1M,+P14D |
Middle of next month (15th) |
@this_quarter |
P0Q |
Start of current quarter |
@next_quarter |
P0Q,+P1Q |
Start of next quarter |
@last_quarter |
P1Q |
Start of previous quarter |
@this_year |
P0Y |
Start of current year |
@next_year |
P0Y,+P1Y |
Start of next year |
@last_year |
P1Y |
Start of previous year |
Frequency Aliases (for advance() and split())
| Alias | Equivalent | Description |
|---|---|---|
@daily |
+P1D |
Every day |
@weekly |
+P1W |
Every week |
@bi-weekly |
+P2W |
Every 2 weeks |
@monthly |
+P1M |
Every month |
@quarterly |
+P3M |
Every 3 months |
@semi-annually |
+P6M |
Every 6 months |
@annually |
+P1Y |
Every year |
Week Start Configuration
By default, weeks start on Monday (ISO 8601). You can configure a different week start day using the week_start parameter:
# Default: Monday (week_start=0)
expr = DateExpr("2024-01-17") # Wednesday
expr.advanced("P0W") # → Monday 2024-01-15
# US convention: Sunday (week_start=6)
expr = DateExpr("2024-01-17", week_start=6) # Wednesday
expr.advanced("P0W") # → Sunday 2024-01-14
# Week start values: 0=Monday, 1=Tuesday, ..., 6=Sunday
The week_start setting affects:
P0Wtruncation: Truncates to the configured first day of the week- Week aliases:
@this_week,@next_week,@last_weekrespect the setting copy()method: Preserves theweek_startvalue
# Week aliases with Sunday start
expr = DateExpr("2024-01-17", week_start=6) # Wednesday
expr.advanced("@this_week") # → Sunday 2024-01-14
expr.advanced("@next_week") # → Sunday 2024-01-21
expr.advanced("@last_week") # → Sunday 2024-01-07
# Setting is preserved through copy()
copy = expr.copy()
copy.advanced("P0W") # → Sunday (setting preserved)
Chained Operations
Comma-separated expressions are applied sequentially:
# End of current month
expr.advanced("P0M,+P1M,-P1D")
# First Monday of month
expr.advanced("P0M,+mo")
# Last Friday of month
expr.advanced("@month-end,-fr")
Splitting Time Ranges
expr = DateExpr("2024-01-01")
# Split into monthly ranges
ranges = expr.split(frequency="P1M", end="+P3M")
# Returns 3 DateRange objects: Jan, Feb, Mar
# Split with rate format
ranges = expr.split(frequency="2/month", end="+P1M")
# Returns 2 DateRange objects per month
Ranges with Gaps (duration parameter)
Use the duration parameter to create ranges shorter than the interval between them:
expr = DateExpr("2024-01-01")
# 1-month ranges every quarter (with 2-month gaps)
ranges = expr.split(frequency="+P3M", duration="P1M", end="+P1Y")
# Returns 4 ranges: Jan 1-Feb 1, Apr 1-May 1, Jul 1-Aug 1, Oct 1-Nov 1
# 1-day ranges every week
ranges = expr.split(frequency="+P1W", duration="P1D", end="+P4W")
# Returns 4 ranges: Jan 1-2, Jan 8-9, Jan 15-16, Jan 22-23
# 8-hour business day blocks every day
expr = DateExpr("2024-01-01T09:00:00")
ranges = expr.split(frequency="+P1D", duration="PT8H", end="+P5D")
# Returns 5 ranges: 9am-5pm each day
Alternating Frequencies
Use semicolon-separated expressions to cycle through different intervals:
# Alternating weeks: 1 week, then 2 weeks, repeating
expr = DateExpr("2024-01-01", frequency="+P1W;+P2W")
ranges = expr.split(end="+P9W")
# Returns ranges: Jan 1-8, Jan 8-22, Jan 22-29, Jan 29-Feb 12, ...
# Business day pattern: 4x 2-hour blocks, then 16-hour overnight gap
expr = DateExpr("2024-01-01T09:00:00", frequency="+PT2H;+PT2H;+PT2H;+PT2H;+PT16H")
ranges = expr.split(end="+P5D")
# Creates time blocks: 9-11am, 11am-1pm, 1-3pm, 3-5pm, then jumps to 9am next day
# Using advance() with alternating frequency
expr = DateExpr("2024-01-01", frequency="+P1W;+P2W")
expr.advance() # +1 week → Jan 8
expr.advance() # +2 weeks → Jan 22
expr.advance() # +1 week → Jan 29 (cycles back)
Custom Quarters
By default, quarters start in January, April, July, and October. You can configure custom quarter boundaries for fiscal years:
# US Federal fiscal year (starts October 1)
expr = DateExpr("2024-11-15", quarters=[10, 1, 4, 7])
expr.advanced("@this_quarter") # → 2024-10-01
expr.advanced("@next_quarter") # → 2025-01-01
# Add/subtract quarters with P#Q syntax
expr = DateExpr("2024-01-15")
expr.advanced("+P1Q") # → 2024-04-15
expr.advanced("+P2Q") # → 2024-07-15
Output Methods
Getting Values
expr = DateExpr("2024-01-15T10:30:00", tz=pytz.timezone("America/Toronto"))
# Get date only
expr.date() # date(2024, 1, 15)
# Get datetime in original timezone
expr.to_datetime() # datetime with original tz
# Get datetime in specific timezone
pst = pytz.timezone("America/Los_Angeles")
expr.datetime(tz=pst) # datetime converted to PST
# Get datetime in UTC
expr.to_utc() # datetime in UTC
Formatting Output
expr = DateExpr("2024-01-15T10:30:00", tz=pytz.timezone("America/Toronto"))
# ISO8601 format
expr.iso8601() # "2024-01-15T10:30:00-0500"
# ISO8601 with timezone name
expr.iso8601(with_tzname=True) # "2024-01-15T10:30:00-0500[EST]"
# ISO8601 in different timezone
expr.iso8601(tz=pytz.UTC) # "2024-01-15T15:30:00+0000"
# Simple date string
expr.to_date_str() # "2024-01-15"
Duration Calculations
Calculate the duration between two dates:
expr = DateExpr("2024-01-15")
# Duration as ISO8601 period string
expr.duration_to("2024-03-20") # "+P2M5D"
expr.duration_to("+P1M") # "+P1M"
# Duration in specific units
expr.duration_seconds("+PT1H") # 3600.0
expr.duration_minutes("+PT90M") # 90.0
expr.duration_hours("+P1D") # 24.0
# Compare with datetime objects
from datetime import datetime
other = datetime(2024, 2, 15, 12, 0, 0)
expr.duration_hours(other) # ~744.5
Utility Methods
expr = DateExpr("2024-02-15")
# Get days in current month
expr.days_in_month() # 29 (leap year)
# Get a single period as DateRange
expr = DateExpr("2024-01-01", frequency="@monthly")
rng = expr.period() # DateRange: Jan 1 - Feb 1
DateRange Class
A DateRange represents a time span with inclusive start and exclusive end.
Creating DateRanges
from dateexpr import DateRange
from datetime import date, datetime
# From dates
range = DateRange(date(2024, 1, 15), date(2024, 1, 20))
# From datetimes (preserves time and timezone)
start = datetime(2024, 1, 15, 9, 0, 0)
end = datetime(2024, 1, 15, 17, 0, 0)
range = DateRange(start, end)
Accessing Start/End
range = DateRange(date(2024, 1, 15), date(2024, 1, 20))
# As datetime
range.start() # datetime at midnight
range.end() # datetime at midnight
# As date
range.start_date() # date(2024, 1, 15)
range.end_date() # date(2024, 1, 20)
# Inclusive end (last full day in range)
range.end_date_inclusive() # date(2024, 1, 19)
# With timezone conversion
est = pytz.timezone("America/New_York")
range.start(tz=est) # datetime in EST
range.start_date(tz=est) # date in EST
# As UTC
range.start_utc() # datetime in UTC
range.end_utc() # datetime in UTC
# As ISO8601 strings
range.start_iso8601() # "2024-01-15T00:00:00+0000"
range.end_iso8601() # "2024-01-20T00:00:00+0000"
Duration Methods
range = DateRange(date(2024, 1, 15), date(2024, 1, 20))
# As ISO8601 period
range.duration() # "+P5D"
# In specific units
range.duration_seconds() # 432000.0
range.duration_minutes() # 7200.0
range.duration_hours() # 120.0
Mutating vs Non-Mutating Methods
| Non-Mutating (returns new) | Mutating (modifies in-place) |
|---|---|
advanced(spec) |
advance(spec) |
truncated_microseconds() |
truncate_microseconds() |
copy() |
- |
Month and Weekday Navigation
Navigate to specific months or weekdays:
expr = DateExpr("2024-06-15")
# Navigate to month (nearest)
expr.advanced("jan") # → 2024-01-15 (nearest January)
# Navigate forward to month
expr.advanced("+jan") # → 2025-01-15 (next January)
# Navigate backward to month
expr.advanced("-jan") # → 2024-01-15 (previous January)
# Navigate to weekday
expr.advanced("mo") # → nearest Monday
expr.advanced("+fr") # → next Friday
expr.advanced("-we") # → previous Wednesday
License
MIT License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dateexpr-1.0.0.tar.gz.
File metadata
- Download URL: dateexpr-1.0.0.tar.gz
- Upload date:
- Size: 14.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
850536cd59f5a6a7ca3f33d1c2d1bd796b9f3b477c2fb353b6f98c9d7d3dde0b
|
|
| MD5 |
9a32462ce9424f746944e32def751117
|
|
| BLAKE2b-256 |
42524444a35da0efae424177762fdd9b9eebbaf4fd12a4d8d0c5273947ec8bec
|
File details
Details for the file dateexpr-1.0.0-py3-none-any.whl.
File metadata
- Download URL: dateexpr-1.0.0-py3-none-any.whl
- Upload date:
- Size: 14.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e2f7a745288fb7070f60998cf86d9e0e8d466709cc238ba9b2c0d62b413c82cb
|
|
| MD5 |
43f235c697d8a2735bb13aa9ef3efbc4
|
|
| BLAKE2b-256 |
372c79ddfcef30f7dd1c95b23cfdf62aaf3a6436fcee946d08c87d372b59fee4
|