Python library for comprehensive datetime range handling with validation, manipulation, and arithmetic operations.
npx @tessl/cli install tessl/pypi-datetimerange@1.2.00
# DateTimeRange
1
2
DateTimeRange is a Python library for comprehensive datetime range handling. It provides functionality for creating, validating, manipulating, and performing operations on datetime ranges, including containment checking, intersection calculation, range arithmetic, iteration, and time range transformations.
3
4
## Package Information
5
6
- **Package Name**: DateTimeRange
7
- **Language**: Python
8
- **Installation**: `pip install DateTimeRange`
9
- **Import Name**: `datetimerange`
10
- **Version**: 1.2.0
11
12
## Core Imports
13
14
```python
15
from datetimerange import DateTimeRange
16
```
17
18
Version and metadata information:
19
```python
20
from datetimerange import __version__, __author__, __license__
21
```
22
23
Type imports for advanced usage:
24
```python
25
from typing import List, Optional, Union
26
```
27
28
## Basic Usage
29
30
### Creating DateTime Ranges
31
32
```python
33
from datetimerange import DateTimeRange
34
import datetime
35
36
# From string representations
37
time_range = DateTimeRange(
38
"2015-03-22T10:00:00+0900",
39
"2015-03-22T10:10:00+0900"
40
)
41
print(time_range)
42
# Output: 2015-03-22T10:00:00+0900 - 2015-03-22T10:10:00+0900
43
44
# From datetime objects
45
start = datetime.datetime(2015, 3, 22, 10, 0, 0)
46
end = datetime.datetime(2015, 3, 22, 10, 10, 0)
47
time_range = DateTimeRange(start, end)
48
49
# From range text
50
time_range = DateTimeRange.from_range_text(
51
"2015-03-22T10:00:00+0900 - 2015-03-22T10:10:00+0900"
52
)
53
```
54
55
### Basic Range Operations
56
57
```python
58
# Check containment
59
print("2015-03-22T10:05:00+0900" in time_range) # True
60
print("2015-03-22T10:15:00+0900" in time_range) # False
61
62
# Get intersection with another range
63
other_range = DateTimeRange(
64
"2015-03-22T10:05:00+0900",
65
"2015-03-22T10:15:00+0900"
66
)
67
intersection = time_range.intersection(other_range)
68
print(intersection)
69
# Output: 2015-03-22T10:05:00+0900 - 2015-03-22T10:10:00+0900
70
71
# Iterate through range
72
import datetime
73
for dt in time_range.range(datetime.timedelta(minutes=2)):
74
print(dt)
75
```
76
77
## Architecture
78
79
The DateTimeRange library is built around a single main class that encapsulates all datetime range functionality:
80
81
- **DateTimeRange Class**: Central class providing all range operations with comprehensive validation and manipulation capabilities
82
- **Properties**: Immutable access to start/end datetimes and calculated timedelta
83
- **Validation**: Built-in time inversion checking and range validity validation
84
- **Operations**: Full suite of mathematical operations (intersection, subtraction, encompassing)
85
- **Iteration**: Generator-based range iteration with flexible step intervals
86
- **Conversion**: Flexible string formatting and parsing with timezone support
87
88
## Capabilities
89
90
### Range Creation and Configuration
91
92
Create and configure datetime ranges with flexible input formats and output customization.
93
94
```python { .api }
95
class DateTimeRange:
96
# Class constants
97
NOT_A_TIME_STR = "NaT" # String representation for invalid/null time values
98
99
def __init__(
100
self,
101
start_datetime=None,
102
end_datetime=None,
103
start_time_format="%Y-%m-%dT%H:%M:%S%z",
104
end_time_format="%Y-%m-%dT%H:%M:%S%z"
105
):
106
"""
107
Initialize a DateTimeRange instance.
108
109
Parameters:
110
- start_datetime: datetime/str, start time of the range (None for empty)
111
- end_datetime: datetime/str, end time of the range (None for empty)
112
- start_time_format: str, strftime format for start datetime string conversion
113
- end_time_format: str, strftime format for end datetime string conversion
114
"""
115
116
@classmethod
117
def from_range_text(
118
cls,
119
range_text: str,
120
separator: str = "-",
121
start_time_format: Optional[str] = None,
122
end_time_format: Optional[str] = None,
123
) -> "DateTimeRange":
124
"""
125
Create DateTimeRange from text representation.
126
127
Parameters:
128
- range_text: str, text containing datetime range (e.g., "2021-01-01T10:00:00 - 2021-01-01T11:00:00")
129
- separator: str, text separating start and end times (default "-")
130
- start_time_format: Optional[str], strftime format for start time
131
- end_time_format: Optional[str], strftime format for end time
132
133
Returns:
134
DateTimeRange instance created from the text
135
"""
136
```
137
138
Configuration attributes:
139
```python { .api }
140
# Instance attributes for customization
141
time_range.start_time_format = "%Y/%m/%d %H:%M:%S" # str: start datetime format
142
time_range.end_time_format = "%Y/%m/%d %H:%M:%S" # str: end datetime format
143
time_range.is_output_elapse = True # bool: show elapsed time in repr
144
time_range.separator = " to " # str: separator in string representation
145
```
146
147
### Range Properties and Access
148
149
Access range boundaries and calculated values.
150
151
```python { .api }
152
@property
153
def start_datetime(self) -> datetime.datetime:
154
"""
155
Start time of the range.
156
157
Returns:
158
datetime.datetime object representing the start time
159
"""
160
161
@property
162
def end_datetime(self) -> datetime.datetime:
163
"""
164
End time of the range.
165
166
Returns:
167
datetime.datetime object representing the end time
168
"""
169
170
@property
171
def timedelta(self) -> datetime.timedelta:
172
"""
173
Time difference between end and start.
174
175
Returns:
176
datetime.timedelta representing (end_datetime - start_datetime)
177
"""
178
```
179
180
### Range Validation and State Checking
181
182
Validate range integrity and check range state.
183
184
```python { .api }
185
def is_set(self) -> bool:
186
"""
187
Check if both start and end datetimes are set.
188
189
Returns:
190
bool: True if both start_datetime and end_datetime are not None
191
"""
192
193
def is_valid_timerange(self) -> bool:
194
"""
195
Check if the time range is valid.
196
197
Returns:
198
bool: True if range is set and has no time inversion
199
"""
200
201
def validate_time_inversion(self) -> None:
202
"""
203
Validate that start time is not greater than end time.
204
205
Raises:
206
ValueError: if start_datetime > end_datetime
207
TypeError: if either datetime is invalid
208
"""
209
```
210
211
### Range Manipulation and Modification
212
213
Modify range boundaries and transform ranges.
214
215
```python { .api }
216
def set_start_datetime(self, value, timezone=None) -> None:
217
"""
218
Set the start datetime of the range.
219
220
Parameters:
221
- value: datetime/str, new start datetime
222
- timezone: Optional[timezone], timezone for string conversion (used when value is string)
223
224
Raises:
225
ValueError: if value is invalid datetime
226
"""
227
228
def set_end_datetime(self, value, timezone=None) -> None:
229
"""
230
Set the end datetime of the range.
231
232
Parameters:
233
- value: datetime/str, new end datetime
234
- timezone: Optional[timezone], timezone for string conversion (used when value is string)
235
236
Raises:
237
ValueError: if value is invalid datetime
238
"""
239
240
def set_time_range(self, start, end) -> None:
241
"""
242
Set both start and end datetimes.
243
244
Parameters:
245
- start: datetime/str, start datetime
246
- end: datetime/str, end datetime
247
"""
248
249
def truncate(self, percentage: float) -> None:
250
"""
251
Truncate percentage/2 of time from both ends of range.
252
253
Parameters:
254
- percentage: float, percentage to truncate (0-100)
255
256
Raises:
257
ValueError: if percentage < 0
258
"""
259
```
260
261
### Range Operations and Set Theory
262
263
Perform mathematical operations between ranges.
264
265
```python { .api }
266
def intersection(self, x: "DateTimeRange") -> "DateTimeRange":
267
"""
268
Get intersection (overlap) of this range with another.
269
270
Parameters:
271
- x: DateTimeRange, range to intersect with
272
273
Returns:
274
DateTimeRange representing the overlapping time period
275
"""
276
277
def is_intersection(self, x: "DateTimeRange") -> bool:
278
"""
279
Check if this range intersects with another.
280
281
Parameters:
282
- x: DateTimeRange, range to check intersection with
283
284
Returns:
285
bool: True if ranges overlap
286
"""
287
288
def subtract(self, x: "DateTimeRange") -> List["DateTimeRange"]:
289
"""
290
Remove another range from this one.
291
292
Parameters:
293
- x: DateTimeRange, range to subtract from this range
294
295
Returns:
296
List[DateTimeRange]: remaining ranges after subtraction
297
- Empty list if x wholly encompasses this range
298
- [self.copy()] if no overlap
299
- [new_range] if partial overlap
300
- [range1, range2] if x creates a gap in the middle
301
"""
302
303
def encompass(self, x: "DateTimeRange") -> "DateTimeRange":
304
"""
305
Create range that encompasses both this range and another.
306
307
Parameters:
308
- x: DateTimeRange, range to encompass with this range
309
310
Returns:
311
DateTimeRange spanning from earliest start to latest end
312
"""
313
314
def split(self, separator: Union[str, datetime.datetime]) -> List["DateTimeRange"]:
315
"""
316
Split range into two ranges at specified datetime.
317
318
Parameters:
319
- separator: Union[str, datetime.datetime], datetime to split the range at
320
321
Returns:
322
List[DateTimeRange]:
323
- [original_copy] if separator not in range or at boundaries
324
- [range1, range2] if valid split (separator included in both)
325
"""
326
```
327
328
### Range Containment and Testing
329
330
Test containment relationships between ranges and datetimes.
331
332
```python { .api }
333
def __contains__(self, x) -> bool:
334
"""
335
Test if datetime or range is contained within this range.
336
337
Parameters:
338
- x: datetime/DateTimeRange/str, value to test containment
339
340
Returns:
341
bool: True if x is fully contained within this range
342
343
For DateTimeRange: x.start >= self.start and x.end <= self.end
344
For datetime: self.start <= x <= self.end
345
"""
346
```
347
348
### Range Iteration and Enumeration
349
350
Iterate through ranges with custom step intervals.
351
352
```python { .api }
353
def range(self, step) -> Iterator[datetime.datetime]:
354
"""
355
Generate datetime values from start to end by step interval.
356
357
Parameters:
358
- step: timedelta/relativedelta, interval between generated values
359
360
Returns:
361
Iterator[datetime.datetime]: sequence of datetime values
362
363
Raises:
364
ValueError: if step is zero or has wrong sign for range direction
365
"""
366
```
367
368
### String Conversion and Formatting
369
370
Convert ranges to formatted string representations.
371
372
```python { .api }
373
def get_start_time_str(self) -> str:
374
"""
375
Get formatted string representation of start datetime.
376
377
Returns:
378
str: start_datetime formatted with start_time_format, or "NaT" if invalid
379
"""
380
381
def get_end_time_str(self) -> str:
382
"""
383
Get formatted string representation of end datetime.
384
385
Returns:
386
str: end_datetime formatted with end_time_format, or "NaT" if invalid
387
"""
388
389
def get_timedelta_second(self) -> float:
390
"""
391
Get time difference in seconds.
392
393
Returns:
394
float: total seconds between start and end datetimes
395
"""
396
397
def __repr__(self) -> str:
398
"""
399
String representation of the range.
400
401
Returns:
402
str: formatted as "start_str - end_str" with optional elapsed time
403
"""
404
```
405
406
### Range Arithmetic Operations
407
408
Perform arithmetic operations with timedelta objects.
409
410
```python { .api }
411
def __add__(self, other) -> "DateTimeRange":
412
"""
413
Add timedelta to both start and end times.
414
415
Parameters:
416
- other: timedelta, time amount to add
417
418
Returns:
419
DateTimeRange: new range with both times shifted forward
420
"""
421
422
def __iadd__(self, other) -> "DateTimeRange":
423
"""
424
In-place addition of timedelta to both times.
425
426
Parameters:
427
- other: timedelta, time amount to add
428
429
Returns:
430
DateTimeRange: self after modification
431
"""
432
433
def __sub__(self, other) -> "DateTimeRange":
434
"""
435
Subtract timedelta from both start and end times.
436
437
Parameters:
438
- other: timedelta, time amount to subtract
439
440
Returns:
441
DateTimeRange: new range with both times shifted backward
442
"""
443
444
def __isub__(self, other) -> "DateTimeRange":
445
"""
446
In-place subtraction of timedelta from both times.
447
448
Parameters:
449
- other: timedelta, time amount to subtract
450
451
Returns:
452
DateTimeRange: self after modification
453
"""
454
```
455
456
### Range Comparison Operations
457
458
Compare ranges for equality and inequality.
459
460
```python { .api }
461
def __eq__(self, other) -> bool:
462
"""
463
Test equality with another DateTimeRange.
464
465
Parameters:
466
- other: DateTimeRange, range to compare with
467
468
Returns:
469
bool: True if both start and end datetimes are equal
470
"""
471
472
def __ne__(self, other) -> bool:
473
"""
474
Test inequality with another DateTimeRange.
475
476
Parameters:
477
- other: DateTimeRange, range to compare with
478
479
Returns:
480
bool: True if start or end datetimes differ
481
"""
482
```
483
484
## Type Definitions
485
486
```python { .api }
487
# Core types used throughout the API
488
datetime.datetime # Standard library datetime object
489
datetime.timedelta # Standard library timedelta object
490
str # String type for datetime representations
491
List[T] # Generic list type (from typing module)
492
Iterator[T] # Iterator protocol type (from typing module)
493
bool # Boolean type
494
float # Floating point number type
495
Optional[T] # Type that can be T or None (from typing module)
496
Union[T, U] # Type that can be T or U (from typing module)
497
498
# Time step types for iteration
499
from dateutil.relativedelta import relativedelta # Relative time deltas with months/years
500
501
# Import pattern for extended functionality
502
import datetime
503
from typing import List, Optional, Union, Iterator
504
from dateutil.relativedelta import relativedelta
505
```
506
507
## Usage Examples
508
509
### Time Range Validation and Error Handling
510
511
```python
512
from datetimerange import DateTimeRange
513
514
# Create ranges and validate
515
time_range = DateTimeRange("2015-01-01T10:00:00", "2015-01-01T09:00:00")
516
try:
517
time_range.validate_time_inversion()
518
except ValueError as e:
519
print(f"Invalid range: {e}")
520
# Fix the range
521
time_range.set_time_range("2015-01-01T09:00:00", "2015-01-01T10:00:00")
522
523
print(time_range.is_valid_timerange()) # True
524
```
525
526
### Complex Range Operations
527
528
```python
529
import datetime
530
from datetimerange import DateTimeRange
531
532
# Create overlapping ranges
533
range1 = DateTimeRange("2015-01-01T10:00:00", "2015-01-01T15:00:00")
534
range2 = DateTimeRange("2015-01-01T12:00:00", "2015-01-01T18:00:00")
535
536
# Find intersection
537
overlap = range1.intersection(range2)
538
print(f"Overlap: {overlap}") # 2015-01-01T12:00:00 - 2015-01-01T15:00:00
539
540
# Subtract one range from another
541
remaining = range1.subtract(range2)
542
print(f"Remaining: {remaining}") # [2015-01-01T10:00:00 - 2015-01-01T12:00:00]
543
544
# Create encompassing range
545
combined = range1.encompass(range2)
546
print(f"Combined: {combined}") # 2015-01-01T10:00:00 - 2015-01-01T18:00:00
547
```
548
549
### Range Iteration and Time Series
550
551
```python
552
import datetime
553
from dateutil.relativedelta import relativedelta
554
from datetimerange import DateTimeRange
555
556
# Daily iteration
557
daily_range = DateTimeRange("2015-01-01T00:00:00", "2015-01-07T00:00:00")
558
for day in daily_range.range(datetime.timedelta(days=1)):
559
print(day.strftime("%Y-%m-%d"))
560
561
# Monthly iteration with relativedelta
562
monthly_range = DateTimeRange("2015-01-01T00:00:00", "2015-12-01T00:00:00")
563
for month in monthly_range.range(relativedelta(months=1)):
564
print(month.strftime("%Y-%m"))
565
566
# Hourly iteration for a day
567
hourly_range = DateTimeRange("2015-01-01T00:00:00", "2015-01-02T00:00:00")
568
business_hours = []
569
for hour in hourly_range.range(datetime.timedelta(hours=1)):
570
if 9 <= hour.hour <= 17: # Business hours
571
business_hours.append(hour)
572
```
573
574
### Range Arithmetic and Shifting
575
576
```python
577
import datetime
578
from datetimerange import DateTimeRange
579
580
# Create base range
581
base_range = DateTimeRange("2015-01-01T10:00:00", "2015-01-01T12:00:00")
582
print(f"Original: {base_range}")
583
584
# Shift forward by 2 hours
585
shifted_forward = base_range + datetime.timedelta(hours=2)
586
print(f"Shifted forward: {shifted_forward}")
587
588
# Shift backward by 30 minutes
589
shifted_back = base_range - datetime.timedelta(minutes=30)
590
print(f"Shifted back: {shifted_back}")
591
592
# In-place modification
593
base_range += datetime.timedelta(days=1)
594
print(f"Next day: {base_range}")
595
```
596
597
### Custom Formatting and Display
598
599
```python
600
from datetimerange import DateTimeRange
601
602
# Create range with custom formatting
603
time_range = DateTimeRange("2015-03-22T10:00:00+0900", "2015-03-22T10:10:00+0900")
604
605
# Customize string format
606
time_range.start_time_format = "%Y/%m/%d %H:%M:%S"
607
time_range.end_time_format = "%Y/%m/%d %H:%M:%S"
608
time_range.separator = " to "
609
print(time_range) # 2015/03/22 10:00:00 to 2015/03/22 10:10:00
610
611
# Show elapsed time
612
time_range.is_output_elapse = True
613
print(time_range) # 2015/03/22 10:00:00 to 2015/03/22 10:10:00 (0:10:00)
614
615
# Get individual formatted strings
616
print(f"Start: {time_range.get_start_time_str()}")
617
print(f"End: {time_range.get_end_time_str()}")
618
print(f"Duration: {time_range.get_timedelta_second()} seconds")
619
```