This Python library provides intuitive methods for defining relative points in time using a custom protocol. It supports chaining of datetime intervals in a parent-child fashion, abstracted with attributes and indexes. For example: year.month[2]
.
Note: This library is in its alpha stage. Contributions and feedback are welcome!
Flexible Interval Expressions: Define points in time by chaining properties of the `Expression` object and indexing them.
# Second day of the 3rd week of the month, 0-based indexing
expr = Expression()
expr.month.week[2].day[1]
Negative Indexing: Support for negative indices to easily access "last" instances of time units (e.g., the last day of the month).
expr.month.day[-1] # Last day of the month
Rollover Handling: Control whether units roll over into the next larger unit or strictly validate.
By default, invalid indices roll over into the parent. For example:
nonleap_date = datetime(2021, 1, 1)
feb_29_expr = Expression().year.month[1].day[28]
feb_29_2021 = feb_29_expr(nonleap_date)
print(feb_29_2021)
2024-03-01 00:00:00
Similarly, with mathmatical operations:
expr = Expression()
feb_29_expr = expr.year.month[1].day[27] + expr.day.n(1)
feb_29_2021 = feb_29_expr(nonleap_date)
print(feb_29_2021)
2024-03-01 00:00:00
To disable this behavior, pass rollover=False
:
feb_29_2021 = feb_29_expr(nonleap_date, rollover=False)
IndexError: Rollover occurred for Month from 2 to 3.
If you want operations to roll over, but not invalid indices, you can pass operation_safe=True
:
feb_29_expr = expr.year.month[1].day[27] + expr.day.n(1)
feb_29_2021 = feb_29_expr(nonleap_date, rollover=False, operation_safe=True)
print(feb_29_2021)
2021-03-01 00:00:00
More Time Intervals: Provides proxies for additional units of time, such as centisecond, decisecond, and decade.
print(
expr
.decade
.year[0]
.quarter[0]
.month[0]
.week[0]
.day[0]
.hour[0]
.minute[0]
.second[0]
.decisecond[0]
.millisecond[0]
.microsecond[0]
)
Decade > Year[1] > Quarter[1] > Month[1] > Week[1] > Day[1] > Hour[1] > Minute[1] > Second[1] > Decisecond[1] > Millisecond[1] > Microsecond[1]
Lazy Evaluation: Define relative time objects and evaluate them against a specific `datetime` object later.
# Last day of the month
last_day_of_month = expr.month.day[-1]
# Evaluates after recieving a baseline
last_day_this_month = last_month_day(datime.now())
print(last_day_this_month)
2024-09-30 00:00:00
Lazy Chain Validation: Rough validation is provided during chaining and indexing, but full validation occurs only during evaluation.
expr.month.day[99]
----------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[4], line 1
----> 1 expr.month.day[99]
ValueError: Day cannot accept index 99 of Month (max: 34)
Lazy Arithmetic Evaluation: Add or subtract relative deltas from an `Expression`. The library ensures proper order of operations during evaluation.
from pyinterval.expression import Expression
from datetime import datetime
expr = Expression() # Instantiate the Expression object
date_expr = expr.year.month[1] # Start with the year and second month
date_expr += expr.day.n(1) # Add 1 day to the expression
date_expr = date_expr.day[27] # Set the 28th day (0-based, so index 27)
date_expr -= expr.month.n(1) # Subtract one month from the expression
time_expr = ( # Set the time to 01:01:01.101001
date_expr
.hour[0]
.minute[0]
.second[0]
.decisecond[0]
.millisecond[0]
.microsecond[0]
)
final_expr = time_expr + expr.month.n(1) # Add one month to the final result
result = final_expr(datetime(2021, 1 ,1)) # Evaluate the expression with a non-leap year
print(result)
result = final_expr(datetime(2024, 1 ,1)) # Evaluate the expression with a leap year
print(result)
2021-03-01 01:01:01.101001
2024-02-29 01:01:01.101001
Self-Explanatory String Representations: The `__repr__` method produces an effective representation of the relative time expression.
print(some_date)
Year > Month[3] > Day[1] + relativedelta(months=-1, days=-1) > Hour[12] > Minute[45]
High Performance: Complex relative dates are evaluated quickly and efficiently.
%%timeit
expr = Expression()
date_expr = expr.year.month[-11]
date_expr += expr.day.n(59)
date_expr -= expr.month.n(1634)
time_expr = (
date_expr
.hour[-823]
.minute[-59]
.second[-59]
.decisecond[-9]
.millisecond[-99]
.microsecond[-999]
)
final_expr = time_expr + expr.month.n(1)
result1 = final_expr(datetime(2021, 1 ,1)) # Evaluate the expression with a non-leap year
result2 = final_expr(datetime(2024, 1 ,1)) # Evaluate the expression with a leap year
assert result1 != result2
385 μs ± 4.22 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
You can chain intervals together to describe increasingly granular time ranges. For example:
expr = Expression().year.month[2].day[4] # 5th day of the 3rd month (0-based indexing)
The structure consists of 4 main parts: root, root scope, scope units, and indices:
Root().root_scope.scope_unit[index]
- Root: The
Expression()
object is always the root, encapsulating the logic. - Root Scope: Defines the top-level time unit (e.g.,
year
). - Scope Unit: Represents smaller time units (e.g.,
month
,day
). - Index: Represents the position within the parent element (e.g.,
year.month[2]
refers to the 3rd month of the year).
The root scope cannot be indexed directly. However, you can call a root scope's n
attribute (e.g., Expression().quarter.n(1)
) to generate a timedelta
.
print(expr.quarter.n(2)) # 6 months relative to the current date
relativedelta(months=+6)
You can pass a date to a scope unit to evaluate the expression and create an absolute datetime
.
relative_date = expr.year.quarter[-1].month[1].week[-1].day[5]
absolute_date = relative_date(datetime.now())
print(absolute_date)
2024-11-26 00:00:00
- Smart Indexing: Implement slicing (
[start:stop:step]
) to generate date ranges at specified granularity. - Validation: Add
expression.verify(datetime) -> bool
to check if a datetime matches the expression or falls within the specified range. - Timedelta Compatibility: Allow arithmetic with
timedelta
objects on expression chains (e.g.,chain - timedelta(days=1)
). - Dynamic Chain Building: Provide built-in methods to dynamically create chains, reducing manual effort for constructing expressions.
- Chain Operators: Support chaining with the
+
operator and breaking chains with the-
operator. - Advanced Smart Indexing: Allow
[start:stop]
to be expressions and[:step]
to be a timedelta. - Rust Implementation: Rebuild the library in Rust, with Python bindings.