staran 1.0.6__py3-none-any.whl → 1.0.8__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 +62 -2
- staran/date/core.py +731 -30
- staran/date/examples/enhanced_features.py +230 -0
- staran/date/examples/v108_features_demo.py +257 -0
- staran/date/i18n.py +376 -0
- staran/date/lunar.py +320 -0
- staran/date/tests/test_enhancements.py +295 -0
- staran/date/tests/test_v108_features.py +400 -0
- staran-1.0.8.dist-info/METADATA +371 -0
- staran-1.0.8.dist-info/RECORD +21 -0
- staran-1.0.6.dist-info/METADATA +0 -170
- staran-1.0.6.dist-info/RECORD +0 -15
- {staran-1.0.6.dist-info → staran-1.0.8.dist-info}/WHEEL +0 -0
- {staran-1.0.6.dist-info → staran-1.0.8.dist-info}/licenses/LICENSE +0 -0
- {staran-1.0.6.dist-info → staran-1.0.8.dist-info}/top_level.txt +0 -0
staran/date/core.py
CHANGED
@@ -17,6 +17,10 @@ import time
|
|
17
17
|
from typing import Union, Optional, Tuple, Dict, Any, List
|
18
18
|
from functools import lru_cache
|
19
19
|
|
20
|
+
# 导入农历和多语言模块
|
21
|
+
from .lunar import LunarDate
|
22
|
+
from .i18n import Language
|
23
|
+
|
20
24
|
class DateError(ValueError):
|
21
25
|
"""Date模块的特定异常基类"""
|
22
26
|
pass
|
@@ -80,11 +84,18 @@ class Date:
|
|
80
84
|
支持YYYY、YYYYMM、YYYYMMDD等多种输入格式,并在运算中
|
81
85
|
自动保持原始格式。
|
82
86
|
|
87
|
+
v1.0.8 新增功能:
|
88
|
+
- 农历日期支持 (from_lunar, to_lunar, format_lunar等)
|
89
|
+
- 多语言配置 (中简、中繁、日、英四种语言)
|
90
|
+
- 全局语言设置,一次配置全局生效
|
91
|
+
|
83
92
|
特性:
|
84
93
|
----
|
85
|
-
-
|
94
|
+
- 120+个统一命名的API方法
|
86
95
|
- 智能格式记忆和保持
|
87
96
|
- 企业级日志记录
|
97
|
+
- 农历与公历互转
|
98
|
+
- 多语言本地化支持
|
88
99
|
- 类型安全的日期转换
|
89
100
|
- 向后兼容的旧API支持
|
90
101
|
|
@@ -98,6 +109,14 @@ class Date:
|
|
98
109
|
>>> print(date1.add_months(2)) # 202506
|
99
110
|
>>> print(date2.add_days(10)) # 20250425
|
100
111
|
>>>
|
112
|
+
>>> # 农历支持 (v1.0.8)
|
113
|
+
>>> lunar_date = Date.from_lunar(2025, 3, 15) # 农历2025年三月十五
|
114
|
+
>>> print(lunar_date.to_lunar().format_chinese()) # 农历2025年三月十五
|
115
|
+
>>>
|
116
|
+
>>> # 多语言支持 (v1.0.8)
|
117
|
+
>>> Date.set_language('en_US') # 设置全局语言为英语
|
118
|
+
>>> print(date2.format_localized()) # 04/15/2025
|
119
|
+
>>> print(date2.format_weekday_localized()) # Tuesday
|
101
120
|
>>> # 统一API命名
|
102
121
|
>>> date = Date('20250415')
|
103
122
|
>>> print(date.format_iso()) # 2025-04-15
|
@@ -212,9 +231,14 @@ class Date:
|
|
212
231
|
|
213
232
|
def _validate_date(self):
|
214
233
|
"""验证日期的有效性"""
|
234
|
+
# 基本范围检查
|
215
235
|
if not (1 <= self.month <= 12):
|
216
236
|
raise InvalidDateValueError(f"无效的月份: {self.month}")
|
217
237
|
|
238
|
+
# 年份合理性检查
|
239
|
+
if not (1900 <= self.year <= 3000):
|
240
|
+
self._logger.warning(f"年份 {self.year} 超出常规范围 (1900-3000)")
|
241
|
+
|
218
242
|
max_days = calendar.monthrange(self.year, self.month)[1]
|
219
243
|
if not (1 <= self.day <= max_days):
|
220
244
|
raise InvalidDateValueError(f"无效的日期: {self.day} (对于 {self.year}-{self.month})")
|
@@ -223,7 +247,37 @@ class Date:
|
|
223
247
|
datetime.date(self.year, self.month, self.day)
|
224
248
|
except ValueError as e:
|
225
249
|
raise InvalidDateValueError(f"无效的日期: {self.year}-{self.month}-{self.day}") from e
|
250
|
+
|
251
|
+
# 特殊日期检查
|
252
|
+
self._check_special_dates()
|
253
|
+
|
254
|
+
def _check_special_dates(self):
|
255
|
+
"""检查特殊日期"""
|
256
|
+
# 检查是否为闰年2月29日
|
257
|
+
if self.month == 2 and self.day == 29 and not calendar.isleap(self.year):
|
258
|
+
raise InvalidDateValueError(f"非闰年 {self.year} 不存在2月29日")
|
259
|
+
|
260
|
+
# 检查历史边界日期
|
261
|
+
if self.year < 1582 and self.month == 10 and 5 <= self.day <= 14:
|
262
|
+
self._logger.warning("日期位于格里高利历改革期间 (1582年10月5-14日)")
|
263
|
+
|
264
|
+
@classmethod
|
265
|
+
def is_valid_date_string(cls, date_string: str) -> bool:
|
266
|
+
"""检查日期字符串是否有效
|
267
|
+
|
268
|
+
Args:
|
269
|
+
date_string: 日期字符串
|
270
|
+
|
271
|
+
Returns:
|
272
|
+
是否为有效日期字符串
|
273
|
+
"""
|
274
|
+
try:
|
275
|
+
cls(date_string)
|
276
|
+
return True
|
277
|
+
except (InvalidDateFormatError, InvalidDateValueError):
|
278
|
+
return False
|
226
279
|
|
280
|
+
@lru_cache(maxsize=128)
|
227
281
|
def _create_with_same_format(self, year: int, month: int, day: int) -> 'Date':
|
228
282
|
"""创建具有相同格式的新Date对象"""
|
229
283
|
new_date = Date(year, month, day)
|
@@ -252,9 +306,17 @@ class Date:
|
|
252
306
|
return cls(date_string)
|
253
307
|
|
254
308
|
@classmethod
|
255
|
-
def from_timestamp(cls, timestamp: Union[int, float]) -> 'Date':
|
256
|
-
"""从时间戳创建Date对象
|
309
|
+
def from_timestamp(cls, timestamp: Union[int, float], timezone_offset: int = 0) -> 'Date':
|
310
|
+
"""从时间戳创建Date对象
|
311
|
+
|
312
|
+
Args:
|
313
|
+
timestamp: Unix时间戳
|
314
|
+
timezone_offset: 时区偏移小时数 (如 +8 表示东八区)
|
315
|
+
"""
|
257
316
|
dt = datetime.datetime.fromtimestamp(timestamp)
|
317
|
+
# 调整时区
|
318
|
+
if timezone_offset != 0:
|
319
|
+
dt = dt + datetime.timedelta(hours=timezone_offset)
|
258
320
|
return cls(dt.date())
|
259
321
|
|
260
322
|
@classmethod
|
@@ -267,6 +329,83 @@ class Date:
|
|
267
329
|
"""创建今日Date对象"""
|
268
330
|
return cls(datetime.date.today())
|
269
331
|
|
332
|
+
@classmethod
|
333
|
+
def from_lunar(cls, year: int, month: int, day: int, is_leap: bool = False) -> 'Date':
|
334
|
+
"""从农历日期创建Date对象 (v1.0.8)
|
335
|
+
|
336
|
+
Args:
|
337
|
+
year: 农历年份
|
338
|
+
month: 农历月份
|
339
|
+
day: 农历日期
|
340
|
+
is_leap: 是否闰月
|
341
|
+
|
342
|
+
Returns:
|
343
|
+
对应的公历Date对象
|
344
|
+
|
345
|
+
Example:
|
346
|
+
>>> date = Date.from_lunar(2025, 3, 15) # 农历2025年三月十五
|
347
|
+
"""
|
348
|
+
lunar_date = LunarDate(year, month, day, is_leap)
|
349
|
+
solar_date = lunar_date.to_solar()
|
350
|
+
return cls(solar_date)
|
351
|
+
|
352
|
+
@classmethod
|
353
|
+
def from_lunar_string(cls, lunar_string: str) -> 'Date':
|
354
|
+
"""从农历字符串创建Date对象 (v1.0.8)
|
355
|
+
|
356
|
+
支持格式:
|
357
|
+
- "20250315" (农历2025年3月15日)
|
358
|
+
- "2025闰0315" (农历2025年闰3月15日)
|
359
|
+
|
360
|
+
Args:
|
361
|
+
lunar_string: 农历日期字符串
|
362
|
+
|
363
|
+
Returns:
|
364
|
+
对应的公历Date对象
|
365
|
+
"""
|
366
|
+
# 解析闰月标记
|
367
|
+
is_leap = '闰' in lunar_string
|
368
|
+
clean_string = lunar_string.replace('闰', '')
|
369
|
+
|
370
|
+
if len(clean_string) != 8:
|
371
|
+
raise InvalidDateFormatError(f"农历日期字符串格式无效: {lunar_string}")
|
372
|
+
|
373
|
+
year = int(clean_string[:4])
|
374
|
+
month = int(clean_string[4:6])
|
375
|
+
day = int(clean_string[6:8])
|
376
|
+
|
377
|
+
return cls.from_lunar(year, month, day, is_leap)
|
378
|
+
|
379
|
+
@classmethod
|
380
|
+
def set_language(cls, language_code: str) -> None:
|
381
|
+
"""设置全局语言 (v1.0.8)
|
382
|
+
|
383
|
+
一次设置,全局生效。支持中简、中繁、日、英四种语言。
|
384
|
+
|
385
|
+
Args:
|
386
|
+
language_code: 语言代码
|
387
|
+
- 'zh_CN': 中文简体
|
388
|
+
- 'zh_TW': 中文繁体
|
389
|
+
- 'ja_JP': 日语
|
390
|
+
- 'en_US': 英语
|
391
|
+
|
392
|
+
Example:
|
393
|
+
>>> Date.set_language('en_US') # 设置为英语
|
394
|
+
>>> Date.set_language('zh_TW') # 设置为繁体中文
|
395
|
+
"""
|
396
|
+
Language.set_global_language(language_code)
|
397
|
+
cls._logger.info(f"全局语言已设置为: {language_code}")
|
398
|
+
|
399
|
+
@classmethod
|
400
|
+
def get_language(cls) -> str:
|
401
|
+
"""获取当前全局语言设置 (v1.0.8)"""
|
402
|
+
return Language.get_global_language()
|
403
|
+
|
404
|
+
@classmethod
|
405
|
+
def get_supported_languages(cls) -> Dict[str, str]:
|
406
|
+
"""获取支持的语言列表 (v1.0.8)"""
|
407
|
+
return Language.get_supported_languages()
|
408
|
+
|
270
409
|
@classmethod
|
271
410
|
def date_range(cls, start: Union[str, 'Date'], end: Union[str, 'Date'],
|
272
411
|
step: int = 1) -> List['Date']:
|
@@ -298,6 +437,64 @@ class Date:
|
|
298
437
|
dates = cls.date_range(start, end)
|
299
438
|
return [date for date in dates if date.is_business_day()]
|
300
439
|
|
440
|
+
@classmethod
|
441
|
+
def weekends(cls, start: Union[str, 'Date'], end: Union[str, 'Date']) -> List['Date']:
|
442
|
+
"""生成周末日期列表"""
|
443
|
+
dates = cls.date_range(start, end)
|
444
|
+
return [date for date in dates if date.is_weekend()]
|
445
|
+
|
446
|
+
@classmethod
|
447
|
+
def month_range(cls, start_year_month: Union[str, 'Date'], months: int) -> List['Date']:
|
448
|
+
"""生成月份范围
|
449
|
+
|
450
|
+
Args:
|
451
|
+
start_year_month: 开始年月 (如 "202501" 或 Date对象)
|
452
|
+
months: 月份数量
|
453
|
+
|
454
|
+
Returns:
|
455
|
+
月份第一天的日期列表
|
456
|
+
|
457
|
+
Example:
|
458
|
+
Date.month_range("202501", 3) # [202501, 202502, 202503]
|
459
|
+
"""
|
460
|
+
if isinstance(start_year_month, str):
|
461
|
+
start_date = cls.from_string(start_year_month)
|
462
|
+
else:
|
463
|
+
start_date = start_year_month
|
464
|
+
|
465
|
+
# 确保是月初
|
466
|
+
start_date = start_date.get_month_start()
|
467
|
+
|
468
|
+
result = []
|
469
|
+
current = start_date
|
470
|
+
for _ in range(months):
|
471
|
+
result.append(current)
|
472
|
+
current = current.add_months(1)
|
473
|
+
|
474
|
+
return result
|
475
|
+
|
476
|
+
@classmethod
|
477
|
+
def quarter_dates(cls, year: int) -> Dict[int, Tuple['Date', 'Date']]:
|
478
|
+
"""获取指定年份的季度起止日期
|
479
|
+
|
480
|
+
Args:
|
481
|
+
year: 年份
|
482
|
+
|
483
|
+
Returns:
|
484
|
+
季度字典 {1: (Q1开始, Q1结束), 2: (Q2开始, Q2结束), ...}
|
485
|
+
"""
|
486
|
+
quarters = {}
|
487
|
+
for quarter in range(1, 5):
|
488
|
+
start_month = (quarter - 1) * 3 + 1
|
489
|
+
end_month = quarter * 3
|
490
|
+
|
491
|
+
start_date = cls(year, start_month, 1)
|
492
|
+
end_date = cls(year, end_month, 1).get_month_end()
|
493
|
+
|
494
|
+
quarters[quarter] = (start_date, end_date)
|
495
|
+
|
496
|
+
return quarters
|
497
|
+
|
301
498
|
# =============================================
|
302
499
|
# to_* 系列:转换方法
|
303
500
|
# =============================================
|
@@ -306,14 +503,6 @@ class Date:
|
|
306
503
|
"""转为元组 (year, month, day)"""
|
307
504
|
return (self.year, self.month, self.day)
|
308
505
|
|
309
|
-
def to_dict(self) -> Dict[str, int]:
|
310
|
-
"""转为字典"""
|
311
|
-
return {
|
312
|
-
'year': self.year,
|
313
|
-
'month': self.month,
|
314
|
-
'day': self.day
|
315
|
-
}
|
316
|
-
|
317
506
|
def to_date_object(self) -> datetime.date:
|
318
507
|
"""转为datetime.date对象"""
|
319
508
|
return datetime.date(self.year, self.month, self.day)
|
@@ -322,29 +511,139 @@ class Date:
|
|
322
511
|
"""转为datetime.datetime对象"""
|
323
512
|
return datetime.datetime(self.year, self.month, self.day)
|
324
513
|
|
325
|
-
def to_timestamp(self) -> float:
|
326
|
-
"""转为时间戳
|
327
|
-
|
514
|
+
def to_timestamp(self, timezone_offset: int = 0) -> float:
|
515
|
+
"""转为时间戳
|
516
|
+
|
517
|
+
Args:
|
518
|
+
timezone_offset: 时区偏移小时数 (如 +8 表示东八区)
|
519
|
+
|
520
|
+
Returns:
|
521
|
+
Unix时间戳
|
522
|
+
"""
|
523
|
+
dt = self.to_datetime_object()
|
524
|
+
# 调整时区
|
525
|
+
if timezone_offset != 0:
|
526
|
+
dt = dt - datetime.timedelta(hours=timezone_offset)
|
527
|
+
return dt.timestamp()
|
528
|
+
|
529
|
+
def to_lunar(self) -> LunarDate:
|
530
|
+
"""转为农历日期对象 (v1.0.8)
|
531
|
+
|
532
|
+
Returns:
|
533
|
+
对应的农历日期对象
|
534
|
+
|
535
|
+
Example:
|
536
|
+
>>> date = Date('20250415')
|
537
|
+
>>> lunar = date.to_lunar()
|
538
|
+
>>> print(lunar.format_chinese()) # 农历2025年三月十八
|
539
|
+
"""
|
540
|
+
return LunarDate.from_solar(self.to_date_object())
|
541
|
+
|
542
|
+
def to_lunar_string(self, compact: bool = True) -> str:
|
543
|
+
"""转为农历字符串 (v1.0.8)
|
544
|
+
|
545
|
+
Args:
|
546
|
+
compact: 是否使用紧凑格式
|
547
|
+
|
548
|
+
Returns:
|
549
|
+
农历日期字符串
|
550
|
+
|
551
|
+
Example:
|
552
|
+
>>> date = Date('20250415')
|
553
|
+
>>> print(date.to_lunar_string()) # 20250318
|
554
|
+
>>> print(date.to_lunar_string(False)) # 农历2025年三月十八
|
555
|
+
"""
|
556
|
+
lunar = self.to_lunar()
|
557
|
+
return lunar.format_compact() if compact else lunar.format_chinese()
|
328
558
|
|
329
|
-
def to_json(self) -> str:
|
330
|
-
"""转为JSON字符串
|
559
|
+
def to_json(self, include_metadata: bool = True) -> str:
|
560
|
+
"""转为JSON字符串
|
561
|
+
|
562
|
+
Args:
|
563
|
+
include_metadata: 是否包含元数据(格式、版本等)
|
564
|
+
|
565
|
+
Returns:
|
566
|
+
JSON字符串
|
567
|
+
"""
|
331
568
|
import json
|
332
|
-
|
569
|
+
|
570
|
+
data = {
|
333
571
|
'date': self.format_iso(),
|
334
|
-
'format': self._input_format,
|
335
572
|
'year': self.year,
|
336
573
|
'month': self.month,
|
337
574
|
'day': self.day
|
338
|
-
}
|
575
|
+
}
|
576
|
+
|
577
|
+
if include_metadata:
|
578
|
+
data.update({
|
579
|
+
'format': self._input_format,
|
580
|
+
'weekday': self.get_weekday(),
|
581
|
+
'is_weekend': self.is_weekend(),
|
582
|
+
'quarter': self.get_quarter(),
|
583
|
+
'version': '1.0.7'
|
584
|
+
})
|
585
|
+
|
586
|
+
return json.dumps(data, ensure_ascii=False)
|
339
587
|
|
340
588
|
@classmethod
|
341
589
|
def from_json(cls, json_str: str) -> 'Date':
|
342
|
-
"""从JSON字符串创建Date对象
|
590
|
+
"""从JSON字符串创建Date对象
|
591
|
+
|
592
|
+
Args:
|
593
|
+
json_str: JSON字符串
|
594
|
+
|
595
|
+
Returns:
|
596
|
+
Date对象
|
597
|
+
|
598
|
+
Raises:
|
599
|
+
ValueError: JSON格式错误或缺少必要字段
|
600
|
+
"""
|
343
601
|
import json
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
602
|
+
|
603
|
+
try:
|
604
|
+
data = json.loads(json_str)
|
605
|
+
except json.JSONDecodeError as e:
|
606
|
+
raise ValueError(f"无效的JSON格式: {e}")
|
607
|
+
|
608
|
+
# 检查必要字段
|
609
|
+
required_fields = ['year', 'month', 'day']
|
610
|
+
missing_fields = [field for field in required_fields if field not in data]
|
611
|
+
if missing_fields:
|
612
|
+
raise ValueError(f"JSON缺少必要字段: {missing_fields}")
|
613
|
+
|
614
|
+
try:
|
615
|
+
date = cls(data['year'], data['month'], data['day'])
|
616
|
+
date._input_format = data.get('format', 'iso')
|
617
|
+
return date
|
618
|
+
except (InvalidDateFormatError, InvalidDateValueError) as e:
|
619
|
+
raise ValueError(f"JSON中的日期数据无效: {e}")
|
620
|
+
|
621
|
+
def to_dict(self, include_metadata: bool = False) -> Dict[str, Any]:
|
622
|
+
"""转为字典
|
623
|
+
|
624
|
+
Args:
|
625
|
+
include_metadata: 是否包含元数据
|
626
|
+
|
627
|
+
Returns:
|
628
|
+
字典表示
|
629
|
+
"""
|
630
|
+
result = {
|
631
|
+
'year': self.year,
|
632
|
+
'month': self.month,
|
633
|
+
'day': self.day
|
634
|
+
}
|
635
|
+
|
636
|
+
if include_metadata:
|
637
|
+
result.update({
|
638
|
+
'format': self._input_format,
|
639
|
+
'weekday': self.get_weekday(),
|
640
|
+
'is_weekend': self.is_weekend(),
|
641
|
+
'quarter': self.get_quarter(),
|
642
|
+
'iso_string': self.format_iso(),
|
643
|
+
'compact_string': self.format_compact()
|
644
|
+
})
|
645
|
+
|
646
|
+
return result
|
348
647
|
|
349
648
|
# =============================================
|
350
649
|
# format_* 系列:格式化方法
|
@@ -455,6 +754,155 @@ class Date:
|
|
455
754
|
else:
|
456
755
|
return f"{abs(diff_days)} days ago"
|
457
756
|
|
757
|
+
def format_localized(self, format_type: str = 'full', language_code: Optional[str] = None) -> str:
|
758
|
+
"""多语言本地化格式 (v1.0.8)
|
759
|
+
|
760
|
+
Args:
|
761
|
+
format_type: 格式类型 (full, short, year_month, month_day)
|
762
|
+
language_code: 语言代码,None时使用全局设置
|
763
|
+
|
764
|
+
Returns:
|
765
|
+
本地化格式的日期字符串
|
766
|
+
|
767
|
+
Example:
|
768
|
+
>>> Date.set_language('en_US')
|
769
|
+
>>> date = Date('20250415')
|
770
|
+
>>> print(date.format_localized()) # 04/15/2025
|
771
|
+
>>> print(date.format_localized('short')) # 04/15/2025
|
772
|
+
"""
|
773
|
+
return Language.format_date(self.year, self.month, self.day, format_type, language_code)
|
774
|
+
|
775
|
+
def format_weekday_localized(self, short: bool = False, language_code: Optional[str] = None) -> str:
|
776
|
+
"""多语言星期几格式 (v1.0.8)
|
777
|
+
|
778
|
+
Args:
|
779
|
+
short: 是否使用短名称
|
780
|
+
language_code: 语言代码,None时使用全局设置
|
781
|
+
|
782
|
+
Returns:
|
783
|
+
本地化的星期几名称
|
784
|
+
|
785
|
+
Example:
|
786
|
+
>>> Date.set_language('ja_JP')
|
787
|
+
>>> date = Date('20250415') # 星期二
|
788
|
+
>>> print(date.format_weekday_localized()) # 火曜日
|
789
|
+
>>> print(date.format_weekday_localized(short=True)) # 火
|
790
|
+
"""
|
791
|
+
weekday_index = self.get_weekday()
|
792
|
+
return Language.get_weekday_name(weekday_index, short, language_code)
|
793
|
+
|
794
|
+
def format_month_localized(self, short: bool = False, language_code: Optional[str] = None) -> str:
|
795
|
+
"""多语言月份格式 (v1.0.8)
|
796
|
+
|
797
|
+
Args:
|
798
|
+
short: 是否使用短名称
|
799
|
+
language_code: 语言代码,None时使用全局设置
|
800
|
+
|
801
|
+
Returns:
|
802
|
+
本地化的月份名称
|
803
|
+
"""
|
804
|
+
return Language.get_month_name(self.month, short, language_code)
|
805
|
+
|
806
|
+
def format_quarter_localized(self, short: bool = False, language_code: Optional[str] = None) -> str:
|
807
|
+
"""多语言季度格式 (v1.0.8)
|
808
|
+
|
809
|
+
Args:
|
810
|
+
short: 是否使用短名称
|
811
|
+
language_code: 语言代码,None时使用全局设置
|
812
|
+
|
813
|
+
Returns:
|
814
|
+
本地化的季度名称
|
815
|
+
"""
|
816
|
+
quarter = self.get_quarter()
|
817
|
+
return Language.get_quarter_name(quarter, short, language_code)
|
818
|
+
|
819
|
+
def format_relative_localized(self, reference_date: Optional['Date'] = None,
|
820
|
+
language_code: Optional[str] = None) -> str:
|
821
|
+
"""多语言相对时间格式 (v1.0.8)
|
822
|
+
|
823
|
+
Args:
|
824
|
+
reference_date: 参考日期,None时使用今天
|
825
|
+
language_code: 语言代码,None时使用全局设置
|
826
|
+
|
827
|
+
Returns:
|
828
|
+
本地化的相对时间描述
|
829
|
+
|
830
|
+
Example:
|
831
|
+
>>> Date.set_language('en_US')
|
832
|
+
>>> today = Date.today()
|
833
|
+
>>> tomorrow = today.add_days(1)
|
834
|
+
>>> print(tomorrow.format_relative_localized()) # tomorrow
|
835
|
+
"""
|
836
|
+
if reference_date is None:
|
837
|
+
reference_date = Date.today()
|
838
|
+
|
839
|
+
diff_days = reference_date.calculate_difference_days(self)
|
840
|
+
|
841
|
+
if diff_days == 0:
|
842
|
+
return Language.format_relative_time('today', language_code=language_code)
|
843
|
+
elif diff_days == 1:
|
844
|
+
return Language.format_relative_time('tomorrow', language_code=language_code)
|
845
|
+
elif diff_days == -1:
|
846
|
+
return Language.format_relative_time('yesterday', language_code=language_code)
|
847
|
+
elif diff_days > 0:
|
848
|
+
if diff_days <= 6:
|
849
|
+
return Language.format_relative_time('days_later', diff_days, language_code)
|
850
|
+
elif diff_days <= 28:
|
851
|
+
weeks = diff_days // 7
|
852
|
+
return Language.format_relative_time('weeks_later', weeks, language_code)
|
853
|
+
elif diff_days <= 365:
|
854
|
+
months = diff_days // 30
|
855
|
+
return Language.format_relative_time('months_later', months, language_code)
|
856
|
+
else:
|
857
|
+
years = diff_days // 365
|
858
|
+
return Language.format_relative_time('years_later', years, language_code)
|
859
|
+
else:
|
860
|
+
abs_days = abs(diff_days)
|
861
|
+
if abs_days <= 6:
|
862
|
+
return Language.format_relative_time('days_ago', abs_days, language_code)
|
863
|
+
elif abs_days <= 28:
|
864
|
+
weeks = abs_days // 7
|
865
|
+
return Language.format_relative_time('weeks_ago', weeks, language_code)
|
866
|
+
elif abs_days <= 365:
|
867
|
+
months = abs_days // 30
|
868
|
+
return Language.format_relative_time('months_ago', months, language_code)
|
869
|
+
else:
|
870
|
+
years = abs_days // 365
|
871
|
+
return Language.format_relative_time('years_ago', years, language_code)
|
872
|
+
|
873
|
+
def format_lunar(self, include_year: bool = True, include_zodiac: bool = False,
|
874
|
+
language_code: Optional[str] = None) -> str:
|
875
|
+
"""农历格式化 (v1.0.8)
|
876
|
+
|
877
|
+
Args:
|
878
|
+
include_year: 是否包含年份
|
879
|
+
include_zodiac: 是否包含生肖
|
880
|
+
language_code: 语言代码,None时使用全局设置
|
881
|
+
|
882
|
+
Returns:
|
883
|
+
农历日期字符串
|
884
|
+
|
885
|
+
Example:
|
886
|
+
>>> date = Date('20250415')
|
887
|
+
>>> print(date.format_lunar()) # 农历2025年三月十八
|
888
|
+
>>> print(date.format_lunar(include_zodiac=True)) # 乙巳(蛇)年三月十八
|
889
|
+
"""
|
890
|
+
lunar = self.to_lunar()
|
891
|
+
return lunar.format_chinese(include_year, include_zodiac)
|
892
|
+
|
893
|
+
def format_lunar_compact(self) -> str:
|
894
|
+
"""农历紧凑格式 (v1.0.8)
|
895
|
+
|
896
|
+
Returns:
|
897
|
+
农历紧凑格式字符串
|
898
|
+
|
899
|
+
Example:
|
900
|
+
>>> date = Date('20250415')
|
901
|
+
>>> print(date.format_lunar_compact()) # 20250318
|
902
|
+
"""
|
903
|
+
lunar = self.to_lunar()
|
904
|
+
return lunar.format_compact()
|
905
|
+
|
458
906
|
# =============================================
|
459
907
|
# get_* 系列:获取方法
|
460
908
|
# =============================================
|
@@ -582,18 +1030,53 @@ class Date:
|
|
582
1030
|
return (self.month in quarter_end_months and
|
583
1031
|
self.day == quarter_end_months[self.month])
|
584
1032
|
|
1033
|
+
def is_lunar_new_year(self) -> bool:
|
1034
|
+
"""是否为农历新年 (v1.0.8)
|
1035
|
+
|
1036
|
+
Returns:
|
1037
|
+
是否为农历正月初一
|
1038
|
+
"""
|
1039
|
+
lunar = self.to_lunar()
|
1040
|
+
return lunar.month == 1 and lunar.day == 1 and not lunar.is_leap
|
1041
|
+
|
1042
|
+
def is_lunar_month_start(self) -> bool:
|
1043
|
+
"""是否为农历月初 (v1.0.8)
|
1044
|
+
|
1045
|
+
Returns:
|
1046
|
+
是否为农历月初一
|
1047
|
+
"""
|
1048
|
+
lunar = self.to_lunar()
|
1049
|
+
return lunar.day == 1
|
1050
|
+
|
1051
|
+
def is_lunar_month_mid(self) -> bool:
|
1052
|
+
"""是否为农历月中 (v1.0.8)
|
1053
|
+
|
1054
|
+
Returns:
|
1055
|
+
是否为农历十五
|
1056
|
+
"""
|
1057
|
+
lunar = self.to_lunar()
|
1058
|
+
return lunar.day == 15
|
1059
|
+
|
1060
|
+
def is_lunar_leap_month(self) -> bool:
|
1061
|
+
"""是否在农历闰月 (v1.0.8)
|
1062
|
+
|
1063
|
+
Returns:
|
1064
|
+
是否为农历闰月
|
1065
|
+
"""
|
1066
|
+
lunar = self.to_lunar()
|
1067
|
+
return lunar.is_leap
|
1068
|
+
|
585
1069
|
def is_holiday(self, country: str = 'CN') -> bool:
|
586
|
-
"""
|
1070
|
+
"""是否为节假日(增强版实现)
|
587
1071
|
|
588
1072
|
Args:
|
589
|
-
country: 国家代码 ('CN', 'US', 'UK' 等)
|
1073
|
+
country: 国家代码 ('CN', 'US', 'UK', 'JP' 等)
|
590
1074
|
|
591
1075
|
Returns:
|
592
1076
|
是否为节假日
|
593
1077
|
|
594
1078
|
Note:
|
595
|
-
|
596
|
-
实际应用中可能需要更完整的节假日数据库
|
1079
|
+
支持多国节假日,包含农历节日计算
|
597
1080
|
"""
|
598
1081
|
if country == 'CN':
|
599
1082
|
# 中国固定节假日
|
@@ -603,20 +1086,93 @@ class Date:
|
|
603
1086
|
(10, 1), # 国庆节
|
604
1087
|
(10, 2), # 国庆节
|
605
1088
|
(10, 3), # 国庆节
|
1089
|
+
(12, 13), # 国家公祭日
|
606
1090
|
]
|
607
|
-
|
1091
|
+
|
1092
|
+
# 检查固定节假日
|
1093
|
+
if (self.month, self.day) in fixed_holidays:
|
1094
|
+
return True
|
1095
|
+
|
1096
|
+
# 特殊节日计算
|
1097
|
+
# 春节:农历正月初一(简化版本,实际需要农历计算)
|
1098
|
+
# 清明节:4月4日或5日(简化)
|
1099
|
+
if self.month == 4 and self.day in [4, 5]:
|
1100
|
+
return True
|
1101
|
+
|
1102
|
+
# 端午节、中秋节等需要农历计算,这里提供扩展接口
|
1103
|
+
return self._check_lunar_holidays()
|
1104
|
+
|
608
1105
|
elif country == 'US':
|
609
|
-
#
|
1106
|
+
# 美国节假日
|
610
1107
|
fixed_holidays = [
|
611
1108
|
(1, 1), # New Year's Day
|
612
1109
|
(7, 4), # Independence Day
|
613
1110
|
(12, 25), # Christmas Day
|
1111
|
+
(11, 11), # Veterans Day
|
1112
|
+
]
|
1113
|
+
|
1114
|
+
if (self.month, self.day) in fixed_holidays:
|
1115
|
+
return True
|
1116
|
+
|
1117
|
+
# 感恩节:11月第四个星期四
|
1118
|
+
if self.month == 11:
|
1119
|
+
return self._is_thanksgiving()
|
1120
|
+
|
1121
|
+
elif country == 'JP':
|
1122
|
+
# 日本节假日
|
1123
|
+
fixed_holidays = [
|
1124
|
+
(1, 1), # 元日
|
1125
|
+
(2, 11), # 建国記念の日
|
1126
|
+
(4, 29), # 昭和の日
|
1127
|
+
(5, 3), # 憲法記念日
|
1128
|
+
(5, 4), # みどりの日
|
1129
|
+
(5, 5), # こどもの日
|
1130
|
+
(11, 3), # 文化の日
|
1131
|
+
(11, 23), # 勤労感謝の日
|
1132
|
+
(12, 23), # 天皇誕生日
|
614
1133
|
]
|
615
1134
|
return (self.month, self.day) in fixed_holidays
|
1135
|
+
|
1136
|
+
elif country == 'UK':
|
1137
|
+
# 英国节假日
|
1138
|
+
fixed_holidays = [
|
1139
|
+
(1, 1), # New Year's Day
|
1140
|
+
(12, 25), # Christmas Day
|
1141
|
+
(12, 26), # Boxing Day
|
1142
|
+
]
|
1143
|
+
return (self.month, self.day) in fixed_holidays
|
1144
|
+
|
616
1145
|
else:
|
617
1146
|
# 未知国家,返回False
|
618
1147
|
return False
|
619
1148
|
|
1149
|
+
def _check_lunar_holidays(self) -> bool:
|
1150
|
+
"""检查农历节假日(扩展接口)
|
1151
|
+
|
1152
|
+
Note:
|
1153
|
+
这是一个扩展接口,实际项目中可以集成农历库
|
1154
|
+
如 `lunardate` 或 `zhdate` 等第三方库
|
1155
|
+
"""
|
1156
|
+
# 这里可以扩展农历节日计算
|
1157
|
+
# 目前返回 False,保持轻量级
|
1158
|
+
return False
|
1159
|
+
|
1160
|
+
def _is_thanksgiving(self) -> bool:
|
1161
|
+
"""判断是否为美国感恩节(11月第四个星期四)"""
|
1162
|
+
if self.month != 11:
|
1163
|
+
return False
|
1164
|
+
|
1165
|
+
# 找到11月第一天是星期几
|
1166
|
+
first_day = Date(self.year, 11, 1)
|
1167
|
+
first_weekday = first_day.get_weekday()
|
1168
|
+
|
1169
|
+
# 计算第四个星期四的日期
|
1170
|
+
# 星期四是weekday=3
|
1171
|
+
days_to_first_thursday = (3 - first_weekday) % 7
|
1172
|
+
fourth_thursday = 1 + days_to_first_thursday + 21 # 第四个星期四
|
1173
|
+
|
1174
|
+
return self.day == fourth_thursday
|
1175
|
+
|
620
1176
|
# =============================================
|
621
1177
|
# add_*/subtract_* 系列:运算方法
|
622
1178
|
# =============================================
|
@@ -705,6 +1261,99 @@ class Date:
|
|
705
1261
|
"""计算从起始日期过了多少天"""
|
706
1262
|
return start_date.calculate_difference_days(self)
|
707
1263
|
|
1264
|
+
# =============================================
|
1265
|
+
# 批量处理方法
|
1266
|
+
# =============================================
|
1267
|
+
|
1268
|
+
@classmethod
|
1269
|
+
def batch_create(cls, date_strings: List[str]) -> List['Date']:
|
1270
|
+
"""批量创建Date对象
|
1271
|
+
|
1272
|
+
Args:
|
1273
|
+
date_strings: 日期字符串列表
|
1274
|
+
|
1275
|
+
Returns:
|
1276
|
+
Date对象列表
|
1277
|
+
|
1278
|
+
Raises:
|
1279
|
+
InvalidDateFormatError: 如果某个字符串格式无效
|
1280
|
+
"""
|
1281
|
+
result = []
|
1282
|
+
for date_str in date_strings:
|
1283
|
+
try:
|
1284
|
+
result.append(cls(date_str))
|
1285
|
+
except (InvalidDateFormatError, InvalidDateValueError) as e:
|
1286
|
+
cls._logger.error(f"批量创建失败: {date_str} - {e}")
|
1287
|
+
raise
|
1288
|
+
return result
|
1289
|
+
|
1290
|
+
@classmethod
|
1291
|
+
def batch_format(cls, dates: List['Date'], format_type: str = 'iso') -> List[str]:
|
1292
|
+
"""批量格式化日期
|
1293
|
+
|
1294
|
+
Args:
|
1295
|
+
dates: Date对象列表
|
1296
|
+
format_type: 格式类型 ('iso', 'chinese', 'compact' 等)
|
1297
|
+
|
1298
|
+
Returns:
|
1299
|
+
格式化后的字符串列表
|
1300
|
+
"""
|
1301
|
+
format_methods = {
|
1302
|
+
'iso': lambda d: d.format_iso(),
|
1303
|
+
'chinese': lambda d: d.format_chinese(),
|
1304
|
+
'compact': lambda d: d.format_compact(),
|
1305
|
+
'slash': lambda d: d.format_slash(),
|
1306
|
+
'dot': lambda d: d.format_dot(),
|
1307
|
+
'default': lambda d: d.format_default()
|
1308
|
+
}
|
1309
|
+
|
1310
|
+
format_func = format_methods.get(format_type, lambda d: d.format_default())
|
1311
|
+
return [format_func(date) for date in dates]
|
1312
|
+
|
1313
|
+
@classmethod
|
1314
|
+
def batch_add_days(cls, dates: List['Date'], days: int) -> List['Date']:
|
1315
|
+
"""批量添加天数
|
1316
|
+
|
1317
|
+
Args:
|
1318
|
+
dates: Date对象列表
|
1319
|
+
days: 要添加的天数
|
1320
|
+
|
1321
|
+
Returns:
|
1322
|
+
新的Date对象列表
|
1323
|
+
"""
|
1324
|
+
return [date.add_days(days) for date in dates]
|
1325
|
+
|
1326
|
+
def apply_business_rule(self, rule: str, **kwargs) -> 'Date':
|
1327
|
+
"""应用业务规则
|
1328
|
+
|
1329
|
+
Args:
|
1330
|
+
rule: 规则名称
|
1331
|
+
- 'month_end': 移动到月末
|
1332
|
+
- 'quarter_end': 移动到季度末
|
1333
|
+
- 'next_business_day': 移动到下一个工作日
|
1334
|
+
- 'prev_business_day': 移动到上一个工作日
|
1335
|
+
**kwargs: 规则参数
|
1336
|
+
|
1337
|
+
Returns:
|
1338
|
+
应用规则后的新Date对象
|
1339
|
+
"""
|
1340
|
+
if rule == 'month_end':
|
1341
|
+
return self.get_month_end()
|
1342
|
+
elif rule == 'quarter_end':
|
1343
|
+
return self.get_quarter_end()
|
1344
|
+
elif rule == 'next_business_day':
|
1345
|
+
current = self
|
1346
|
+
while not current.is_business_day():
|
1347
|
+
current = current.add_days(1)
|
1348
|
+
return current
|
1349
|
+
elif rule == 'prev_business_day':
|
1350
|
+
current = self
|
1351
|
+
while not current.is_business_day():
|
1352
|
+
current = current.subtract_days(1)
|
1353
|
+
return current
|
1354
|
+
else:
|
1355
|
+
raise ValueError(f"未知的业务规则: {rule}")
|
1356
|
+
|
708
1357
|
# =============================================
|
709
1358
|
# 配置和日志方法
|
710
1359
|
# =============================================
|
@@ -750,6 +1399,58 @@ class Date:
|
|
750
1399
|
def __hash__(self) -> int:
|
751
1400
|
return hash(self.to_tuple())
|
752
1401
|
|
1402
|
+
def compare_lunar(self, other: 'Date') -> int:
|
1403
|
+
"""农历日期比较 (v1.0.8)
|
1404
|
+
|
1405
|
+
Args:
|
1406
|
+
other: 另一个Date对象
|
1407
|
+
|
1408
|
+
Returns:
|
1409
|
+
-1: self < other, 0: self == other, 1: self > other
|
1410
|
+
|
1411
|
+
Example:
|
1412
|
+
>>> date1 = Date.from_lunar(2025, 1, 1) # 农历正月初一
|
1413
|
+
>>> date2 = Date.from_lunar(2025, 1, 15) # 农历正月十五
|
1414
|
+
>>> print(date1.compare_lunar(date2)) # -1
|
1415
|
+
"""
|
1416
|
+
lunar_self = self.to_lunar()
|
1417
|
+
lunar_other = other.to_lunar()
|
1418
|
+
|
1419
|
+
if lunar_self < lunar_other:
|
1420
|
+
return -1
|
1421
|
+
elif lunar_self > lunar_other:
|
1422
|
+
return 1
|
1423
|
+
else:
|
1424
|
+
return 0
|
1425
|
+
|
1426
|
+
def is_same_lunar_month(self, other: 'Date') -> bool:
|
1427
|
+
"""是否同一农历月份 (v1.0.8)
|
1428
|
+
|
1429
|
+
Args:
|
1430
|
+
other: 另一个Date对象
|
1431
|
+
|
1432
|
+
Returns:
|
1433
|
+
是否为同一农历月份
|
1434
|
+
"""
|
1435
|
+
lunar_self = self.to_lunar()
|
1436
|
+
lunar_other = other.to_lunar()
|
1437
|
+
return (lunar_self.year == lunar_other.year and
|
1438
|
+
lunar_self.month == lunar_other.month and
|
1439
|
+
lunar_self.is_leap == lunar_other.is_leap)
|
1440
|
+
|
1441
|
+
def is_same_lunar_day(self, other: 'Date') -> bool:
|
1442
|
+
"""是否同一农历日期 (v1.0.8)
|
1443
|
+
|
1444
|
+
Args:
|
1445
|
+
other: 另一个Date对象
|
1446
|
+
|
1447
|
+
Returns:
|
1448
|
+
是否为同一农历日期
|
1449
|
+
"""
|
1450
|
+
lunar_self = self.to_lunar()
|
1451
|
+
lunar_other = other.to_lunar()
|
1452
|
+
return lunar_self == lunar_other
|
1453
|
+
|
753
1454
|
# =============================================
|
754
1455
|
# 向后兼容的旧API
|
755
1456
|
# =============================================
|