Skip to content

Commit 4469e90

Browse files
committed
re -> datetime
1 parent c2be673 commit 4469e90

File tree

1 file changed

+113
-126
lines changed

1 file changed

+113
-126
lines changed

date_hour/date_hour.py

Lines changed: 113 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,177 +1,164 @@
1-
import re
21
from datetime import datetime, timedelta
2+
from typing import Union
33
from pydantic_core import core_schema
44
from pydantic import GetCoreSchemaHandler
55

6+
67
class DateHour(str):
78
'''Класс для работы с временными метками с границами периодов.'''
89

9-
patterns = [
10-
(re.compile(r'^\d{4}$'), '%Y'), # 2024
11-
(re.compile(r'^\d{4}-\d{2}$'), '%Y-%m'), # 2024-01
12-
(re.compile(r'^\d{4}-\d{2}-\d{2}$'), '%Y-%m-%d'), # 2024-01-15
13-
(re.compile(r'^\d{4}-\d{2}-\d{2} \d{2}$'), '%Y-%m-%d %H'), # 2024-01-15 14
14-
(re.compile(r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$'), '%Y-%m-%d %H:%M:%S'), # 2024-01-15 14:59:59
15-
]
16-
17-
def __new__(cls, value: str | datetime) -> 'DateHour':
10+
_formats = {
11+
'%Y-%m-%d %H:%M:%S': 'hour', # секунды
12+
'%Y-%m-%d %H:%M': 'hour', # минуты
13+
'%Y-%m-%d %H': 'hour', # Час
14+
'%Y-%m-%d': 'day', # День
15+
'%Y-%m': 'month', # Месяц
16+
'%Y': 'year', # Год
17+
}
18+
19+
def __new__(cls, value: Union[str, datetime]) -> 'DateHour':
1820
if isinstance(value, datetime):
1921
value = value.strftime('%Y-%m-%d %H:%M:%S')
22+
2023
if not isinstance(value, str):
2124
raise ValueError(f'Ожидалась строка, получен {type(value)}: {value}')
2225

23-
matched_format = None
24-
for pattern, date_format in cls.patterns:
25-
if pattern.fullmatch(value):
26-
matched_format = date_format
27-
break
26+
dt, format_type = cls._parse_string(value)
2827

29-
if not matched_format:
30-
raise ValueError(
31-
f'Некорректный формат даты: "{value}". '
32-
f'Поддерживаемые форматы: "YYYY", "YYYY-MM", "YYYY-MM-DD", "YYYY-MM-DD HH", "YYYY-MM-DD HH:MM:SS"'
33-
)
28+
normalized_dt = dt.replace(minute=0, second=0, microsecond=0)
29+
normalized_str = normalized_dt.strftime('%Y-%m-%d %H:%M:%S')
3430

35-
try:
36-
value = datetime.strptime(value, matched_format).replace(minute=0, second=0, microsecond=0)
37-
except ValueError as e:
38-
raise ValueError(f'Неверная дата/время: {e}')
39-
40-
return super().__new__(cls, value)
31+
instance = super().__new__(cls, normalized_str)
32+
instance._format_type = format_type
33+
return instance
34+
35+
@classmethod
36+
def _parse_string(cls, value: str) -> tuple[datetime, str]:
37+
'''Парсит строку в datetime и возвращает тип формата.'''
38+
errors = []
39+
40+
for fmt, format_type in cls._formats.items():
41+
try:
42+
dt = datetime.strptime(value, fmt)
43+
return dt, format_type
44+
except ValueError as e:
45+
errors.append(f"{fmt}: {e}")
46+
continue
47+
48+
# Если ни один формат не подошел
49+
supported_formats = "\n - ".join(cls._formats.keys())
50+
raise ValueError(
51+
f'Не удалось распарсить дату: "{value}".\n'
52+
f'Поддерживаемые форматы:\n - {supported_formats}'
53+
)
54+
55+
def _get_datetime(self) -> datetime:
56+
'''Возвращает datetime объект из строки.'''
57+
return datetime.strptime(self, '%Y-%m-%d %H:%M:%S')
4158

4259
def _get_start_datetime(self) -> datetime:
43-
'''Возвращает начало периода'''
44-
for pattern, date_format in self.patterns:
45-
if pattern.fullmatch(self):
46-
dt = datetime.strptime(self, date_format)
47-
format_type = self._get_format_type()
48-
49-
if format_type == 'year':
50-
return dt.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)
51-
elif format_type == 'month':
52-
return dt.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
53-
elif format_type == 'day':
54-
return dt.replace(hour=0, minute=0, second=0, microsecond=0)
55-
elif format_type == 'hour':
56-
return dt.replace(minute=0, second=0, microsecond=0)
57-
elif format_type == 'full':
58-
return dt.replace(minute=0, second=0, microsecond=0)
59-
raise ValueError(f'Не удалось распарсить дату: {self}')
60+
'''Возвращает начало периода.'''
61+
dt = self._get_datetime()
62+
63+
if self._format_type == 'year':
64+
return dt.replace(month=1, day=1, hour=0)
65+
elif self._format_type == 'month':
66+
return dt.replace(day=1, hour=0)
67+
elif self._format_type == 'day':
68+
return dt.replace(hour=0)
69+
else: # hour
70+
return dt # Уже нормализовано до начала часа
6071

6172
def _get_stop_datetime(self) -> datetime:
62-
'''Возвращает конец периода'''
63-
for pattern, date_format in self.patterns:
64-
if pattern.fullmatch(self):
65-
dt = datetime.strptime(self, date_format)
66-
format_type = self._get_format_type()
67-
68-
if format_type == 'year':
69-
# Конец года: 31 декабря 23:00:00
70-
return dt.replace(month=12, day=31, hour=23, minute=0, second=0, microsecond=0)
71-
elif format_type == 'month':
72-
# Конец месяца: последний день 23:00:00
73-
if dt.month == 12:
74-
next_month = dt.replace(year=dt.year + 1, month=1, day=1)
75-
else:
76-
next_month = dt.replace(month=dt.month + 1, day=1)
77-
last_day = next_month - timedelta(days=1)
78-
return last_day.replace(hour=23, minute=0, second=0, microsecond=0)
79-
elif format_type == 'day':
80-
# Конец дня: 23:00:00
81-
return dt.replace(hour=23, minute=0, second=0, microsecond=0)
82-
elif format_type == 'hour':
83-
# Для часа: тот же час
84-
return dt.replace(minute=0, second=0, microsecond=0)
85-
elif format_type == 'full':
86-
# Для полного времени: нормализуем до часа
87-
return dt.replace(minute=0, second=0, microsecond=0)
88-
raise ValueError(f'Не удалось распарсить дату: {self}')
89-
90-
def _get_format_type(self) -> str:
91-
'''Возвращает тип формата'''
92-
if re.fullmatch(r'^\d{4}$', self):
93-
return 'year'
94-
elif re.fullmatch(r'^\d{4}-\d{2}$', self):
95-
return 'month'
96-
elif re.fullmatch(r'^\d{4}-\d{2}-\d{2}$', self):
97-
return 'day'
98-
elif re.fullmatch(r'^\d{4}-\d{2}-\d{2} \d{2}$', self):
99-
return 'hour'
100-
elif re.fullmatch(r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$', self):
101-
return 'full'
102-
else:
103-
raise ValueError(f'Неизвестный формат: {self}')
73+
'''Возвращает конец периода.'''
74+
dt = self._get_datetime()
75+
76+
if self._format_type == 'year':
77+
# Конец года: 31 декабря 23:00:00
78+
return dt.replace(month=12, day=31, hour=23)
79+
elif self._format_type == 'month':
80+
# Конец месяца: последний день 23:00:00
81+
if dt.month == 12:
82+
next_month = dt.replace(year=dt.year + 1, month=1, day=1)
83+
else:
84+
next_month = dt.replace(month=dt.month + 1, day=1)
85+
last_day = next_month - timedelta(days=1)
86+
return last_day.replace(hour=23)
87+
elif self._format_type == 'day':
88+
# Конец дня: 23:00:00
89+
return dt.replace(hour=23)
90+
else: # hour
91+
# Для часа: тот же час
92+
return dt
10493

10594
@property
10695
def start(self) -> str:
107-
'''Начало периода в формате YYYY-MM-DD HH:MM:SS'''
96+
'''Начало периода в формате YYYY-MM-DD HH:MM:SS.'''
10897
return self._get_start_datetime().strftime('%Y-%m-%d %H:%M:%S')
10998

11099
@property
111100
def stop(self) -> str:
112-
'''Конец периода в формате YYYY-MM-DD HH:MM:SS'''
101+
'''Конец периода в формате YYYY-MM-DD HH:MM:SS.'''
113102
return self._get_stop_datetime().strftime('%Y-%m-%d %H:%M:%S')
114103

115104
def __sub__(self, hours: int) -> 'DateHour':
116-
'''Вычитает указанное количество часов от начала периода'''
105+
'''Вычитает указанное количество часов от начала периода.'''
117106
start_dt = self._get_start_datetime()
118107
new_dt = start_dt - timedelta(hours=hours)
119-
return DateHour(new_dt.strftime('%Y-%m-%d %H:%M:%S'))
108+
return DateHour(new_dt)
120109

121110
def __add__(self, hours: int) -> 'DateHour':
122-
'''Добавляет указанное количество часов к началу периода'''
111+
'''Добавляет указанное количество часов к началу периода.'''
123112
start_dt = self._get_start_datetime()
124113
new_dt = start_dt + timedelta(hours=hours)
125-
return DateHour(new_dt.strftime('%Y-%m-%d %H:%M:%S'))
114+
return DateHour(new_dt)
126115

127116
@classmethod
128117
def __get_pydantic_core_schema__(
129118
cls,
130119
source: type,
131120
handler: GetCoreSchemaHandler
132121
) -> core_schema.CoreSchema:
133-
def validate(value: str) -> 'DateHour':
122+
def validate(value: Union[str, datetime]) -> 'DateHour':
134123
if isinstance(value, cls):
135124
return value
136125
return cls(value)
137126

138127
return core_schema.no_info_plain_validator_function(
139128
function=validate,
140-
serialization=core_schema.plain_serializer_function_ser_schema(lambda v: str(v))
129+
serialization=core_schema.plain_serializer_function_ser_schema(str)
141130
)
142131

143132

144133
if __name__ == '__main__':
145-
# Тест 1: Год
146-
dh1 = DateHour("2024")
147-
print(f"DateHour('2024').start = {dh1.start}") # 2024-01-01 00:00:00
148-
print(f"DateHour('2024').stop = {dh1.stop}") # 2024-12-31 23:00:00
149-
print(f"DateHour('2024') - 1 = {(dh1 - 1).start}") # 2023-12-31 23:00:00
150-
151-
print()
152-
153-
# Тест 2: Месяц
154-
dh2 = DateHour("2024-01")
155-
print(f"DateHour('2024-01').start = {dh2.start}") # 2024-01-01 00:00:00
156-
print(f"DateHour('2024-01').stop = {dh2.stop}") # 2024-01-31 23:00:00
157-
158-
print()
159-
160-
# Тест 3: День
161-
dh3 = DateHour("2024-01-15")
162-
print(f"DateHour('2024-01-15').start = {dh3.start}") # 2024-01-15 00:00:00
163-
print(f"DateHour('2024-01-15').stop = {dh3.stop}") # 2024-01-15 23:00:00
164-
165-
print()
166-
167-
# Тест 4: Час
168-
dh4 = DateHour("2024-01-15 14")
169-
print(f"DateHour('2024-01-15 14').start = {dh4.start}") # 2024-01-15 14:00:00
170-
print(f"DateHour('2024-01-15 14').stop = {dh4.stop}") # 2024-01-15 14:00:00
171-
172-
print()
173-
174-
# Тест 5: Полное время (нормализуется до часа)
175-
dh5 = DateHour("2024-01-15 14:59:59")
176-
print(f"DateHour('2024-01-15 14:59:59').start = {dh5.start}") # 2024-01-15 14:00:00
177-
print(f"DateHour('2024-01-15 14:59:59').stop = {dh5.stop}") # 2024-01-15 14:00:00
134+
print("=== Тестирование DateHour с определением типа при парсинге ===")
135+
136+
# Тест 1: Разные форматы
137+
test_cases = [
138+
"2024", # Год
139+
"2024-01", # Месяц
140+
"2024-01-15", # День
141+
"2024-01-15 14", # Час
142+
"2024-01-15 14:30", # Время с минутами
143+
"2024-01-15 14:30:45", # Полное время
144+
datetime(2024, 1, 15, 14, 30, 45), # datetime объект
145+
]
146+
147+
for i, case in enumerate(test_cases, 1):
148+
try:
149+
dh = DateHour(case)
150+
print(f"{i}. {case!r:25} -> {dh!r:25} | тип: {dh._format_type:6} | start: {dh.start:19} | stop: {dh.stop:19}")
151+
except Exception as e:
152+
print(f"{i}. {case!r:25} -> ОШИБКА: {e}")
153+
154+
print("\n=== Арифметические операции ===")
155+
dh = DateHour("2024-01-15 14:30:00")
156+
print(f"Исходный: {dh} -> start: {dh.start}")
157+
print(f"+2 часа: {(dh + 2)} -> start: {(dh + 2).start}")
158+
print(f"-3 часа: {(dh - 3)} -> start: {(dh - 3).start}")
159+
160+
print("\n=== Проверка границ периодов ===")
161+
print(f"Год 2024: {DateHour('2024').start} - {DateHour('2024').stop}")
162+
print(f"Месяц 2024-01: {DateHour('2024-01').start} - {DateHour('2024-01').stop}")
163+
print(f"День 2024-01-15: {DateHour('2024-01-15').start} - {DateHour('2024-01-15').stop}")
164+
print(f"Час 2024-01-15 14: {DateHour('2024-01-15 14').start} - {DateHour('2024-01-15 14').stop}")

0 commit comments

Comments
 (0)