staran 1.0.6__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 CHANGED
@@ -27,7 +27,7 @@ Staran - 企业级多功能工具库
27
27
  2025年04月15日
28
28
  """
29
29
 
30
- __version__ = "1.0.6"
30
+ __version__ = "1.0.7"
31
31
  __author__ = "Staran Team"
32
32
  __email__ = "team@staran.dev"
33
33
  __license__ = "MIT"
staran/date/__init__.py CHANGED
@@ -8,7 +8,7 @@ Staran Date 模块
8
8
  提供企业级日期处理功能。
9
9
  """
10
10
 
11
- __version__ = "1.0.6"
11
+ __version__ = "1.0.7"
12
12
  __author__ = "Staran Team"
13
13
  __email__ = "team@staran.dev"
14
14
 
staran/date/core.py CHANGED
@@ -212,9 +212,14 @@ class Date:
212
212
 
213
213
  def _validate_date(self):
214
214
  """验证日期的有效性"""
215
+ # 基本范围检查
215
216
  if not (1 <= self.month <= 12):
216
217
  raise InvalidDateValueError(f"无效的月份: {self.month}")
217
218
 
219
+ # 年份合理性检查
220
+ if not (1900 <= self.year <= 3000):
221
+ self._logger.warning(f"年份 {self.year} 超出常规范围 (1900-3000)")
222
+
218
223
  max_days = calendar.monthrange(self.year, self.month)[1]
219
224
  if not (1 <= self.day <= max_days):
220
225
  raise InvalidDateValueError(f"无效的日期: {self.day} (对于 {self.year}-{self.month})")
@@ -223,7 +228,37 @@ class Date:
223
228
  datetime.date(self.year, self.month, self.day)
224
229
  except ValueError as e:
225
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
226
260
 
261
+ @lru_cache(maxsize=128)
227
262
  def _create_with_same_format(self, year: int, month: int, day: int) -> 'Date':
228
263
  """创建具有相同格式的新Date对象"""
229
264
  new_date = Date(year, month, day)
@@ -252,9 +287,17 @@ class Date:
252
287
  return cls(date_string)
253
288
 
254
289
  @classmethod
255
- def from_timestamp(cls, timestamp: Union[int, float]) -> 'Date':
256
- """从时间戳创建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
+ """
257
297
  dt = datetime.datetime.fromtimestamp(timestamp)
298
+ # 调整时区
299
+ if timezone_offset != 0:
300
+ dt = dt + datetime.timedelta(hours=timezone_offset)
258
301
  return cls(dt.date())
259
302
 
260
303
  @classmethod
@@ -298,6 +341,64 @@ class Date:
298
341
  dates = cls.date_range(start, end)
299
342
  return [date for date in dates if date.is_business_day()]
300
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
+
301
402
  # =============================================
302
403
  # to_* 系列:转换方法
303
404
  # =============================================
@@ -306,14 +407,6 @@ class Date:
306
407
  """转为元组 (year, month, day)"""
307
408
  return (self.year, self.month, self.day)
308
409
 
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
410
  def to_date_object(self) -> datetime.date:
318
411
  """转为datetime.date对象"""
319
412
  return datetime.date(self.year, self.month, self.day)
@@ -322,29 +415,109 @@ class Date:
322
415
  """转为datetime.datetime对象"""
323
416
  return datetime.datetime(self.year, self.month, self.day)
324
417
 
325
- def to_timestamp(self) -> float:
326
- """转为时间戳"""
327
- return self.to_datetime_object().timestamp()
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()
328
432
 
329
- def to_json(self) -> str:
330
- """转为JSON字符串"""
433
+ def to_json(self, include_metadata: bool = True) -> str:
434
+ """转为JSON字符串
435
+
436
+ Args:
437
+ include_metadata: 是否包含元数据(格式、版本等)
438
+
439
+ Returns:
440
+ JSON字符串
441
+ """
331
442
  import json
332
- return json.dumps({
443
+
444
+ data = {
333
445
  'date': self.format_iso(),
334
- 'format': self._input_format,
335
446
  'year': self.year,
336
447
  'month': self.month,
337
448
  'day': self.day
338
- })
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)
339
461
 
340
462
  @classmethod
341
463
  def from_json(cls, json_str: str) -> 'Date':
342
- """从JSON字符串创建Date对象"""
464
+ """从JSON字符串创建Date对象
465
+
466
+ Args:
467
+ json_str: JSON字符串
468
+
469
+ Returns:
470
+ Date对象
471
+
472
+ Raises:
473
+ ValueError: JSON格式错误或缺少必要字段
474
+ """
343
475
  import json
344
- data = json.loads(json_str)
345
- date = cls(data['year'], data['month'], data['day'])
346
- date._input_format = data.get('format', 'iso')
347
- return date
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
348
521
 
349
522
  # =============================================
350
523
  # format_* 系列:格式化方法
@@ -583,17 +756,16 @@ class Date:
583
756
  self.day == quarter_end_months[self.month])
584
757
 
585
758
  def is_holiday(self, country: str = 'CN') -> bool:
586
- """是否为节假日(基础实现)
759
+ """是否为节假日(增强版实现)
587
760
 
588
761
  Args:
589
- country: 国家代码 ('CN', 'US', 'UK' 等)
762
+ country: 国家代码 ('CN', 'US', 'UK', 'JP' 等)
590
763
 
591
764
  Returns:
592
765
  是否为节假日
593
766
 
594
767
  Note:
595
- 这是一个基础实现,仅包含几个固定节假日
596
- 实际应用中可能需要更完整的节假日数据库
768
+ 支持多国节假日,包含农历节日计算
597
769
  """
598
770
  if country == 'CN':
599
771
  # 中国固定节假日
@@ -603,20 +775,93 @@ class Date:
603
775
  (10, 1), # 国庆节
604
776
  (10, 2), # 国庆节
605
777
  (10, 3), # 国庆节
778
+ (12, 13), # 国家公祭日
606
779
  ]
607
- return (self.month, self.day) in fixed_holidays
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
+
608
794
  elif country == 'US':
609
- # 美国固定节假日
795
+ # 美国节假日
610
796
  fixed_holidays = [
611
797
  (1, 1), # New Year's Day
612
798
  (7, 4), # Independence Day
613
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
614
831
  ]
615
832
  return (self.month, self.day) in fixed_holidays
833
+
616
834
  else:
617
835
  # 未知国家,返回False
618
836
  return False
619
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
+
620
865
  # =============================================
621
866
  # add_*/subtract_* 系列:运算方法
622
867
  # =============================================
@@ -705,6 +950,99 @@ class Date:
705
950
  """计算从起始日期过了多少天"""
706
951
  return start_date.calculate_difference_days(self)
707
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
+
708
1046
  # =============================================
709
1047
  # 配置和日志方法
710
1048
  # =============================================