Skip to main content

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:

  • P0W truncation: Truncates to the configured first day of the week
  • Week aliases: @this_week, @next_week, @last_week respect the setting
  • copy() method: Preserves the week_start value
# 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dateexpr-1.0.0.tar.gz (14.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dateexpr-1.0.0-py3-none-any.whl (14.3 kB view details)

Uploaded Python 3

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

Hashes for dateexpr-1.0.0.tar.gz
Algorithm Hash digest
SHA256 850536cd59f5a6a7ca3f33d1c2d1bd796b9f3b477c2fb353b6f98c9d7d3dde0b
MD5 9a32462ce9424f746944e32def751117
BLAKE2b-256 42524444a35da0efae424177762fdd9b9eebbaf4fd12a4d8d0c5273947ec8bec

See more details on using hashes here.

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

Hashes for dateexpr-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e2f7a745288fb7070f60998cf86d9e0e8d466709cc238ba9b2c0d62b413c82cb
MD5 43f235c697d8a2735bb13aa9ef3efbc4
BLAKE2b-256 372c79ddfcef30f7dd1c95b23cfdf62aaf3a6436fcee946d08c87d372b59fee4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page