opendate 0.1.13__tar.gz → 0.1.20__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of opendate might be problematic. Click here for more details.

@@ -0,0 +1,762 @@
1
+ Metadata-Version: 2.4
2
+ Name: opendate
3
+ Version: 0.1.20
4
+ Summary: Python business datetimes
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Author: bissli
8
+ Author-email: bissli.xyz@protonmail.com
9
+ Requires-Python: >=3.9,<4.0
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Provides-Extra: test
19
+ Requires-Dist: asserts ; extra == "test"
20
+ Requires-Dist: bump2version ; extra == "test"
21
+ Requires-Dist: pandas-market-calendars
22
+ Requires-Dist: pdbpp ; extra == "test"
23
+ Requires-Dist: pendulum
24
+ Requires-Dist: pytest ; extra == "test"
25
+ Requires-Dist: regex
26
+ Requires-Dist: typing-extensions
27
+ Requires-Dist: wrapt
28
+ Project-URL: Repository, https://github.com/bissli/opendate
29
+ Description-Content-Type: text/markdown
30
+
31
+ # OpenDate
32
+
33
+ A powerful date and time library for Python, built on top of [Pendulum](https://github.com/sdispater/pendulum) with extensive business day support and financial date calculations.
34
+
35
+ ## Overview
36
+
37
+ OpenDate extends Pendulum's excellent date/time handling with:
38
+
39
+ - **Business Day Calculations**: NYSE calendar by default (extensible to other calendars)
40
+ - **Enhanced Parsing**: Support for special codes, business day offsets, and multiple formats
41
+ - **Financial Functions**: Excel-compatible yearfrac, fractional months, and period calculations
42
+ - **Type Safety**: Comprehensive type annotations and conversion decorators
43
+ - **Timezone Handling**: Smart defaults and easy timezone conversions
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install opendate
49
+ ```
50
+
51
+ ## Quick Start
52
+
53
+ ```python
54
+ from date import Date, DateTime, Time, Interval, EST
55
+
56
+ # Create dates
57
+ today = Date.today()
58
+ meeting = DateTime(2024, 1, 15, 14, 30, tzinfo=EST)
59
+
60
+ # Business day arithmetic
61
+ next_business_day = today.business().add(days=1) # or today.b.add(days=1)
62
+ five_business_days_ago = today.b.subtract(days=5)
63
+
64
+ # Parse various formats
65
+ date = Date.parse('2024-01-15')
66
+ date = Date.parse('01/15/2024')
67
+ date = Date.parse('T-3b') # 3 business days ago
68
+
69
+ # Intervals and ranges
70
+ interval = Interval(Date(2024, 1, 1), Date(2024, 12, 31))
71
+ business_days = interval.b.days # Count only business days
72
+ yearfrac = interval.yearfrac(0) # Financial year fraction
73
+ ```
74
+
75
+ ## Core Classes
76
+
77
+ ### Date
78
+
79
+ Extended `pendulum.Date` with business day awareness:
80
+
81
+ ```python
82
+ from date import Date, NYSE
83
+
84
+ # Create dates
85
+ today = Date.today()
86
+ date = Date(2024, 1, 15)
87
+ parsed = Date.parse('2024-01-15')
88
+
89
+ # Business day operations
90
+ date.business().add(days=5) # Add 5 business days
91
+ date.b.subtract(days=3) # Subtract 3 business days (shorthand)
92
+ date.b.start_of('month') # First business day of month
93
+ date.b.end_of('month') # Last business day of month
94
+
95
+ # Check business day status
96
+ date.is_business_day() # True if NYSE is open
97
+ date.business_hours() # (open_time, close_time) or (None, None)
98
+
99
+ # Period boundaries
100
+ date.start_of('week') # Monday (or next business day if .b)
101
+ date.end_of('month') # Last day of month
102
+ date.first_of('quarter') # First day of quarter
103
+ date.last_of('year') # December 31
104
+
105
+ # Navigation
106
+ date.next(WeekDay.FRIDAY) # Next Friday
107
+ date.previous(WeekDay.MONDAY) # Previous Monday
108
+ date.lookback('month') # One month ago
109
+ ```
110
+
111
+ ### DateTime
112
+
113
+ Extended `pendulum.DateTime` with business day support:
114
+
115
+ **Important differences from Pendulum:**
116
+ - `DateTime.today()` returns start of day (00:00:00) instead of current time like `pendulum.today()`
117
+ - Methods preserve business status and entity when chaining
118
+ - `DateTime.instance()` adds UTC timezone by default if none specified
119
+
120
+ ```python
121
+ from date import DateTime, EST, UTC
122
+
123
+ # Create datetimes (timezone parameter available)
124
+ now = DateTime.now() # Current time in local timezone
125
+ now_utc = DateTime.now(tz=UTC) # Current time in UTC
126
+ dt = DateTime(2024, 1, 15, 9, 30, 0, tzinfo=EST)
127
+ parsed = DateTime.parse('2024-01-15T09:30:00') # Parsed with local timezone
128
+
129
+ # Business day operations (preserves time)
130
+ dt.b.add(days=1) # Next business day at 9:30 AM
131
+ dt.b.subtract(days=3) # Three business days ago at 9:30 AM
132
+
133
+ # Timezone conversions
134
+ dt.in_timezone(UTC) # Convert to UTC
135
+ dt.astimezone(EST) # Alternative syntax
136
+
137
+ # Combine date and time
138
+ from date import Date, Time
139
+ date = Date(2024, 1, 15)
140
+ time = Time(9, 30, tzinfo=EST)
141
+ dt = DateTime.combine(date, time, tzinfo=EST) # tzinfo parameter determines result timezone
142
+
143
+ # Extract components
144
+ dt.date() # Date(2024, 1, 15)
145
+ dt.time() # Time(9, 30, 0, tzinfo=EST)
146
+ ```
147
+
148
+ ### Time
149
+
150
+ Extended `pendulum.Time` with enhanced parsing:
151
+
152
+ ```python
153
+ from date import Time, UTC
154
+
155
+ # Create times (timezone must be specified)
156
+ time = Time(9, 30, 0, tzinfo=UTC)
157
+
158
+ # Parsed times default to UTC timezone
159
+ parsed = Time.parse('9:30 AM') # Returns with tzinfo=UTC
160
+ parsed = Time.parse('14:30:45.123') # Returns with tzinfo=UTC
161
+
162
+ # Supported formats
163
+ Time.parse('9:30') # 09:30:00
164
+ Time.parse('9:30 PM') # 21:30:00
165
+ Time.parse('093015') # 09:30:15
166
+ Time.parse('14:30:45.123456') # With microseconds
167
+
168
+ # Timezone conversion
169
+ time.in_timezone(EST) # Convert to different timezone
170
+ ```
171
+
172
+ ### Interval
173
+
174
+ Extended `pendulum.Interval` with business day and financial calculations:
175
+
176
+ **Note:** Unlike Pendulum's `Interval.months` (which returns int), OpenDate's returns float with fractional months calculated from actual day counts.
177
+
178
+ ```python
179
+ from date import Date, Interval, NYSE
180
+
181
+ # Create intervals
182
+ start = Date(2024, 1, 1)
183
+ end = Date(2024, 12, 31)
184
+ interval = Interval(start, end)
185
+
186
+ # Basic properties
187
+ interval.days # 365 (calendar days)
188
+ interval.months # 12.0 (float with fractional months)
189
+ interval.years # 1 (always floors to int)
190
+ interval.quarters # 4.0 (approximate, based on days/365*4)
191
+
192
+ # Business day calculations
193
+ interval.b.days # ~252 (only business days)
194
+ interval.entity(NYSE).b.days # Explicitly set calendar entity
195
+
196
+ # Check which days are business days
197
+ business_flags = list(interval.is_business_day_range())
198
+ # [True, True, True, False, True, ...] # Mon-Fri are True, Sat-Sun are False
199
+
200
+ # Iterate over interval with different units
201
+ for date in interval.range('days'):
202
+ print(date) # Every day
203
+
204
+ for date in interval.range('weeks'):
205
+ print(date) # Every Monday
206
+
207
+ for date in interval.range('months'):
208
+ print(date) # First of each month
209
+
210
+ for date in interval.range('years'):
211
+ print(date) # January 1st of each year
212
+
213
+ # Business days only (works with 'days' unit)
214
+ for date in interval.b.range('days'):
215
+ print(date) # Only business days (skips weekends/holidays)
216
+
217
+ # Get period boundaries within interval
218
+ month_starts = interval.start_of('month')
219
+ # [2024-01-01, 2024-02-01, ..., 2024-12-01]
220
+
221
+ month_ends = interval.end_of('month')
222
+ # [2024-01-31, 2024-02-29, ..., 2024-12-31]
223
+
224
+ week_starts = interval.start_of('week')
225
+ # All Mondays in the interval
226
+
227
+ week_ends = interval.end_of('week')
228
+ # All Sundays in the interval
229
+
230
+ quarter_starts = interval.start_of('quarter')
231
+ # [2024-01-01, 2024-04-01, 2024-07-01, 2024-10-01]
232
+
233
+ year_starts = interval.start_of('year')
234
+ # [2024-01-01]
235
+
236
+ # Business day adjustments for period boundaries
237
+ # When a period start/end falls on a non-business day, it's automatically adjusted
238
+ interval_2018 = Interval(Date(2018, 1, 5), Date(2018, 4, 5))
239
+
240
+ # Start of month - shifts forward to next business day if needed
241
+ business_month_starts = interval_2018.b.start_of('month')
242
+ # [2018-01-02, 2018-02-01, 2018-03-01, 2018-04-02]
243
+ # Note: Jan 1 is holiday → Jan 2, Apr 1 is Sunday → Apr 2
244
+
245
+ # End of month - shifts backward to previous business day if needed
246
+ business_month_ends = interval_2018.b.end_of('month')
247
+ # [2018-01-31, 2018-02-28, 2018-03-29, 2018-04-30]
248
+ # Note: Mar 30 is Good Friday → Mar 29
249
+
250
+ # Works for all time units
251
+ interval_weeks = Interval(Date(2018, 1, 5), Date(2018, 1, 25))
252
+ business_week_starts = interval_weeks.b.start_of('week')
253
+ # [2018-01-02, 2018-01-08, 2018-01-16, 2018-01-22]
254
+ # Note: Jan 1 (Mon) is holiday → Jan 2, Jan 15 (Mon) is MLK Day → Jan 16
255
+
256
+ business_week_ends = interval_weeks.b.end_of('week')
257
+ # [2018-01-05, 2018-01-12, 2018-01-19, 2018-01-26]
258
+ # All Fridays (already business days)
259
+
260
+ interval_years = Interval(Date(2017, 6, 1), Date(2019, 6, 1))
261
+ business_year_starts = interval_years.b.start_of('year')
262
+ # [2017-01-03, 2018-01-02, 2019-01-02]
263
+ # Jan 1 falls on holidays/weekends → adjusted to first business day
264
+
265
+ business_year_ends = interval_years.b.end_of('year')
266
+ # [2017-12-29, 2018-12-31, 2019-12-31]
267
+ # Dec 31 on weekends → adjusted to last business day before
268
+
269
+ # Financial calculations (Excel-compatible)
270
+ interval.yearfrac(0) # US 30/360 basis (corporate bonds)
271
+ interval.yearfrac(1) # Actual/actual (Treasury bonds)
272
+ interval.yearfrac(2) # Actual/360 (money market)
273
+ interval.yearfrac(3) # Actual/365 (some bonds)
274
+ interval.yearfrac(4) # European 30/360 (Eurobonds)
275
+
276
+ # Negative intervals (when end < start)
277
+ backward = Interval(Date(2024, 12, 31), Date(2024, 1, 1))
278
+ backward.days # -365
279
+ backward.months # -12.0
280
+ backward.years # -1
281
+ ```
282
+
283
+ ## Enhanced Parsing
284
+
285
+ ### Date Parsing
286
+
287
+ ```python
288
+ from date import Date
289
+
290
+ # Standard formats
291
+ Date.parse('2024-01-15') # YYYY-MM-DD
292
+ Date.parse('01/15/2024') # MM/DD/YYYY
293
+ Date.parse('01/15/24') # MM/DD/YY
294
+ Date.parse('20240115') # YYYYMMDD
295
+
296
+ # Named months
297
+ Date.parse('15-Jan-2024') # DD-MON-YYYY
298
+ Date.parse('Jan 15, 2024') # MON DD, YYYY
299
+ Date.parse('January 15, 2024') # Full month name
300
+ Date.parse('15JAN2024') # Compact
301
+
302
+ # Special codes
303
+ Date.parse('T') # Today
304
+ Date.parse('Y') # Yesterday
305
+ Date.parse('P') # Previous business day
306
+ Date.parse('M') # Last day of previous month
307
+
308
+ # Date arithmetic with parsing
309
+ Date.parse('T-5') # 5 days ago
310
+ Date.parse('T+10') # 10 days from now
311
+ Date.parse('T-3b') # 3 business days ago
312
+ Date.parse('P+2b') # 2 business days after previous business day
313
+ ```
314
+
315
+ ### DateTime Parsing
316
+
317
+ ```python
318
+ from date import DateTime
319
+
320
+ # ISO 8601
321
+ DateTime.parse('2024-01-15T09:30:00')
322
+ DateTime.parse('2024-01-15T09:30:00Z')
323
+
324
+ # Date and time separated
325
+ DateTime.parse('2024-01-15 09:30:00')
326
+ DateTime.parse('01/15/2024 09:30:00')
327
+
328
+ # Unix timestamps
329
+ DateTime.parse(1640995200) # Seconds
330
+ DateTime.parse(1640995200000) # Milliseconds (auto-detected)
331
+
332
+ # Special codes (returns start of day)
333
+ DateTime.parse('T') # Today at 00:00:00
334
+ DateTime.parse('Y') # Yesterday at 00:00:00
335
+ DateTime.parse('P') # Previous business day at 00:00:00
336
+ ```
337
+
338
+ ## Business Day Operations
339
+
340
+ ### Calendar Entities
341
+
342
+ ```python
343
+ from date import Date, NYSE, Entity
344
+
345
+ # Use default NYSE calendar
346
+ date = Date.today().business().add(days=5)
347
+
348
+ # Set entity explicitly
349
+ date = Date.today().entity(NYSE).business().add(days=5)
350
+
351
+ # Check business day status
352
+ Date(2024, 1, 1).is_business_day() # False (New Year's Day)
353
+ Date(2024, 7, 4).is_business_day() # False (Independence Day)
354
+ Date(2024, 12, 25).is_business_day() # False (Christmas)
355
+
356
+ # Get market hours
357
+ Date(2024, 1, 15).business_hours() # (09:30 AM EST, 04:00 PM EST)
358
+ Date(2024, 1, 1).business_hours() # (None, None) - holiday
359
+ ```
360
+
361
+ ### Business Day Arithmetic
362
+
363
+ ```python
364
+ from date import Date
365
+
366
+ date = Date(2024, 1, 15)
367
+
368
+ # Add/subtract business days
369
+ date.b.add(days=5) # 5 business days later
370
+ date.b.subtract(days=3) # 3 business days earlier
371
+
372
+ # Works across weekends and holidays
373
+ Date(2024, 3, 29).b.add(days=1) # 2024-04-01 (skips Good Friday + weekend)
374
+
375
+ # Period boundaries with business days
376
+ Date(2024, 7, 6).b.start_of('month') # 2024-07-05 (skips July 4th)
377
+ Date(2024, 4, 30).b.end_of('month') # 2024-04-30 (Tuesday, not Sunday)
378
+ ```
379
+
380
+ ## Financial Calculations
381
+
382
+ ### Year Fractions
383
+
384
+ Excel-compatible year fraction calculations for financial formulas:
385
+
386
+ ```python
387
+ from date import Date, Interval
388
+
389
+ start = Date(2024, 1, 1)
390
+ end = Date(2024, 12, 31)
391
+ interval = Interval(start, end)
392
+
393
+ # Different day count conventions
394
+ interval.yearfrac(0) # US (NASD) 30/360 - common for US corporate bonds
395
+ interval.yearfrac(1) # Actual/actual - US Treasury bonds
396
+ interval.yearfrac(2) # Actual/360 - money market instruments
397
+ interval.yearfrac(3) # Actual/365 - some corporate bonds
398
+ interval.yearfrac(4) # European 30/360 - Eurobonds
399
+ ```
400
+
401
+ ### Fractional Periods
402
+
403
+ **Note:** `Interval.months` returns float (unlike Pendulum which returns int).
404
+ Fractional months are calculated based on actual day counts within partial months.
405
+
406
+ ```python
407
+ from date import Date, Interval
408
+
409
+ # Fractional months (not just integer count)
410
+ Interval(Date(2024, 1, 1), Date(2024, 2, 15)).months # ~1.5
411
+ Interval(Date(2024, 1, 15), Date(2024, 2, 14)).months # ~0.97
412
+ Interval(Date(2024, 1, 1), Date(2024, 2, 1)).months # 1.0 (exactly)
413
+
414
+ # Approximate quarters
415
+ Interval(Date(2024, 1, 1), Date(2024, 3, 31)).quarters # ~1.0
416
+ ```
417
+
418
+ ## Timezone Handling
419
+
420
+ ```python
421
+ from date import DateTime, EST, UTC, GMT, LCL, Timezone
422
+
423
+ # Built-in timezones
424
+ dt_est = DateTime(2024, 1, 15, 9, 30, tzinfo=EST) # US/Eastern (same as America/New_York)
425
+ dt_utc = DateTime(2024, 1, 15, 14, 30, tzinfo=UTC) # UTC
426
+ dt_gmt = DateTime(2024, 1, 15, 14, 30, tzinfo=GMT) # GMT
427
+ dt_lcl = DateTime.now(tz=LCL) # Local timezone (from system)
428
+
429
+ # Custom timezones
430
+ tokyo = Timezone('Asia/Tokyo')
431
+ dt_tokyo = DateTime(2024, 1, 15, 23, 30, tzinfo=tokyo)
432
+
433
+ # Convert between timezones (preserves the instant in time, changes timezone)
434
+ dt_est.in_timezone(UTC) # 9:30 AM EST → 2:30 PM UTC
435
+ dt_utc.in_tz(EST) # 2:30 PM UTC → 9:30 AM EST (shorthand)
436
+ dt_est.astimezone(tokyo) # 9:30 AM EST → 11:30 PM JST
437
+ ```
438
+
439
+ ## Helper Functions and Decorators
440
+
441
+ ### Type Conversion Decorators
442
+
443
+ ```python
444
+ from date import expect_date, expect_datetime, expect_time
445
+ import datetime
446
+
447
+ @expect_date
448
+ def process_date(d):
449
+ return d.add(days=1)
450
+
451
+ # Automatically converts datetime.date to Date
452
+ result = process_date(datetime.date(2024, 1, 15)) # Returns Date object
453
+
454
+ @expect_datetime
455
+ def process_datetime(dt):
456
+ return dt.add(hours=1)
457
+
458
+ # Automatically converts to DateTime
459
+ result = process_datetime(datetime.datetime(2024, 1, 15, 9, 0))
460
+ ```
461
+
462
+ ### Timezone Decorators
463
+
464
+ ```python
465
+ from date import prefer_utc_timezone, expect_utc_timezone
466
+
467
+ @prefer_utc_timezone
468
+ def get_timestamp():
469
+ return DateTime(2024, 1, 15, 9, 0) # Adds UTC if no timezone
470
+
471
+ @expect_utc_timezone
472
+ def get_utc_time():
473
+ return DateTime(2024, 1, 15, 9, 0, tzinfo=EST) # Forces to UTC
474
+ ```
475
+
476
+ ## Legacy Compatibility Functions
477
+
478
+ The `date.extras` module provides standalone functions for backward compatibility:
479
+
480
+ ```python
481
+ from date import is_business_day, is_within_business_hours
482
+ from date import overlap_days
483
+ from date import Date, Interval
484
+
485
+ # Check current time against market hours
486
+ is_business_day() # Is today a business day?
487
+ is_within_business_hours() # Is it between market open/close?
488
+
489
+ # Calculate interval overlap
490
+ interval1 = (Date(2024, 1, 1), Date(2024, 1, 31))
491
+ interval2 = (Date(2024, 1, 15), Date(2024, 2, 15))
492
+
493
+ # Boolean check - do they overlap?
494
+ overlap_days(interval1, interval2) # True (they overlap)
495
+
496
+ # Get actual day count of overlap
497
+ overlap_days(interval1, interval2, days=True) # 17 (days of overlap)
498
+
499
+ # Works with Interval objects too
500
+ int1 = Interval(Date(2024, 1, 1), Date(2024, 1, 31))
501
+ int2 = Interval(Date(2024, 1, 15), Date(2024, 2, 15))
502
+ overlap_days(int1, int2, days=True) # 17
503
+
504
+ # Non-overlapping intervals return negative
505
+ int3 = Interval(Date(2024, 1, 1), Date(2024, 1, 10))
506
+ int4 = Interval(Date(2024, 1, 20), Date(2024, 1, 31))
507
+ overlap_days(int3, int4) # False
508
+ overlap_days(int3, int4, days=True) # -9 (negative = no overlap)
509
+ ```
510
+
511
+ ## Advanced Features
512
+
513
+ ### Method Chaining
514
+
515
+ Date and DateTime operations preserve type and state (business mode, entity), allowing for clean method chaining:
516
+
517
+ ```python
518
+ from date import Date, NYSE
519
+
520
+ result = Date(2024, 1, 15)\
521
+ .entity(NYSE)\
522
+ .business()\
523
+ .end_of('month')\
524
+ .subtract(days=5)\
525
+ .start_of('week')
526
+ ```
527
+
528
+ ### Custom Date Navigation
529
+
530
+ ```python
531
+ from date import Date, WeekDay
532
+
533
+ date = Date(2024, 1, 15)
534
+
535
+ # Nth occurrence of weekday in period
536
+ date.nth_of('month', 3, WeekDay.WEDNESDAY) # 3rd Wednesday of month
537
+
538
+ # Named day navigation
539
+ date.next(WeekDay.FRIDAY) # Next Friday
540
+ date.previous(WeekDay.MONDAY) # Previous Monday
541
+
542
+ # Relative date finding
543
+ date.closest(date1, date2) # Closest of two dates
544
+ date.farthest(date1, date2) # Farthest of two dates
545
+ date.average(other_date) # Average of two dates
546
+ ```
547
+
548
+ ### Lookback Operations
549
+
550
+ ```python
551
+ from date import Date
552
+
553
+ date = Date(2024, 1, 15)
554
+
555
+ date.lookback('day') # Yesterday
556
+ date.lookback('week') # One week ago
557
+ date.lookback('month') # One month ago
558
+ date.lookback('quarter') # One quarter ago
559
+ date.lookback('year') # One year ago
560
+
561
+ # With business mode
562
+ date.b.lookback('month') # One month ago, adjusted to business day
563
+ ```
564
+
565
+ ## Compatibility
566
+
567
+ OpenDate maintains compatibility with:
568
+
569
+ - **Pendulum**: Most Pendulum methods work as expected, with some notable differences:
570
+ - `Interval.months` returns float (with fractional months) instead of int
571
+ - `DateTime.today()` returns start of day (00:00:00) instead of current time
572
+ - Methods preserve business day status and entity when chaining
573
+ - **Python datetime**: Seamless conversion via `instance()` methods
574
+ - **Pandas**: Works with pandas Timestamp and datetime64
575
+ - **NumPy**: Supports numpy datetime64 conversion
576
+
577
+ ```python
578
+ from date import Date, DateTime
579
+ import datetime
580
+ import pandas as pd
581
+ import numpy as np
582
+
583
+ # From Python datetime
584
+ Date.instance(datetime.date(2024, 1, 15))
585
+ DateTime.instance(datetime.datetime(2024, 1, 15, 9, 30))
586
+
587
+ # From Pandas
588
+ Date.instance(pd.Timestamp('2024-01-15'))
589
+ DateTime.instance(pd.Timestamp('2024-01-15 09:30:00'))
590
+
591
+ # From NumPy
592
+ Date.instance(np.datetime64('2024-01-15'))
593
+ DateTime.instance(np.datetime64('2024-01-15T09:30:00'))
594
+ ```
595
+
596
+ ## Why OpenDate?
597
+
598
+ ### Over Pendulum
599
+
600
+ - Business day calculations with holiday awareness
601
+ - Financial functions (yearfrac, fractional periods)
602
+ - Enhanced parsing with special codes and business day offsets
603
+ - Built-in NYSE calendar (extensible to others)
604
+ - `Interval.months` returns float for precise financial calculations
605
+ - `DateTime.today()` returns start of day for consistent behavior
606
+
607
+ **Note:** Some Pendulum behavior is intentionally modified for financial use cases.
608
+
609
+ ### Over datetime
610
+
611
+ - All benefits of Pendulum (better API, timezone handling, etc.)
612
+ - Plus all OpenDate business day features
613
+ - Cleaner syntax for common date operations
614
+ - Period-aware calculations
615
+
616
+ ### Over pandas
617
+
618
+ - Lighter weight for non-DataFrame operations
619
+ - Better business day support
620
+ - Cleaner API for date arithmetic
621
+ - Financial functions built-in
622
+
623
+ ## Examples
624
+
625
+ ### Generate Month-End Dates
626
+
627
+ ```python
628
+ from date import Date, Interval
629
+
630
+ interval = Interval(Date(2024, 1, 1), Date(2024, 12, 31))
631
+
632
+ # Get all month-end dates
633
+ month_ends = interval.end_of('month')
634
+ # [2024-01-31, 2024-02-29, ..., 2024-12-31]
635
+
636
+ # Get all month-start dates
637
+ month_starts = interval.start_of('month')
638
+ # [2024-01-01, 2024-02-01, ..., 2024-12-01]
639
+
640
+ # Get business day month-ends (adjusts for weekends/holidays)
641
+ business_month_ends = interval.b.end_of('month')
642
+ # Automatically adjusts any month-end that falls on non-business day
643
+ # to the previous business day
644
+
645
+ # Get business day month-starts (adjusts for weekends/holidays)
646
+ business_month_starts = interval.b.start_of('month')
647
+ # Automatically adjusts any month-start that falls on non-business day
648
+ # to the next business day
649
+
650
+ # Works with other periods too
651
+ quarter_ends = interval.end_of('quarter')
652
+ # [2024-03-31, 2024-06-30, 2024-09-30, 2024-12-31]
653
+
654
+ business_quarter_ends = interval.b.end_of('quarter')
655
+ # Quarter-ends adjusted to business days
656
+
657
+ week_starts = interval.start_of('week')
658
+ # All Mondays in 2024
659
+
660
+ business_week_starts = interval.b.start_of('week')
661
+ # All week starts adjusted to business days (skips holidays on Mondays)
662
+
663
+ # Partial year example
664
+ partial = Interval(Date(2024, 3, 15), Date(2024, 7, 20))
665
+ partial.end_of('month')
666
+ # [2024-03-31, 2024-04-30, 2024-05-31, 2024-06-30, 2024-07-31]
667
+
668
+ partial.b.end_of('month')
669
+ # Same as above but adjusted for any non-business days
670
+ ```
671
+
672
+ ### Calculate Business Days Between Dates
673
+
674
+ ```python
675
+ from date import Date, Interval
676
+
677
+ start = Date(2024, 1, 1)
678
+ end = Date(2024, 12, 31)
679
+ interval = Interval(start, end)
680
+
681
+ # Count business days
682
+ business_days = interval.b.days # ~252
683
+
684
+ # Count calendar days
685
+ calendar_days = interval.days # 365
686
+
687
+ # Get list of which days are business days
688
+ is_bday = list(interval.is_business_day_range())
689
+ # [True, True, False, False, True, ...]
690
+
691
+ # Iterate only over business days
692
+ for bday in interval.b.range('days'):
693
+ print(f"{bday} is a business day")
694
+ ```
695
+
696
+ ### Find Next Options Expiration (3rd Friday)
697
+
698
+ ```python
699
+ from date import Date, WeekDay
700
+
701
+ today = Date.today()
702
+ third_friday = today.add(months=1).start_of('month').nth_of('month', 3, WeekDay.FRIDAY)
703
+ ```
704
+
705
+ ### Working with Market Hours
706
+
707
+ ```python
708
+ from date import DateTime, NYSE
709
+
710
+ now = DateTime.now(tz=NYSE.tz)
711
+
712
+ if now.is_business_day():
713
+ open_time, close_time = now.business_hours()
714
+ if open_time <= now <= close_time:
715
+ print("Market is open")
716
+ ```
717
+
718
+ ### Calculate Interest Accrual
719
+
720
+ ```python
721
+ from date import Date, Interval
722
+
723
+ issue_date = Date(2024, 1, 15)
724
+ settlement_date = Date(2024, 6, 15)
725
+ coupon_rate = 0.05
726
+
727
+ # Using Actual/360 convention (basis 2)
728
+ days_fraction = Interval(issue_date, settlement_date).yearfrac(2)
729
+ accrued_interest = coupon_rate * days_fraction
730
+ ```
731
+
732
+ ## Testing
733
+
734
+ OpenDate includes comprehensive test coverage:
735
+
736
+ ```bash
737
+ # Run all tests
738
+ pytest
739
+
740
+ # Run specific test files
741
+ pytest tests/test_date.py
742
+ pytest tests/test_business.py
743
+ pytest tests/test_interval.py
744
+
745
+ # Run with coverage
746
+ pytest --cov=date tests/
747
+ ```
748
+
749
+ ## Contributing
750
+
751
+ OpenDate is open source (MIT License). Contributions welcome!
752
+
753
+ ## License
754
+
755
+ MIT License - see LICENSE file for details.
756
+
757
+ ## Credits
758
+
759
+ Built on top of the excellent [Pendulum](https://github.com/sdispater/pendulum) library by Sébastien Eustace.
760
+
761
+ Business day calendars provided by [pandas-market-calendars](https://github.com/rsheftel/pandas_market_calendars).
762
+