staran 1.0.5__py3-none-any.whl → 1.0.7__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.
- staran/__init__.py +1 -1
- staran/date/__init__.py +12 -3
- staran/date/core.py +587 -15
- staran/date/examples/enhanced_features.py +230 -0
- staran/date/tests/test_core.py +4 -2
- staran/date/tests/test_enhancements.py +295 -0
- staran-1.0.7.dist-info/METADATA +306 -0
- staran-1.0.7.dist-info/RECORD +17 -0
- staran-1.0.5.dist-info/METADATA +0 -144
- staran-1.0.5.dist-info/RECORD +0 -15
- {staran-1.0.5.dist-info → staran-1.0.7.dist-info}/WHEEL +0 -0
- {staran-1.0.5.dist-info → staran-1.0.7.dist-info}/licenses/LICENSE +0 -0
- {staran-1.0.5.dist-info → staran-1.0.7.dist-info}/top_level.txt +0 -0
staran/date/core.py
CHANGED
@@ -14,7 +14,7 @@ import calendar
|
|
14
14
|
import re
|
15
15
|
import logging
|
16
16
|
import time
|
17
|
-
from typing import Union, Optional, Tuple, Dict, Any
|
17
|
+
from typing import Union, Optional, Tuple, Dict, Any, List
|
18
18
|
from functools import lru_cache
|
19
19
|
|
20
20
|
class DateError(ValueError):
|
@@ -104,8 +104,17 @@ class Date:
|
|
104
104
|
>>> print(date.format_chinese()) # 2025年04月15日
|
105
105
|
>>> print(date.get_weekday()) # 1 (星期二)
|
106
106
|
>>> print(date.is_weekend()) # False
|
107
|
+
|
108
|
+
Raises:
|
109
|
+
-------
|
110
|
+
InvalidDateFormatError
|
111
|
+
当输入的日期格式无效时抛出
|
112
|
+
InvalidDateValueError
|
113
|
+
当日期值超出有效范围时抛出
|
107
114
|
"""
|
108
115
|
|
116
|
+
__slots__ = ('year', 'month', 'day', '_input_format')
|
117
|
+
|
109
118
|
# 类级别的日志记录器
|
110
119
|
_logger = DateLogger()
|
111
120
|
|
@@ -203,9 +212,14 @@ class Date:
|
|
203
212
|
|
204
213
|
def _validate_date(self):
|
205
214
|
"""验证日期的有效性"""
|
215
|
+
# 基本范围检查
|
206
216
|
if not (1 <= self.month <= 12):
|
207
217
|
raise InvalidDateValueError(f"无效的月份: {self.month}")
|
208
218
|
|
219
|
+
# 年份合理性检查
|
220
|
+
if not (1900 <= self.year <= 3000):
|
221
|
+
self._logger.warning(f"年份 {self.year} 超出常规范围 (1900-3000)")
|
222
|
+
|
209
223
|
max_days = calendar.monthrange(self.year, self.month)[1]
|
210
224
|
if not (1 <= self.day <= max_days):
|
211
225
|
raise InvalidDateValueError(f"无效的日期: {self.day} (对于 {self.year}-{self.month})")
|
@@ -214,10 +228,39 @@ class Date:
|
|
214
228
|
datetime.date(self.year, self.month, self.day)
|
215
229
|
except ValueError as e:
|
216
230
|
raise InvalidDateValueError(f"无效的日期: {self.year}-{self.month}-{self.day}") from e
|
231
|
+
|
232
|
+
# 特殊日期检查
|
233
|
+
self._check_special_dates()
|
234
|
+
|
235
|
+
def _check_special_dates(self):
|
236
|
+
"""检查特殊日期"""
|
237
|
+
# 检查是否为闰年2月29日
|
238
|
+
if self.month == 2 and self.day == 29 and not calendar.isleap(self.year):
|
239
|
+
raise InvalidDateValueError(f"非闰年 {self.year} 不存在2月29日")
|
240
|
+
|
241
|
+
# 检查历史边界日期
|
242
|
+
if self.year < 1582 and self.month == 10 and 5 <= self.day <= 14:
|
243
|
+
self._logger.warning("日期位于格里高利历改革期间 (1582年10月5-14日)")
|
244
|
+
|
245
|
+
@classmethod
|
246
|
+
def is_valid_date_string(cls, date_string: str) -> bool:
|
247
|
+
"""检查日期字符串是否有效
|
248
|
+
|
249
|
+
Args:
|
250
|
+
date_string: 日期字符串
|
251
|
+
|
252
|
+
Returns:
|
253
|
+
是否为有效日期字符串
|
254
|
+
"""
|
255
|
+
try:
|
256
|
+
cls(date_string)
|
257
|
+
return True
|
258
|
+
except (InvalidDateFormatError, InvalidDateValueError):
|
259
|
+
return False
|
217
260
|
|
218
261
|
@lru_cache(maxsize=128)
|
219
262
|
def _create_with_same_format(self, year: int, month: int, day: int) -> 'Date':
|
220
|
-
"""创建具有相同格式的新Date对象
|
263
|
+
"""创建具有相同格式的新Date对象"""
|
221
264
|
new_date = Date(year, month, day)
|
222
265
|
new_date._input_format = self._input_format
|
223
266
|
return new_date
|
@@ -244,9 +287,17 @@ class Date:
|
|
244
287
|
return cls(date_string)
|
245
288
|
|
246
289
|
@classmethod
|
247
|
-
def from_timestamp(cls, timestamp: Union[int, float]) -> 'Date':
|
248
|
-
"""从时间戳创建Date对象
|
290
|
+
def from_timestamp(cls, timestamp: Union[int, float], timezone_offset: int = 0) -> 'Date':
|
291
|
+
"""从时间戳创建Date对象
|
292
|
+
|
293
|
+
Args:
|
294
|
+
timestamp: Unix时间戳
|
295
|
+
timezone_offset: 时区偏移小时数 (如 +8 表示东八区)
|
296
|
+
"""
|
249
297
|
dt = datetime.datetime.fromtimestamp(timestamp)
|
298
|
+
# 调整时区
|
299
|
+
if timezone_offset != 0:
|
300
|
+
dt = dt + datetime.timedelta(hours=timezone_offset)
|
250
301
|
return cls(dt.date())
|
251
302
|
|
252
303
|
@classmethod
|
@@ -259,6 +310,95 @@ class Date:
|
|
259
310
|
"""创建今日Date对象"""
|
260
311
|
return cls(datetime.date.today())
|
261
312
|
|
313
|
+
@classmethod
|
314
|
+
def date_range(cls, start: Union[str, 'Date'], end: Union[str, 'Date'],
|
315
|
+
step: int = 1) -> List['Date']:
|
316
|
+
"""生成日期范围
|
317
|
+
|
318
|
+
Args:
|
319
|
+
start: 开始日期
|
320
|
+
end: 结束日期
|
321
|
+
step: 步长(天数)
|
322
|
+
|
323
|
+
Returns:
|
324
|
+
日期列表
|
325
|
+
"""
|
326
|
+
if isinstance(start, str):
|
327
|
+
start = cls.from_string(start)
|
328
|
+
if isinstance(end, str):
|
329
|
+
end = cls.from_string(end)
|
330
|
+
|
331
|
+
dates = []
|
332
|
+
current = start
|
333
|
+
while current <= end:
|
334
|
+
dates.append(current)
|
335
|
+
current = current.add_days(step)
|
336
|
+
return dates
|
337
|
+
|
338
|
+
@classmethod
|
339
|
+
def business_days(cls, start: Union[str, 'Date'], end: Union[str, 'Date']) -> List['Date']:
|
340
|
+
"""生成工作日列表"""
|
341
|
+
dates = cls.date_range(start, end)
|
342
|
+
return [date for date in dates if date.is_business_day()]
|
343
|
+
|
344
|
+
@classmethod
|
345
|
+
def weekends(cls, start: Union[str, 'Date'], end: Union[str, 'Date']) -> List['Date']:
|
346
|
+
"""生成周末日期列表"""
|
347
|
+
dates = cls.date_range(start, end)
|
348
|
+
return [date for date in dates if date.is_weekend()]
|
349
|
+
|
350
|
+
@classmethod
|
351
|
+
def month_range(cls, start_year_month: Union[str, 'Date'], months: int) -> List['Date']:
|
352
|
+
"""生成月份范围
|
353
|
+
|
354
|
+
Args:
|
355
|
+
start_year_month: 开始年月 (如 "202501" 或 Date对象)
|
356
|
+
months: 月份数量
|
357
|
+
|
358
|
+
Returns:
|
359
|
+
月份第一天的日期列表
|
360
|
+
|
361
|
+
Example:
|
362
|
+
Date.month_range("202501", 3) # [202501, 202502, 202503]
|
363
|
+
"""
|
364
|
+
if isinstance(start_year_month, str):
|
365
|
+
start_date = cls.from_string(start_year_month)
|
366
|
+
else:
|
367
|
+
start_date = start_year_month
|
368
|
+
|
369
|
+
# 确保是月初
|
370
|
+
start_date = start_date.get_month_start()
|
371
|
+
|
372
|
+
result = []
|
373
|
+
current = start_date
|
374
|
+
for _ in range(months):
|
375
|
+
result.append(current)
|
376
|
+
current = current.add_months(1)
|
377
|
+
|
378
|
+
return result
|
379
|
+
|
380
|
+
@classmethod
|
381
|
+
def quarter_dates(cls, year: int) -> Dict[int, Tuple['Date', 'Date']]:
|
382
|
+
"""获取指定年份的季度起止日期
|
383
|
+
|
384
|
+
Args:
|
385
|
+
year: 年份
|
386
|
+
|
387
|
+
Returns:
|
388
|
+
季度字典 {1: (Q1开始, Q1结束), 2: (Q2开始, Q2结束), ...}
|
389
|
+
"""
|
390
|
+
quarters = {}
|
391
|
+
for quarter in range(1, 5):
|
392
|
+
start_month = (quarter - 1) * 3 + 1
|
393
|
+
end_month = quarter * 3
|
394
|
+
|
395
|
+
start_date = cls(year, start_month, 1)
|
396
|
+
end_date = cls(year, end_month, 1).get_month_end()
|
397
|
+
|
398
|
+
quarters[quarter] = (start_date, end_date)
|
399
|
+
|
400
|
+
return quarters
|
401
|
+
|
262
402
|
# =============================================
|
263
403
|
# to_* 系列:转换方法
|
264
404
|
# =============================================
|
@@ -267,14 +407,6 @@ class Date:
|
|
267
407
|
"""转为元组 (year, month, day)"""
|
268
408
|
return (self.year, self.month, self.day)
|
269
409
|
|
270
|
-
def to_dict(self) -> Dict[str, int]:
|
271
|
-
"""转为字典"""
|
272
|
-
return {
|
273
|
-
'year': self.year,
|
274
|
-
'month': self.month,
|
275
|
-
'day': self.day
|
276
|
-
}
|
277
|
-
|
278
410
|
def to_date_object(self) -> datetime.date:
|
279
411
|
"""转为datetime.date对象"""
|
280
412
|
return datetime.date(self.year, self.month, self.day)
|
@@ -283,9 +415,109 @@ class Date:
|
|
283
415
|
"""转为datetime.datetime对象"""
|
284
416
|
return datetime.datetime(self.year, self.month, self.day)
|
285
417
|
|
286
|
-
def to_timestamp(self) -> float:
|
287
|
-
"""转为时间戳
|
288
|
-
|
418
|
+
def to_timestamp(self, timezone_offset: int = 0) -> float:
|
419
|
+
"""转为时间戳
|
420
|
+
|
421
|
+
Args:
|
422
|
+
timezone_offset: 时区偏移小时数 (如 +8 表示东八区)
|
423
|
+
|
424
|
+
Returns:
|
425
|
+
Unix时间戳
|
426
|
+
"""
|
427
|
+
dt = self.to_datetime_object()
|
428
|
+
# 调整时区
|
429
|
+
if timezone_offset != 0:
|
430
|
+
dt = dt - datetime.timedelta(hours=timezone_offset)
|
431
|
+
return dt.timestamp()
|
432
|
+
|
433
|
+
def to_json(self, include_metadata: bool = True) -> str:
|
434
|
+
"""转为JSON字符串
|
435
|
+
|
436
|
+
Args:
|
437
|
+
include_metadata: 是否包含元数据(格式、版本等)
|
438
|
+
|
439
|
+
Returns:
|
440
|
+
JSON字符串
|
441
|
+
"""
|
442
|
+
import json
|
443
|
+
|
444
|
+
data = {
|
445
|
+
'date': self.format_iso(),
|
446
|
+
'year': self.year,
|
447
|
+
'month': self.month,
|
448
|
+
'day': self.day
|
449
|
+
}
|
450
|
+
|
451
|
+
if include_metadata:
|
452
|
+
data.update({
|
453
|
+
'format': self._input_format,
|
454
|
+
'weekday': self.get_weekday(),
|
455
|
+
'is_weekend': self.is_weekend(),
|
456
|
+
'quarter': self.get_quarter(),
|
457
|
+
'version': '1.0.7'
|
458
|
+
})
|
459
|
+
|
460
|
+
return json.dumps(data, ensure_ascii=False)
|
461
|
+
|
462
|
+
@classmethod
|
463
|
+
def from_json(cls, json_str: str) -> 'Date':
|
464
|
+
"""从JSON字符串创建Date对象
|
465
|
+
|
466
|
+
Args:
|
467
|
+
json_str: JSON字符串
|
468
|
+
|
469
|
+
Returns:
|
470
|
+
Date对象
|
471
|
+
|
472
|
+
Raises:
|
473
|
+
ValueError: JSON格式错误或缺少必要字段
|
474
|
+
"""
|
475
|
+
import json
|
476
|
+
|
477
|
+
try:
|
478
|
+
data = json.loads(json_str)
|
479
|
+
except json.JSONDecodeError as e:
|
480
|
+
raise ValueError(f"无效的JSON格式: {e}")
|
481
|
+
|
482
|
+
# 检查必要字段
|
483
|
+
required_fields = ['year', 'month', 'day']
|
484
|
+
missing_fields = [field for field in required_fields if field not in data]
|
485
|
+
if missing_fields:
|
486
|
+
raise ValueError(f"JSON缺少必要字段: {missing_fields}")
|
487
|
+
|
488
|
+
try:
|
489
|
+
date = cls(data['year'], data['month'], data['day'])
|
490
|
+
date._input_format = data.get('format', 'iso')
|
491
|
+
return date
|
492
|
+
except (InvalidDateFormatError, InvalidDateValueError) as e:
|
493
|
+
raise ValueError(f"JSON中的日期数据无效: {e}")
|
494
|
+
|
495
|
+
def to_dict(self, include_metadata: bool = False) -> Dict[str, Any]:
|
496
|
+
"""转为字典
|
497
|
+
|
498
|
+
Args:
|
499
|
+
include_metadata: 是否包含元数据
|
500
|
+
|
501
|
+
Returns:
|
502
|
+
字典表示
|
503
|
+
"""
|
504
|
+
result = {
|
505
|
+
'year': self.year,
|
506
|
+
'month': self.month,
|
507
|
+
'day': self.day
|
508
|
+
}
|
509
|
+
|
510
|
+
if include_metadata:
|
511
|
+
result.update({
|
512
|
+
'format': self._input_format,
|
513
|
+
'weekday': self.get_weekday(),
|
514
|
+
'is_weekend': self.is_weekend(),
|
515
|
+
'quarter': self.get_quarter(),
|
516
|
+
'iso_string': self.format_iso(),
|
517
|
+
'compact_string': self.format_compact()
|
518
|
+
})
|
519
|
+
|
520
|
+
return result
|
289
521
|
|
290
522
|
# =============================================
|
291
523
|
# format_* 系列:格式化方法
|
@@ -335,6 +567,67 @@ class Date:
|
|
335
567
|
dt = self.to_datetime_object()
|
336
568
|
return dt.strftime(fmt)
|
337
569
|
|
570
|
+
def format_weekday(self, lang: str = 'zh') -> str:
|
571
|
+
"""格式化星期几"""
|
572
|
+
weekday = self.get_weekday()
|
573
|
+
if lang == 'zh':
|
574
|
+
weekdays = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日']
|
575
|
+
else: # en
|
576
|
+
weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
|
577
|
+
return weekdays[weekday]
|
578
|
+
|
579
|
+
def format_month_name(self, lang: str = 'zh') -> str:
|
580
|
+
"""格式化月份名称"""
|
581
|
+
if lang == 'zh':
|
582
|
+
months = ['一月', '二月', '三月', '四月', '五月', '六月',
|
583
|
+
'七月', '八月', '九月', '十月', '十一月', '十二月']
|
584
|
+
else: # en
|
585
|
+
months = ['January', 'February', 'March', 'April', 'May', 'June',
|
586
|
+
'July', 'August', 'September', 'October', 'November', 'December']
|
587
|
+
return months[self.month - 1]
|
588
|
+
|
589
|
+
def format_relative(self, reference_date: Optional['Date'] = None, lang: str = 'zh') -> str:
|
590
|
+
"""相对时间格式化"""
|
591
|
+
if reference_date is None:
|
592
|
+
reference_date = Date.today()
|
593
|
+
|
594
|
+
diff_days = reference_date.calculate_difference_days(self)
|
595
|
+
|
596
|
+
if lang == 'zh':
|
597
|
+
if diff_days == 0:
|
598
|
+
return "今天"
|
599
|
+
elif diff_days == 1:
|
600
|
+
return "明天"
|
601
|
+
elif diff_days == -1:
|
602
|
+
return "昨天"
|
603
|
+
elif diff_days == 2:
|
604
|
+
return "后天"
|
605
|
+
elif diff_days == -2:
|
606
|
+
return "前天"
|
607
|
+
elif 3 <= diff_days <= 6:
|
608
|
+
return f"{diff_days}天后"
|
609
|
+
elif -6 <= diff_days <= -3:
|
610
|
+
return f"{abs(diff_days)}天前"
|
611
|
+
elif 7 <= diff_days <= 13:
|
612
|
+
return "下周"
|
613
|
+
elif -13 <= diff_days <= -7:
|
614
|
+
return "上周"
|
615
|
+
elif diff_days > 0:
|
616
|
+
return f"{diff_days}天后"
|
617
|
+
else:
|
618
|
+
return f"{abs(diff_days)}天前"
|
619
|
+
else: # en
|
620
|
+
if diff_days == 0:
|
621
|
+
return "today"
|
622
|
+
elif diff_days == 1:
|
623
|
+
return "tomorrow"
|
624
|
+
elif diff_days == -1:
|
625
|
+
return "yesterday"
|
626
|
+
elif diff_days > 0:
|
627
|
+
return f"in {diff_days} days"
|
628
|
+
else:
|
629
|
+
return f"{abs(diff_days)} days ago"
|
630
|
+
|
338
631
|
# =============================================
|
339
632
|
# get_* 系列:获取方法
|
340
633
|
# =============================================
|
@@ -364,6 +657,38 @@ class Date:
|
|
364
657
|
"""获取年末日期"""
|
365
658
|
return self._create_with_same_format(self.year, 12, 31)
|
366
659
|
|
660
|
+
def get_quarter_start(self) -> 'Date':
|
661
|
+
"""获取季度开始日期"""
|
662
|
+
quarter = self.get_quarter()
|
663
|
+
start_month = (quarter - 1) * 3 + 1
|
664
|
+
return self._create_with_same_format(self.year, start_month, 1)
|
665
|
+
|
666
|
+
def get_quarter_end(self) -> 'Date':
|
667
|
+
"""获取季度结束日期"""
|
668
|
+
quarter = self.get_quarter()
|
669
|
+
end_month = quarter * 3
|
670
|
+
if end_month == 3:
|
671
|
+
end_day = 31
|
672
|
+
elif end_month == 6:
|
673
|
+
end_day = 30
|
674
|
+
elif end_month == 9:
|
675
|
+
end_day = 30
|
676
|
+
else: # 12
|
677
|
+
end_day = 31
|
678
|
+
return self._create_with_same_format(self.year, end_month, end_day)
|
679
|
+
|
680
|
+
def get_week_start(self) -> 'Date':
|
681
|
+
"""获取本周开始日期(周一)"""
|
682
|
+
days_since_monday = self.get_weekday()
|
683
|
+
start_date = self.subtract_days(days_since_monday)
|
684
|
+
return start_date
|
685
|
+
|
686
|
+
def get_week_end(self) -> 'Date':
|
687
|
+
"""获取本周结束日期(周日)"""
|
688
|
+
days_until_sunday = 6 - self.get_weekday()
|
689
|
+
end_date = self.add_days(days_until_sunday)
|
690
|
+
return end_date
|
691
|
+
|
367
692
|
def get_days_in_month(self) -> int:
|
368
693
|
"""获取当月天数"""
|
369
694
|
return calendar.monthrange(self.year, self.month)[1]
|
@@ -372,6 +697,18 @@ class Date:
|
|
372
697
|
"""获取当年天数"""
|
373
698
|
return 366 if calendar.isleap(self.year) else 365
|
374
699
|
|
700
|
+
def get_quarter(self) -> int:
|
701
|
+
"""获取季度 (1-4)"""
|
702
|
+
return (self.month - 1) // 3 + 1
|
703
|
+
|
704
|
+
def get_week_of_year(self) -> int:
|
705
|
+
"""获取年内第几周 (ISO周数)"""
|
706
|
+
return self.to_date_object().isocalendar()[1]
|
707
|
+
|
708
|
+
def get_day_of_year(self) -> int:
|
709
|
+
"""获取年内第几天 (1-366)"""
|
710
|
+
return self.to_date_object().timetuple().tm_yday
|
711
|
+
|
375
712
|
# =============================================
|
376
713
|
# is_* 系列:判断方法
|
377
714
|
# =============================================
|
@@ -404,6 +741,127 @@ class Date:
|
|
404
741
|
"""是否为年末"""
|
405
742
|
return self.month == 12 and self.day == 31
|
406
743
|
|
744
|
+
def is_business_day(self) -> bool:
|
745
|
+
"""是否为工作日(简单版本,仅考虑周末)"""
|
746
|
+
return self.get_weekday() < 5
|
747
|
+
|
748
|
+
def is_quarter_start(self) -> bool:
|
749
|
+
"""是否为季度开始"""
|
750
|
+
return self.day == 1 and self.month in [1, 4, 7, 10]
|
751
|
+
|
752
|
+
def is_quarter_end(self) -> bool:
|
753
|
+
"""是否为季度结束"""
|
754
|
+
quarter_end_months = {3: 31, 6: 30, 9: 30, 12: 31}
|
755
|
+
return (self.month in quarter_end_months and
|
756
|
+
self.day == quarter_end_months[self.month])
|
757
|
+
|
758
|
+
def is_holiday(self, country: str = 'CN') -> bool:
|
759
|
+
"""是否为节假日(增强版实现)
|
760
|
+
|
761
|
+
Args:
|
762
|
+
country: 国家代码 ('CN', 'US', 'UK', 'JP' 等)
|
763
|
+
|
764
|
+
Returns:
|
765
|
+
是否为节假日
|
766
|
+
|
767
|
+
Note:
|
768
|
+
支持多国节假日,包含农历节日计算
|
769
|
+
"""
|
770
|
+
if country == 'CN':
|
771
|
+
# 中国固定节假日
|
772
|
+
fixed_holidays = [
|
773
|
+
(1, 1), # 元旦
|
774
|
+
(5, 1), # 劳动节
|
775
|
+
(10, 1), # 国庆节
|
776
|
+
(10, 2), # 国庆节
|
777
|
+
(10, 3), # 国庆节
|
778
|
+
(12, 13), # 国家公祭日
|
779
|
+
]
|
780
|
+
|
781
|
+
# 检查固定节假日
|
782
|
+
if (self.month, self.day) in fixed_holidays:
|
783
|
+
return True
|
784
|
+
|
785
|
+
# 特殊节日计算
|
786
|
+
# 春节:农历正月初一(简化版本,实际需要农历计算)
|
787
|
+
# 清明节:4月4日或5日(简化)
|
788
|
+
if self.month == 4 and self.day in [4, 5]:
|
789
|
+
return True
|
790
|
+
|
791
|
+
# 端午节、中秋节等需要农历计算,这里提供扩展接口
|
792
|
+
return self._check_lunar_holidays()
|
793
|
+
|
794
|
+
elif country == 'US':
|
795
|
+
# 美国节假日
|
796
|
+
fixed_holidays = [
|
797
|
+
(1, 1), # New Year's Day
|
798
|
+
(7, 4), # Independence Day
|
799
|
+
(12, 25), # Christmas Day
|
800
|
+
(11, 11), # Veterans Day
|
801
|
+
]
|
802
|
+
|
803
|
+
if (self.month, self.day) in fixed_holidays:
|
804
|
+
return True
|
805
|
+
|
806
|
+
# 感恩节:11月第四个星期四
|
807
|
+
if self.month == 11:
|
808
|
+
return self._is_thanksgiving()
|
809
|
+
|
810
|
+
elif country == 'JP':
|
811
|
+
# 日本节假日
|
812
|
+
fixed_holidays = [
|
813
|
+
(1, 1), # 元日
|
814
|
+
(2, 11), # 建国記念の日
|
815
|
+
(4, 29), # 昭和の日
|
816
|
+
(5, 3), # 憲法記念日
|
817
|
+
(5, 4), # みどりの日
|
818
|
+
(5, 5), # こどもの日
|
819
|
+
(11, 3), # 文化の日
|
820
|
+
(11, 23), # 勤労感謝の日
|
821
|
+
(12, 23), # 天皇誕生日
|
822
|
+
]
|
823
|
+
return (self.month, self.day) in fixed_holidays
|
824
|
+
|
825
|
+
elif country == 'UK':
|
826
|
+
# 英国节假日
|
827
|
+
fixed_holidays = [
|
828
|
+
(1, 1), # New Year's Day
|
829
|
+
(12, 25), # Christmas Day
|
830
|
+
(12, 26), # Boxing Day
|
831
|
+
]
|
832
|
+
return (self.month, self.day) in fixed_holidays
|
833
|
+
|
834
|
+
else:
|
835
|
+
# 未知国家,返回False
|
836
|
+
return False
|
837
|
+
|
838
|
+
def _check_lunar_holidays(self) -> bool:
|
839
|
+
"""检查农历节假日(扩展接口)
|
840
|
+
|
841
|
+
Note:
|
842
|
+
这是一个扩展接口,实际项目中可以集成农历库
|
843
|
+
如 `lunardate` 或 `zhdate` 等第三方库
|
844
|
+
"""
|
845
|
+
# 这里可以扩展农历节日计算
|
846
|
+
# 目前返回 False,保持轻量级
|
847
|
+
return False
|
848
|
+
|
849
|
+
def _is_thanksgiving(self) -> bool:
|
850
|
+
"""判断是否为美国感恩节(11月第四个星期四)"""
|
851
|
+
if self.month != 11:
|
852
|
+
return False
|
853
|
+
|
854
|
+
# 找到11月第一天是星期几
|
855
|
+
first_day = Date(self.year, 11, 1)
|
856
|
+
first_weekday = first_day.get_weekday()
|
857
|
+
|
858
|
+
# 计算第四个星期四的日期
|
859
|
+
# 星期四是weekday=3
|
860
|
+
days_to_first_thursday = (3 - first_weekday) % 7
|
861
|
+
fourth_thursday = 1 + days_to_first_thursday + 21 # 第四个星期四
|
862
|
+
|
863
|
+
return self.day == fourth_thursday
|
864
|
+
|
407
865
|
# =============================================
|
408
866
|
# add_*/subtract_* 系列:运算方法
|
409
867
|
# =============================================
|
@@ -471,6 +929,120 @@ class Date:
|
|
471
929
|
"""计算与另一个日期的月数差(近似)"""
|
472
930
|
return (other.year - self.year) * 12 + (other.month - self.month)
|
473
931
|
|
932
|
+
def calculate_age_years(self, reference_date: Optional['Date'] = None) -> int:
|
933
|
+
"""计算年龄(以年为单位)"""
|
934
|
+
if reference_date is None:
|
935
|
+
reference_date = Date.today()
|
936
|
+
|
937
|
+
age = reference_date.year - self.year
|
938
|
+
|
939
|
+
# 如果还没到生日,年龄减1
|
940
|
+
if (reference_date.month, reference_date.day) < (self.month, self.day):
|
941
|
+
age -= 1
|
942
|
+
|
943
|
+
return age
|
944
|
+
|
945
|
+
def days_until(self, target_date: 'Date') -> int:
|
946
|
+
"""计算距离目标日期还有多少天"""
|
947
|
+
return self.calculate_difference_days(target_date)
|
948
|
+
|
949
|
+
def days_since(self, start_date: 'Date') -> int:
|
950
|
+
"""计算从起始日期过了多少天"""
|
951
|
+
return start_date.calculate_difference_days(self)
|
952
|
+
|
953
|
+
# =============================================
|
954
|
+
# 批量处理方法
|
955
|
+
# =============================================
|
956
|
+
|
957
|
+
@classmethod
|
958
|
+
def batch_create(cls, date_strings: List[str]) -> List['Date']:
|
959
|
+
"""批量创建Date对象
|
960
|
+
|
961
|
+
Args:
|
962
|
+
date_strings: 日期字符串列表
|
963
|
+
|
964
|
+
Returns:
|
965
|
+
Date对象列表
|
966
|
+
|
967
|
+
Raises:
|
968
|
+
InvalidDateFormatError: 如果某个字符串格式无效
|
969
|
+
"""
|
970
|
+
result = []
|
971
|
+
for date_str in date_strings:
|
972
|
+
try:
|
973
|
+
result.append(cls(date_str))
|
974
|
+
except (InvalidDateFormatError, InvalidDateValueError) as e:
|
975
|
+
cls._logger.error(f"批量创建失败: {date_str} - {e}")
|
976
|
+
raise
|
977
|
+
return result
|
978
|
+
|
979
|
+
@classmethod
|
980
|
+
def batch_format(cls, dates: List['Date'], format_type: str = 'iso') -> List[str]:
|
981
|
+
"""批量格式化日期
|
982
|
+
|
983
|
+
Args:
|
984
|
+
dates: Date对象列表
|
985
|
+
format_type: 格式类型 ('iso', 'chinese', 'compact' 等)
|
986
|
+
|
987
|
+
Returns:
|
988
|
+
格式化后的字符串列表
|
989
|
+
"""
|
990
|
+
format_methods = {
|
991
|
+
'iso': lambda d: d.format_iso(),
|
992
|
+
'chinese': lambda d: d.format_chinese(),
|
993
|
+
'compact': lambda d: d.format_compact(),
|
994
|
+
'slash': lambda d: d.format_slash(),
|
995
|
+
'dot': lambda d: d.format_dot(),
|
996
|
+
'default': lambda d: d.format_default()
|
997
|
+
}
|
998
|
+
|
999
|
+
format_func = format_methods.get(format_type, lambda d: d.format_default())
|
1000
|
+
return [format_func(date) for date in dates]
|
1001
|
+
|
1002
|
+
@classmethod
|
1003
|
+
def batch_add_days(cls, dates: List['Date'], days: int) -> List['Date']:
|
1004
|
+
"""批量添加天数
|
1005
|
+
|
1006
|
+
Args:
|
1007
|
+
dates: Date对象列表
|
1008
|
+
days: 要添加的天数
|
1009
|
+
|
1010
|
+
Returns:
|
1011
|
+
新的Date对象列表
|
1012
|
+
"""
|
1013
|
+
return [date.add_days(days) for date in dates]
|
1014
|
+
|
1015
|
+
def apply_business_rule(self, rule: str, **kwargs) -> 'Date':
|
1016
|
+
"""应用业务规则
|
1017
|
+
|
1018
|
+
Args:
|
1019
|
+
rule: 规则名称
|
1020
|
+
- 'month_end': 移动到月末
|
1021
|
+
- 'quarter_end': 移动到季度末
|
1022
|
+
- 'next_business_day': 移动到下一个工作日
|
1023
|
+
- 'prev_business_day': 移动到上一个工作日
|
1024
|
+
**kwargs: 规则参数
|
1025
|
+
|
1026
|
+
Returns:
|
1027
|
+
应用规则后的新Date对象
|
1028
|
+
"""
|
1029
|
+
if rule == 'month_end':
|
1030
|
+
return self.get_month_end()
|
1031
|
+
elif rule == 'quarter_end':
|
1032
|
+
return self.get_quarter_end()
|
1033
|
+
elif rule == 'next_business_day':
|
1034
|
+
current = self
|
1035
|
+
while not current.is_business_day():
|
1036
|
+
current = current.add_days(1)
|
1037
|
+
return current
|
1038
|
+
elif rule == 'prev_business_day':
|
1039
|
+
current = self
|
1040
|
+
while not current.is_business_day():
|
1041
|
+
current = current.subtract_days(1)
|
1042
|
+
return current
|
1043
|
+
else:
|
1044
|
+
raise ValueError(f"未知的业务规则: {rule}")
|
1045
|
+
|
474
1046
|
# =============================================
|
475
1047
|
# 配置和日志方法
|
476
1048
|
# =============================================
|