datefind 0.1.0__py3-none-any.whl
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.
- datefind/__init__.py +44 -0
- datefind/constants.py +82 -0
- datefind/datefind.py +192 -0
- datefind/py.typed +0 -0
- datefind/utils/__init__.py +5 -0
- datefind/utils/date_factory.py +163 -0
- datefind-0.1.0.dist-info/METADATA +85 -0
- datefind-0.1.0.dist-info/RECORD +10 -0
- datefind-0.1.0.dist-info/WHEEL +4 -0
- datefind-0.1.0.dist-info/licenses/LICENSE +9 -0
datefind/__init__.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Datefind."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Generator
|
|
4
|
+
from typing import Literal
|
|
5
|
+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
6
|
+
|
|
7
|
+
from tzlocal import get_localzone
|
|
8
|
+
|
|
9
|
+
from datefind.constants import FirstNumber
|
|
10
|
+
from datefind.datefind import Date, DateFind
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def find_dates(
|
|
14
|
+
text: str, first: Literal["month", "day", "year"] = "month", tz: str = ""
|
|
15
|
+
) -> Generator[Date, None, None]:
|
|
16
|
+
"""Search text for dates and return them as Date objects.
|
|
17
|
+
|
|
18
|
+
Parse the input text to locate and extract dates in various formats. When ambiguous dates are encountered (e.g. 01/02/03), use the `first` parameter to determine the order of month/day/year. Convert all dates to the specified timezone.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
text (str): The text to search for dates
|
|
22
|
+
first (Literal["month", "day", "year"]): The position of month/day/year when parsing ambiguous dates. Defaults to "month"
|
|
23
|
+
tz (str): The timezone name (e.g. "America/New_York") to localize dates. Uses system timezone if empty. Defaults to ""
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Generator[Date, None, None]: A generator yielding Date objects for each date found in the text
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
ValueError: If an invalid first parameter or timezone is provided
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
first_number = FirstNumber[first.upper()]
|
|
33
|
+
except KeyError as e:
|
|
34
|
+
msg = f"Invalid first number: {first}"
|
|
35
|
+
raise ValueError(msg) from e
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
timezone = ZoneInfo(tz) if tz else get_localzone()
|
|
39
|
+
except ZoneInfoNotFoundError as e:
|
|
40
|
+
msg = f"Invalid timezone: {tz}"
|
|
41
|
+
raise ValueError(msg) from e
|
|
42
|
+
|
|
43
|
+
datefind = DateFind(text=text, tz=timezone, first_number=first_number)
|
|
44
|
+
return datefind.find_dates()
|
datefind/constants.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Constants for datefind."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FirstNumber(Enum):
|
|
7
|
+
"""Enum for the first number to find in ambiguous dates."""
|
|
8
|
+
|
|
9
|
+
MONTH = "month"
|
|
10
|
+
DAY = "day"
|
|
11
|
+
YEAR = "year"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DayToNumber(Enum):
|
|
15
|
+
"""Enum for the day to number."""
|
|
16
|
+
|
|
17
|
+
FIRST = 1
|
|
18
|
+
SECOND = 2
|
|
19
|
+
THIRD = 3
|
|
20
|
+
FOURTH = 4
|
|
21
|
+
FIFTH = 5
|
|
22
|
+
SIXTH = 6
|
|
23
|
+
SEVENTH = 7
|
|
24
|
+
EIGHTH = 8
|
|
25
|
+
NINTH = 9
|
|
26
|
+
TENTH = 10
|
|
27
|
+
ELEVENTH = 11
|
|
28
|
+
TWELFTH = 12
|
|
29
|
+
THIRTEENTH = 13
|
|
30
|
+
FOURTEENTH = 14
|
|
31
|
+
FIFTEENTH = 15
|
|
32
|
+
SIXTEENTH = 16
|
|
33
|
+
SEVENTEENTH = 17
|
|
34
|
+
EIGHTEENTH = 18
|
|
35
|
+
NINETEENTH = 19
|
|
36
|
+
TWENTIETH = 20
|
|
37
|
+
TWENTYFIRST = 21
|
|
38
|
+
TWENTYSECOND = 22
|
|
39
|
+
TWENTYTHIRD = 23
|
|
40
|
+
TWENTYFOURTH = 24
|
|
41
|
+
TWENTYFIFTH = 25
|
|
42
|
+
TWENTYSIXTH = 26
|
|
43
|
+
TWENTYSEVENTH = 27
|
|
44
|
+
TWENTYEIGHTH = 28
|
|
45
|
+
TWENTYNINTH = 29
|
|
46
|
+
THIRTIETH = 30
|
|
47
|
+
THIRTYFIRST = 31
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
CENTURY = 20 # Used for years provided with two digits
|
|
51
|
+
SEP_CHARS = r"/\:\-\,\.\s\_\+\@ " # Define separator chars for reuse
|
|
52
|
+
SEP_CHARS_REGEX = rf"[{SEP_CHARS}]*?"
|
|
53
|
+
DD_FLEXIBLE = r"[12][0-9]|3[01]|0?[1-9]"
|
|
54
|
+
DD = r"0[1-9]|[12][0-9]|3[01]"
|
|
55
|
+
MM = r"0[1-9]|1[012]"
|
|
56
|
+
MM_FLEXIBLE = r"1[012]|0?[1-9]"
|
|
57
|
+
YYYY = r"19\d\d|20\d\d"
|
|
58
|
+
YYYY_FLEXIBLE = rf"{YYYY}|\d\d"
|
|
59
|
+
JANUARY = r"january|jan?"
|
|
60
|
+
FEBRUARY = r"february|feb?"
|
|
61
|
+
MARCH = r"march|mar?"
|
|
62
|
+
APRIL = r"april|apr?"
|
|
63
|
+
MAY = r"may"
|
|
64
|
+
JUNE = r"june?"
|
|
65
|
+
JULY = r"july?"
|
|
66
|
+
AUGUST = r"august|aug?"
|
|
67
|
+
SEPTEMBER = r"september|sept?"
|
|
68
|
+
OCTOBER = r"october|oct?"
|
|
69
|
+
NOVEMBER = r"november|nov?"
|
|
70
|
+
DECEMBER = r"december|dec?"
|
|
71
|
+
MONTH = rf"{JANUARY}|{FEBRUARY}|{MARCH}|{APRIL}|{MAY}|{JUNE}|{JULY}|{AUGUST}|{SEPTEMBER}|{OCTOBER}|{NOVEMBER}|{DECEMBER}"
|
|
72
|
+
DAY_NUMBERS = rf"first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth|eleventh|twelfth|thirteenth|fourteenth|fifteenth|sixteenth|seventeenth|eighteenth|nineteenth|twentieth|twenty{SEP_CHARS_REGEX}first|twenty{SEP_CHARS_REGEX}second|twenty{SEP_CHARS_REGEX}third|twenty{SEP_CHARS_REGEX}fourth|twenty{SEP_CHARS_REGEX}fifth|twenty{SEP_CHARS_REGEX}sixth|twenty{SEP_CHARS_REGEX}seventh|twenty{SEP_CHARS_REGEX}eighth|twenty{SEP_CHARS_REGEX}ninth|thirtieth|thirty{SEP_CHARS_REGEX}first"
|
|
73
|
+
DIGIT_SUFFIXES = r"st|th|rd|nd"
|
|
74
|
+
TODAY = r"today'?s?"
|
|
75
|
+
YESTERDAY = r"yesterday'?s?"
|
|
76
|
+
TOMORROW = r"tomorrow'?s?"
|
|
77
|
+
LAST_WEEK = rf"last{SEP_CHARS_REGEX}week'?s?"
|
|
78
|
+
NEXT_WEEK = rf"next{SEP_CHARS_REGEX}week'?s?"
|
|
79
|
+
WEEKDAYS = (
|
|
80
|
+
r"monday|tuesday|wednesday|thursday|friday|saturday|sunday|mon|tues?|wed|thu|thurs?|fri|sat|sun"
|
|
81
|
+
)
|
|
82
|
+
TIME_PERIOD = r"am|pm"
|
datefind/datefind.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Datefind is a Python library for finding dates in text."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Generator
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from zoneinfo import ZoneInfo
|
|
7
|
+
|
|
8
|
+
import regex as re
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from datefind.utils import DatePatternFactory
|
|
12
|
+
|
|
13
|
+
from .constants import (
|
|
14
|
+
APRIL,
|
|
15
|
+
AUGUST,
|
|
16
|
+
CENTURY,
|
|
17
|
+
DECEMBER,
|
|
18
|
+
FEBRUARY,
|
|
19
|
+
JANUARY,
|
|
20
|
+
JULY,
|
|
21
|
+
JUNE,
|
|
22
|
+
MARCH,
|
|
23
|
+
MAY,
|
|
24
|
+
NOVEMBER,
|
|
25
|
+
OCTOBER,
|
|
26
|
+
SEP_CHARS,
|
|
27
|
+
SEPTEMBER,
|
|
28
|
+
DayToNumber,
|
|
29
|
+
FirstNumber,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
console = Console()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class Date:
|
|
37
|
+
"""Store information about a date found in text.
|
|
38
|
+
|
|
39
|
+
Properties:
|
|
40
|
+
date (datetime): The parsed datetime object
|
|
41
|
+
text (str): The full text that was searched
|
|
42
|
+
match (str): The specific text that matched the date pattern
|
|
43
|
+
span (tuple[int, int]): The start and end positions of the match in the text
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
date: datetime
|
|
47
|
+
text: str
|
|
48
|
+
match: str
|
|
49
|
+
span: tuple[int, int]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DateFind:
|
|
53
|
+
"""Find and parse dates within text using pattern matching.
|
|
54
|
+
|
|
55
|
+
Searches text for date patterns and returns Date objects containing the parsed datetime, original text, matched pattern, and location of the match.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
text (str): The text to search for dates
|
|
59
|
+
tz (ZoneInfo): The timezone to use for the parsed dates
|
|
60
|
+
first_number (FirstNumber): Whether the first number in a date pattern represents the day or month
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
text: str,
|
|
66
|
+
tz: ZoneInfo,
|
|
67
|
+
first_number: FirstNumber,
|
|
68
|
+
):
|
|
69
|
+
self.text = text
|
|
70
|
+
self.tz = tz
|
|
71
|
+
self.first_number = first_number
|
|
72
|
+
self.base_date = datetime(datetime.now(self.tz).year, 1, 1, tzinfo=self.tz)
|
|
73
|
+
self.factory = DatePatternFactory(first_number=self.first_number)
|
|
74
|
+
|
|
75
|
+
def find_dates(self) -> Generator[Date, None, None]:
|
|
76
|
+
"""Search the text for date patterns and parse them into Date objects.
|
|
77
|
+
|
|
78
|
+
Parse dates in various formats including relative dates like 'today', 'yesterday', 'next week' as well as explicit dates with day, month and year components. All dates are converted to datetime objects in the timezone specified during initialization.
|
|
79
|
+
|
|
80
|
+
Yields:
|
|
81
|
+
Generator[Date, None, None]: A sequence of Date objects containing the parsed datetime, original text, matched pattern, and location of the match.
|
|
82
|
+
"""
|
|
83
|
+
regex = self.factory.make_regex()
|
|
84
|
+
matches = regex.finditer(self.text)
|
|
85
|
+
|
|
86
|
+
for match in matches:
|
|
87
|
+
if match.groupdict().get("today"):
|
|
88
|
+
as_dt = datetime.now(self.tz)
|
|
89
|
+
|
|
90
|
+
elif match.groupdict().get("yesterday"):
|
|
91
|
+
as_dt = datetime.now(self.tz) - timedelta(days=1)
|
|
92
|
+
|
|
93
|
+
elif match.groupdict().get("tomorrow"):
|
|
94
|
+
as_dt = datetime.now(self.tz) + timedelta(days=1)
|
|
95
|
+
|
|
96
|
+
elif match.groupdict().get("last_week"):
|
|
97
|
+
as_dt = datetime.now(self.tz) - timedelta(days=7)
|
|
98
|
+
|
|
99
|
+
elif match.groupdict().get("next_week"):
|
|
100
|
+
as_dt = datetime.now(self.tz) + timedelta(days=7)
|
|
101
|
+
|
|
102
|
+
else:
|
|
103
|
+
day = self._day_to_number(match=match) or 1
|
|
104
|
+
month = self._month_to_number(match=match) or datetime.now(self.tz).month
|
|
105
|
+
year = self._year_to_number(match=match) or datetime.now(self.tz).year
|
|
106
|
+
|
|
107
|
+
as_dt = datetime(year=int(year), month=month, day=day, tzinfo=self.tz)
|
|
108
|
+
|
|
109
|
+
date = Date(
|
|
110
|
+
date=as_dt,
|
|
111
|
+
text=self.text,
|
|
112
|
+
match=match.group(),
|
|
113
|
+
span=match.span(),
|
|
114
|
+
)
|
|
115
|
+
yield date
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def _year_to_number(match: re.Match) -> int:
|
|
119
|
+
"""Parse a year string from a regex match into a numeric year.
|
|
120
|
+
|
|
121
|
+
Convert 2-digit years to 4-digit years by prepending the current century. For example, '23' becomes '2023'.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
match (re.Match): The regex match containing year information in named groups
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
int: The numeric year value
|
|
128
|
+
"""
|
|
129
|
+
if year := match.groupdict().get("year"):
|
|
130
|
+
if len(year) == 2: # noqa: PLR2004
|
|
131
|
+
return int(f"{CENTURY}{year}")
|
|
132
|
+
return int(year)
|
|
133
|
+
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def _month_to_number(match: re.Match) -> int:
|
|
138
|
+
"""Parse a month string from a regex match into a numeric month (1-12).
|
|
139
|
+
|
|
140
|
+
Convert both text month names (e.g. "January", "Feb") and numeric months from the regex match. Text matching is case-insensitive.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
match (re.Match): The regex match containing month information in named groups
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
int: The numeric month value (1-12)
|
|
147
|
+
"""
|
|
148
|
+
month_patterns = {
|
|
149
|
+
JANUARY: 1,
|
|
150
|
+
FEBRUARY: 2,
|
|
151
|
+
MARCH: 3,
|
|
152
|
+
APRIL: 4,
|
|
153
|
+
MAY: 5,
|
|
154
|
+
JUNE: 6,
|
|
155
|
+
JULY: 7,
|
|
156
|
+
AUGUST: 8,
|
|
157
|
+
SEPTEMBER: 9,
|
|
158
|
+
OCTOBER: 10,
|
|
159
|
+
NOVEMBER: 11,
|
|
160
|
+
DECEMBER: 12,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if month_as_text := match.groupdict().get("month_as_text"):
|
|
164
|
+
for pattern, number in month_patterns.items():
|
|
165
|
+
if re.search(pattern, month_as_text, re.IGNORECASE):
|
|
166
|
+
return number
|
|
167
|
+
|
|
168
|
+
if month := match.groupdict().get("month"):
|
|
169
|
+
return int(month)
|
|
170
|
+
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def _day_to_number(match: re.Match) -> int | None:
|
|
175
|
+
"""Parse a day string from a regex match into a numeric day of month.
|
|
176
|
+
|
|
177
|
+
Convert both text day representations (e.g. "1st", "2nd") and numeric days from the regex match.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
match (re.Match): The regex match containing day information in named groups
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
int | None: The numeric day value, or None if no day found
|
|
184
|
+
"""
|
|
185
|
+
if day_as_text := match.groupdict().get("day_as_text"):
|
|
186
|
+
day_as_text = re.sub(rf"[{SEP_CHARS}]", "", day_as_text)
|
|
187
|
+
return DayToNumber[day_as_text.upper()].value
|
|
188
|
+
|
|
189
|
+
if day := match.groupdict().get("day"):
|
|
190
|
+
return int(day)
|
|
191
|
+
|
|
192
|
+
return None
|
datefind/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Create regex patterns for matching dates in text.
|
|
2
|
+
|
|
3
|
+
Contains pattern definitions and factory classes for building flexible date-matching regular expressions that handle various date formats and styles.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import assert_never
|
|
7
|
+
|
|
8
|
+
import regex as re
|
|
9
|
+
|
|
10
|
+
from datefind.constants import (
|
|
11
|
+
DAY_NUMBERS,
|
|
12
|
+
DD,
|
|
13
|
+
DD_FLEXIBLE,
|
|
14
|
+
DIGIT_SUFFIXES,
|
|
15
|
+
LAST_WEEK,
|
|
16
|
+
MM,
|
|
17
|
+
MM_FLEXIBLE,
|
|
18
|
+
MONTH,
|
|
19
|
+
NEXT_WEEK,
|
|
20
|
+
SEP_CHARS,
|
|
21
|
+
TODAY,
|
|
22
|
+
TOMORROW,
|
|
23
|
+
YESTERDAY,
|
|
24
|
+
YYYY,
|
|
25
|
+
YYYY_FLEXIBLE,
|
|
26
|
+
FirstNumber,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
DD = f"(?P<day>{DD})(?:{DIGIT_SUFFIXES})?"
|
|
30
|
+
DD_AS_TEXT = f"(?P<day_as_text>{DAY_NUMBERS})"
|
|
31
|
+
DD_FLEXIBLE = f"(?P<day>{DD_FLEXIBLE})(?:{DIGIT_SUFFIXES})?"
|
|
32
|
+
END = rf"(?![0-9]|[{SEP_CHARS}][0-9])"
|
|
33
|
+
LAST_WEEK = rf"(?P<last_week>{LAST_WEEK})"
|
|
34
|
+
MM = rf"(?P<month>{MM})"
|
|
35
|
+
MM_FLEXIBLE = rf"(?P<month>{MM_FLEXIBLE})"
|
|
36
|
+
MONTH_AS_TEXT = f"(?P<month_as_text>{MONTH})"
|
|
37
|
+
NEXT_WEEK = rf"(?P<next_week>{NEXT_WEEK})"
|
|
38
|
+
SEPARATOR = rf"[{SEP_CHARS}]*?"
|
|
39
|
+
MONTH_DAY_SEPARATOR = rf"{SEPARATOR}(?:the|of)?{SEPARATOR}"
|
|
40
|
+
START = rf"(?<![0-9]|[0-9][{SEP_CHARS}])"
|
|
41
|
+
TODAY = rf"(?P<today>{TODAY})"
|
|
42
|
+
TOMORROW = rf"(?P<tomorrow>{TOMORROW})"
|
|
43
|
+
YESTERDAY = rf"(?P<yesterday>{YESTERDAY})"
|
|
44
|
+
YYYY = rf"(?P<year>{YYYY})"
|
|
45
|
+
YYYY_FLEXIBLE = rf"(?P<year>{YYYY_FLEXIBLE})"
|
|
46
|
+
|
|
47
|
+
ISO8601 = r"(?P<year>-?(\:[1-9][0-9]*)?[0-9]{4})\-(?P<month>1[0-2]|0[1-9])\-(?P<day>3[01]|0[1-9]|[12][0-9])T(?P<hour>2[0-3]|[01][0-9])\:(?P<minute>[0-5][0-9]):(?P<seconds>[0-5][0-9])(?:[\.,]+(?P<microseconds>[0-9]+))?(?P<offset>(?:Z|[+-](?:2[0-3]|[01][0-9])\:[0-5][0-9]))?"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
YYYY_MONTH_DD = rf"""
|
|
51
|
+
{START}
|
|
52
|
+
{YYYY}
|
|
53
|
+
{SEPARATOR}
|
|
54
|
+
{MONTH_AS_TEXT}
|
|
55
|
+
{SEPARATOR}
|
|
56
|
+
({DD_FLEXIBLE}|{DD_AS_TEXT})
|
|
57
|
+
{END}
|
|
58
|
+
"""
|
|
59
|
+
DD_MONTH_YYYY = rf"""
|
|
60
|
+
{START}
|
|
61
|
+
({DD_FLEXIBLE}|{DD_AS_TEXT})
|
|
62
|
+
{MONTH_DAY_SEPARATOR}
|
|
63
|
+
{MONTH_AS_TEXT}
|
|
64
|
+
{SEPARATOR}
|
|
65
|
+
{YYYY}
|
|
66
|
+
{END}
|
|
67
|
+
"""
|
|
68
|
+
MONTH_DD_YYYY = rf"""
|
|
69
|
+
{START}
|
|
70
|
+
{MONTH_AS_TEXT}
|
|
71
|
+
{MONTH_DAY_SEPARATOR}
|
|
72
|
+
({DD_FLEXIBLE}|{DD_AS_TEXT})
|
|
73
|
+
{SEPARATOR}
|
|
74
|
+
{YYYY}
|
|
75
|
+
{END}
|
|
76
|
+
"""
|
|
77
|
+
YYYY_MONTH = rf"""
|
|
78
|
+
{START}
|
|
79
|
+
{YYYY}
|
|
80
|
+
{SEPARATOR}
|
|
81
|
+
{MONTH_AS_TEXT}
|
|
82
|
+
{END}
|
|
83
|
+
"""
|
|
84
|
+
MONTH_DD = rf"""
|
|
85
|
+
{START}
|
|
86
|
+
{MONTH_AS_TEXT}
|
|
87
|
+
{SEPARATOR}
|
|
88
|
+
({DD_FLEXIBLE}|{DD_AS_TEXT})
|
|
89
|
+
{END}
|
|
90
|
+
"""
|
|
91
|
+
MONTH_YYYY = rf"""
|
|
92
|
+
{START}
|
|
93
|
+
{MONTH_AS_TEXT}
|
|
94
|
+
{SEPARATOR}
|
|
95
|
+
{YYYY}
|
|
96
|
+
{END}
|
|
97
|
+
"""
|
|
98
|
+
NATURAL_DATE = rf"""
|
|
99
|
+
{START}
|
|
100
|
+
{TODAY}|{YESTERDAY}|{TOMORROW}|{LAST_WEEK}|{NEXT_WEEK}
|
|
101
|
+
{END}
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class DatePatternFactory:
|
|
106
|
+
"""Factory for creating date patterns."""
|
|
107
|
+
|
|
108
|
+
def __init__(self, first_number: FirstNumber) -> None:
|
|
109
|
+
"""Initialize the factory. Defaults to US date format with month first.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
first_number (FirstNumber): The first number to use.
|
|
113
|
+
"""
|
|
114
|
+
self.first_number = first_number
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def _make_pattern(order: list[str]) -> str:
|
|
118
|
+
"""Create a regex pattern by joining date components in the specified order.
|
|
119
|
+
|
|
120
|
+
Combine date pattern components (year, month, day) with separators to form a complete regex pattern. The components are joined in the order specified, with each component separated by the SEPARATOR pattern except for the last one.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
order (list[str]): List of date pattern components in the desired order (e.g. [YYYY, MM, DD])
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
str: A raw formatted string containing the complete regex pattern with start/end markers
|
|
127
|
+
"""
|
|
128
|
+
pattern = f"{START}"
|
|
129
|
+
for part in order:
|
|
130
|
+
pattern += part
|
|
131
|
+
if part != order[-1]:
|
|
132
|
+
pattern += f"{SEPARATOR}"
|
|
133
|
+
pattern += f"{END}"
|
|
134
|
+
return rf"{pattern}"
|
|
135
|
+
|
|
136
|
+
def make_regex(self) -> re.Pattern:
|
|
137
|
+
"""Create a compiled regular expression pattern for matching dates in text.
|
|
138
|
+
|
|
139
|
+
Generate a regex pattern that matches various date formats including natural language dates (today, tomorrow, etc) and numeric dates. The order of numeric components is determined by the first_number setting specified during initialization.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
re.Pattern: A compiled regex pattern with flags for case-insensitive and verbose matching
|
|
143
|
+
"""
|
|
144
|
+
common_patterns = rf"{DD_MONTH_YYYY}|{MONTH_DD_YYYY}|{YYYY_MONTH_DD}|{MONTH_DD}|{YYYY_MONTH}|{MONTH_YYYY}|{NATURAL_DATE}"
|
|
145
|
+
|
|
146
|
+
# Set patterns that rely on the first_number flag.
|
|
147
|
+
match self.first_number:
|
|
148
|
+
case FirstNumber.MONTH:
|
|
149
|
+
yyyy_xx_xx = self._make_pattern([YYYY, MM_FLEXIBLE, DD_FLEXIBLE])
|
|
150
|
+
xx_xx_xx = self._make_pattern([MM_FLEXIBLE, DD_FLEXIBLE, YYYY_FLEXIBLE])
|
|
151
|
+
case FirstNumber.DAY:
|
|
152
|
+
yyyy_xx_xx = self._make_pattern([YYYY, DD_FLEXIBLE, MM_FLEXIBLE])
|
|
153
|
+
xx_xx_xx = self._make_pattern([DD_FLEXIBLE, MM_FLEXIBLE, YYYY_FLEXIBLE])
|
|
154
|
+
case FirstNumber.YEAR:
|
|
155
|
+
yyyy_xx_xx = self._make_pattern([YYYY, MM_FLEXIBLE, DD_FLEXIBLE])
|
|
156
|
+
xx_xx_xx = self._make_pattern([YYYY_FLEXIBLE, MM_FLEXIBLE, DD_FLEXIBLE])
|
|
157
|
+
case _: # pragma: no cover
|
|
158
|
+
assert_never(self.first_number)
|
|
159
|
+
|
|
160
|
+
return re.compile(
|
|
161
|
+
f"{common_patterns}|{yyyy_xx_xx}|{xx_xx_xx}",
|
|
162
|
+
re.IGNORECASE | re.VERBOSE | re.MULTILINE | re.UNICODE | re.DOTALL,
|
|
163
|
+
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: datefind
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A python module for locating dates within text.
|
|
5
|
+
Project-URL: Homepage, https://github.com/natelandau/datefind
|
|
6
|
+
Project-URL: Repository, https://github.com/natelandau/datefind
|
|
7
|
+
Author-email: Nate Landau <github@natenate.org>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Python: <=3.13,>=3.11
|
|
15
|
+
Requires-Dist: regex>=2024.11.6
|
|
16
|
+
Requires-Dist: tzlocal>=5.3.1
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
[](https://github.com/natelandau/datefind/actions/workflows/test.yml) [](https://codecov.io/gh/natelandau/datefind)
|
|
20
|
+
|
|
21
|
+
# datefind
|
|
22
|
+
|
|
23
|
+
A python module for locating dates within text. Use this package to search for dates and convert them to datetime objects.
|
|
24
|
+
|
|
25
|
+
Finds dates in many different formats.
|
|
26
|
+
|
|
27
|
+
- All numeric - `2024-01-01`, `01/01/2024`, `01012024`
|
|
28
|
+
- Natural language - `January 1st, 2024`, `March nineteenth, 2024`, `twenty fifth of January`
|
|
29
|
+
- Fuzzy dates - `today`, `yesterday`, `tomorrow`, `last week`, `next week`
|
|
30
|
+
|
|
31
|
+
> [!NOTE]\
|
|
32
|
+
> datefind is designed to be used with year, month, and day only. It does not support hours, minutes, seconds, or microseconds.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
Requires Python 3.11 or higher.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install datefind
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from datefind import find_dates
|
|
46
|
+
|
|
47
|
+
string = "2024-01-01 and 2024-01-02"
|
|
48
|
+
|
|
49
|
+
for date in find_dates(string, tz="America/New_York"):
|
|
50
|
+
print(date)
|
|
51
|
+
|
|
52
|
+
>>> Date(
|
|
53
|
+
date=datetime.datetime(2024, 1, 1, 0, 0, tzinfo=zoneinfo.ZoneInfo(key='America/New_York')),
|
|
54
|
+
text='2024-01-01 and 2024-01-02',
|
|
55
|
+
match='2024-01-01',
|
|
56
|
+
span=(0, 10)
|
|
57
|
+
)
|
|
58
|
+
>>> Date(
|
|
59
|
+
date=datetime.datetime(2024, 1, 2, 0, 0, tzinfo=zoneinfo.ZoneInfo(key='America/New_York')),
|
|
60
|
+
text='2024-01-01 and 2024-01-02',
|
|
61
|
+
match='2024-01-02',
|
|
62
|
+
span=(15, 25)
|
|
63
|
+
)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### find_dates
|
|
67
|
+
|
|
68
|
+
The `find_dates()` function is the main entry point for the datefind package. It takes a string of text and returns a generator of Date objects.
|
|
69
|
+
|
|
70
|
+
**Arguments**
|
|
71
|
+
|
|
72
|
+
- `text` - The text to search for dates.
|
|
73
|
+
- `first` - The first number to find in ambiguous dates. (one of `month`, `day`, `year`) Default is `month`
|
|
74
|
+
- `tz` - The timezone to use. Defaults to the local timezone.
|
|
75
|
+
|
|
76
|
+
For each date found, a Date object is returned. The Date object has the following properties:
|
|
77
|
+
|
|
78
|
+
- `date` - The datetime object.
|
|
79
|
+
- `text` - The full original text.
|
|
80
|
+
- `match` - The matched portion of the text.
|
|
81
|
+
- `span` - The span of the matched text in the original text.
|
|
82
|
+
|
|
83
|
+
## Contributing
|
|
84
|
+
|
|
85
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for more information.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
datefind/__init__.py,sha256=GpoYC8sodoyz_k3waK_W1vgBfvijaYy-FbdUjyKQv8A,1684
|
|
2
|
+
datefind/constants.py,sha256=052CtVYb-ZR8lfO4012wsR6VmjG2Ovq29GjSiJFiW2M,2419
|
|
3
|
+
datefind/datefind.py,sha256=TzG0wu07aQ0Ott5jvjOjKSH2CcbuPgWQHhKilbRIc-A,6150
|
|
4
|
+
datefind/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
datefind/utils/__init__.py,sha256=XI246xDLK81gQ0mfL9XoUytPX-_XoHJfDgvoEPbex2Y,110
|
|
6
|
+
datefind/utils/date_factory.py,sha256=mvLXt1PDV13nyWeZ3c2AxmAU-dAtGKU4cMTfBVwE6EY,5190
|
|
7
|
+
datefind-0.1.0.dist-info/METADATA,sha256=iPQPi0hP_sl7_xKkP1W9wnkqDXfp_0ry3HRB8tuiOIg,2888
|
|
8
|
+
datefind-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
9
|
+
datefind-0.1.0.dist-info/licenses/LICENSE,sha256=Ur0dbtYXGPg840eCwDlk8pyTq7q16l6-LCK2xZDoU6s,1078
|
|
10
|
+
datefind-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Nate Landau
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|