staran 1.0.8__py3-none-any.whl → 1.0.10__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 +0 -62
- staran/date/__init__.py +72 -87
- staran/date/core/__init__.py +24 -0
- staran/date/{core.py → core/core.py} +1094 -9
- staran/date/examples/v1010_features_demo.py +376 -0
- staran/date/examples/v109_features_demo.py +302 -0
- staran/date/extensions/__init__.py +48 -0
- staran/date/extensions/expressions.py +554 -0
- staran/date/extensions/solar_terms.py +417 -0
- staran/date/extensions/timezone.py +263 -0
- staran/date/integrations/__init__.py +38 -0
- staran/date/integrations/api_server.py +754 -0
- staran/date/integrations/visualization.py +689 -0
- staran/date/tests/run_tests.py +77 -6
- staran/date/tests/test_v1010_features.py +495 -0
- staran/date/tests/test_v109_features.py +316 -0
- staran-1.0.10.dist-info/METADATA +240 -0
- staran-1.0.10.dist-info/RECORD +34 -0
- staran-1.0.10.dist-info/entry_points.txt +2 -0
- staran-1.0.8.dist-info/METADATA +0 -371
- staran-1.0.8.dist-info/RECORD +0 -21
- /staran/date/{i18n.py → core/i18n.py} +0 -0
- /staran/date/{lunar.py → core/lunar.py} +0 -0
- {staran-1.0.8.dist-info → staran-1.0.10.dist-info}/WHEEL +0 -0
- {staran-1.0.8.dist-info → staran-1.0.10.dist-info}/licenses/LICENSE +0 -0
- {staran-1.0.8.dist-info → staran-1.0.10.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,417 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
|
4
|
+
"""
|
5
|
+
Staran 二十四节气模块 v1.0.10
|
6
|
+
==========================
|
7
|
+
|
8
|
+
提供完整的二十四节气计算和查询功能。
|
9
|
+
|
10
|
+
主要功能:
|
11
|
+
- 节气日期计算
|
12
|
+
- 节气信息查询
|
13
|
+
- 节气与农历的关系
|
14
|
+
- 节气文化属性
|
15
|
+
"""
|
16
|
+
|
17
|
+
import datetime
|
18
|
+
import math
|
19
|
+
from typing import Dict, List, Tuple, Optional, Union
|
20
|
+
from dataclasses import dataclass
|
21
|
+
|
22
|
+
@dataclass
|
23
|
+
class SolarTerm:
|
24
|
+
"""节气信息类"""
|
25
|
+
name: str # 节气名称
|
26
|
+
index: int # 节气序号 (0-23)
|
27
|
+
date: datetime.date # 节气日期
|
28
|
+
season: str # 所属季节
|
29
|
+
description: str # 节气描述
|
30
|
+
traditional_activities: List[str] # 传统活动
|
31
|
+
climate_features: str # 气候特征
|
32
|
+
agricultural_guidance: str # 农业指导
|
33
|
+
|
34
|
+
class SolarTerms:
|
35
|
+
"""二十四节气计算类"""
|
36
|
+
|
37
|
+
# 二十四节气名称和信息
|
38
|
+
SOLAR_TERMS_INFO = {
|
39
|
+
0: {
|
40
|
+
'name': '立春', 'season': '春季',
|
41
|
+
'description': '春季开始,万物复苏',
|
42
|
+
'activities': ['迎春', '打春牛', '咬春'],
|
43
|
+
'climate': '气温回升,春意萌动',
|
44
|
+
'agriculture': '准备春耕,育苗播种'
|
45
|
+
},
|
46
|
+
1: {
|
47
|
+
'name': '雨水', 'season': '春季',
|
48
|
+
'description': '雨量增多,气温回升',
|
49
|
+
'activities': ['拉保保', '占稻色'],
|
50
|
+
'climate': '降雨增多,冰雪融化',
|
51
|
+
'agriculture': '春灌开始,作物返青'
|
52
|
+
},
|
53
|
+
2: {
|
54
|
+
'name': '惊蛰', 'season': '春季',
|
55
|
+
'description': '春雷初响,蛰虫苏醒',
|
56
|
+
'activities': ['祭白虎', '打小人'],
|
57
|
+
'climate': '气温快速回升,雷声频繁',
|
58
|
+
'agriculture': '春耕大忙,防治病虫害'
|
59
|
+
},
|
60
|
+
3: {
|
61
|
+
'name': '春分', 'season': '春季',
|
62
|
+
'description': '昼夜平分,春色正浓',
|
63
|
+
'activities': ['竖蛋', '吃春菜', '放风筝'],
|
64
|
+
'climate': '昼夜等长,气候温和',
|
65
|
+
'agriculture': '春播春种,田间管理'
|
66
|
+
},
|
67
|
+
4: {
|
68
|
+
'name': '清明', 'season': '春季',
|
69
|
+
'description': '天气清朗,草木繁茂',
|
70
|
+
'activities': ['扫墓', '踏青', '插柳'],
|
71
|
+
'climate': '天气晴朗,气温适宜',
|
72
|
+
'agriculture': '种瓜点豆,春茶采摘'
|
73
|
+
},
|
74
|
+
5: {
|
75
|
+
'name': '谷雨', 'season': '春季',
|
76
|
+
'description': '雨润谷物,春季结束',
|
77
|
+
'activities': ['喝谷雨茶', '赏牡丹'],
|
78
|
+
'climate': '雨量充沛,利于作物生长',
|
79
|
+
'agriculture': '春播结束,夏季作物管理'
|
80
|
+
},
|
81
|
+
6: {
|
82
|
+
'name': '立夏', 'season': '夏季',
|
83
|
+
'description': '夏季开始,万物茂盛',
|
84
|
+
'activities': ['迎夏', '尝新', '称人'],
|
85
|
+
'climate': '气温明显升高,雷雨增多',
|
86
|
+
'agriculture': '夏收夏种,防汛抗旱'
|
87
|
+
},
|
88
|
+
7: {
|
89
|
+
'name': '小满', 'season': '夏季',
|
90
|
+
'description': '夏熟作物籽粒渐满',
|
91
|
+
'activities': ['祭车神', '蚕神'],
|
92
|
+
'climate': '气温升高,降水增多',
|
93
|
+
'agriculture': '夏熟作物管理,防治病虫'
|
94
|
+
},
|
95
|
+
8: {
|
96
|
+
'name': '芒种', 'season': '夏季',
|
97
|
+
'description': '有芒作物成熟收获',
|
98
|
+
'activities': ['安苗', '打泥巴仗'],
|
99
|
+
'climate': '气温高,湿度大',
|
100
|
+
'agriculture': '夏收夏种,抢收抢种'
|
101
|
+
},
|
102
|
+
9: {
|
103
|
+
'name': '夏至', 'season': '夏季',
|
104
|
+
'description': '白昼最长,夏日极致',
|
105
|
+
'activities': ['祭神祀祖', '消夏避伏'],
|
106
|
+
'climate': '日照最长,气温最高',
|
107
|
+
'agriculture': '田间管理,防暑降温'
|
108
|
+
},
|
109
|
+
10: {
|
110
|
+
'name': '小暑', 'season': '夏季',
|
111
|
+
'description': '暑热初临,温度升高',
|
112
|
+
'activities': ['食新', '晒伏'],
|
113
|
+
'climate': '气温持续升高,进入伏天',
|
114
|
+
'agriculture': '防暑抗旱,中耕除草'
|
115
|
+
},
|
116
|
+
11: {
|
117
|
+
'name': '大暑', 'season': '夏季',
|
118
|
+
'description': '酷暑炎热,一年最热',
|
119
|
+
'activities': ['喝伏茶', '晒伏姜'],
|
120
|
+
'climate': '全年最热,多雷雨',
|
121
|
+
'agriculture': '防暑降温,抗旱保苗'
|
122
|
+
},
|
123
|
+
12: {
|
124
|
+
'name': '立秋', 'season': '秋季',
|
125
|
+
'description': '秋季开始,暑热渐消',
|
126
|
+
'activities': ['啃秋', '贴秋膘'],
|
127
|
+
'climate': '白天炎热,早晚凉爽',
|
128
|
+
'agriculture': '秋收准备,后期管理'
|
129
|
+
},
|
130
|
+
13: {
|
131
|
+
'name': '处暑', 'season': '秋季',
|
132
|
+
'description': '暑热结束,秋凉渐至',
|
133
|
+
'activities': ['放河灯', '开渔节'],
|
134
|
+
'climate': '昼夜温差大,秋高气爽',
|
135
|
+
'agriculture': '秋收开始,防旱防涝'
|
136
|
+
},
|
137
|
+
14: {
|
138
|
+
'name': '白露', 'season': '秋季',
|
139
|
+
'description': '露水凝结,秋意渐浓',
|
140
|
+
'activities': ['收清露', '祭禹王'],
|
141
|
+
'climate': '昼夜温差增大,露水出现',
|
142
|
+
'agriculture': '秋收繁忙,防范霜冻'
|
143
|
+
},
|
144
|
+
15: {
|
145
|
+
'name': '秋分', 'season': '秋季',
|
146
|
+
'description': '昼夜平分,秋色满园',
|
147
|
+
'activities': ['竖蛋', '吃秋菜', '送秋牛'],
|
148
|
+
'climate': '昼夜等长,气候凉爽',
|
149
|
+
'agriculture': '秋收秋种,收获季节'
|
150
|
+
},
|
151
|
+
16: {
|
152
|
+
'name': '寒露', 'season': '秋季',
|
153
|
+
'description': '露水转寒,深秋来临',
|
154
|
+
'activities': ['登高', '赏菊'],
|
155
|
+
'climate': '气温下降,露水较凉',
|
156
|
+
'agriculture': '秋收扫尾,播种冬作物'
|
157
|
+
},
|
158
|
+
17: {
|
159
|
+
'name': '霜降', 'season': '秋季',
|
160
|
+
'description': '初霜降临,秋季结束',
|
161
|
+
'activities': ['赏菊', '吃柿子'],
|
162
|
+
'climate': '气温骤降,出现初霜',
|
163
|
+
'agriculture': '防霜保暖,冬作物管理'
|
164
|
+
},
|
165
|
+
18: {
|
166
|
+
'name': '立冬', 'season': '冬季',
|
167
|
+
'description': '冬季开始,万物收藏',
|
168
|
+
'activities': ['迎冬', '补冬'],
|
169
|
+
'climate': '气温明显下降,进入冬季',
|
170
|
+
'agriculture': '冬季准备,农作物收藏'
|
171
|
+
},
|
172
|
+
19: {
|
173
|
+
'name': '小雪', 'season': '冬季',
|
174
|
+
'description': '初雪飞舞,寒意渐浓',
|
175
|
+
'activities': ['腌腊肉', '品茗'],
|
176
|
+
'climate': '气温持续下降,开始降雪',
|
177
|
+
'agriculture': '御寒保温,储备过冬物资'
|
178
|
+
},
|
179
|
+
20: {
|
180
|
+
'name': '大雪', 'season': '冬季',
|
181
|
+
'description': '雪花纷飞,天地苍茫',
|
182
|
+
'activities': ['腌肉', '观雪'],
|
183
|
+
'climate': '大雪纷飞,气温骤降',
|
184
|
+
'agriculture': '防寒保暖,牲畜越冬管理'
|
185
|
+
},
|
186
|
+
21: {
|
187
|
+
'name': '冬至', 'season': '冬季',
|
188
|
+
'description': '白昼最短,冬日极致',
|
189
|
+
'activities': ['吃饺子', '祭祖'],
|
190
|
+
'climate': '日照最短,寒冷达到顶峰',
|
191
|
+
'agriculture': '农事休闲,计划来年'
|
192
|
+
},
|
193
|
+
22: {
|
194
|
+
'name': '小寒', 'season': '冬季',
|
195
|
+
'description': '寒冷加剧,三九时节',
|
196
|
+
'activities': ['吃腊八粥', '写春联'],
|
197
|
+
'climate': '严寒时期,气温极低',
|
198
|
+
'agriculture': '防寒保暖,准备春节'
|
199
|
+
},
|
200
|
+
23: {
|
201
|
+
'name': '大寒', 'season': '冬季',
|
202
|
+
'description': '严寒酷冷,冬季结束',
|
203
|
+
'activities': ['除尘', '贴年画'],
|
204
|
+
'climate': '全年最冷,准备迎春',
|
205
|
+
'agriculture': '农事较少,准备春耕'
|
206
|
+
}
|
207
|
+
}
|
208
|
+
|
209
|
+
@classmethod
|
210
|
+
def calculate_solar_term_date(cls, year: int, term_index: int) -> datetime.date:
|
211
|
+
"""
|
212
|
+
计算指定年份的节气日期
|
213
|
+
使用天文算法计算精确的节气时间
|
214
|
+
"""
|
215
|
+
# 基础数据:2000年各节气的平均日期
|
216
|
+
base_dates = [
|
217
|
+
(2, 4), (2, 19), (3, 6), (3, 21), (4, 5), (4, 20), # 立春到谷雨
|
218
|
+
(5, 6), (5, 21), (6, 6), (6, 21), (7, 7), (7, 23), # 立夏到大暑
|
219
|
+
(8, 8), (8, 23), (9, 8), (9, 23), (10, 8), (10, 23), # 立秋到霜降
|
220
|
+
(11, 7), (11, 22), (12, 7), (12, 22), (1, 6), (1, 20) # 立冬到大寒
|
221
|
+
]
|
222
|
+
|
223
|
+
base_month, base_day = base_dates[term_index]
|
224
|
+
|
225
|
+
# 年份修正(简化算法)
|
226
|
+
year_diff = year - 2000
|
227
|
+
|
228
|
+
# 节气时间修正公式(简化版)
|
229
|
+
# 实际的节气计算需要复杂的天文算法
|
230
|
+
correction = year_diff * 0.2422 # 每年约0.2422天的偏移
|
231
|
+
|
232
|
+
# 特殊年份修正
|
233
|
+
if year % 4 == 0 and year % 100 != 0 or year % 400 == 0:
|
234
|
+
# 闰年修正
|
235
|
+
if term_index >= 4: # 清明之后
|
236
|
+
correction -= 1
|
237
|
+
|
238
|
+
# 计算实际日期
|
239
|
+
total_days = base_day + correction
|
240
|
+
actual_day = int(total_days)
|
241
|
+
|
242
|
+
# 处理月份边界
|
243
|
+
actual_month = base_month
|
244
|
+
if term_index >= 22: # 小寒、大寒在下一年
|
245
|
+
actual_year = year + 1
|
246
|
+
else:
|
247
|
+
actual_year = year
|
248
|
+
|
249
|
+
# 调整超出月份天数的情况
|
250
|
+
import calendar
|
251
|
+
max_day = calendar.monthrange(actual_year, actual_month)[1]
|
252
|
+
if actual_day > max_day:
|
253
|
+
actual_day -= max_day
|
254
|
+
actual_month += 1
|
255
|
+
if actual_month > 12:
|
256
|
+
actual_month = 1
|
257
|
+
actual_year += 1
|
258
|
+
|
259
|
+
try:
|
260
|
+
return datetime.date(actual_year, actual_month, actual_day)
|
261
|
+
except ValueError:
|
262
|
+
# 容错处理
|
263
|
+
return datetime.date(actual_year, actual_month, min(actual_day, max_day))
|
264
|
+
|
265
|
+
@classmethod
|
266
|
+
def get_solar_term(cls, year: int, term_index: int) -> SolarTerm:
|
267
|
+
"""获取指定年份的节气信息"""
|
268
|
+
if not (0 <= term_index <= 23):
|
269
|
+
raise ValueError("节气序号必须在0-23之间")
|
270
|
+
|
271
|
+
info = cls.SOLAR_TERMS_INFO[term_index]
|
272
|
+
date = cls.calculate_solar_term_date(year, term_index)
|
273
|
+
|
274
|
+
return SolarTerm(
|
275
|
+
name=info['name'],
|
276
|
+
index=term_index,
|
277
|
+
date=date,
|
278
|
+
season=info['season'],
|
279
|
+
description=info['description'],
|
280
|
+
traditional_activities=info['activities'],
|
281
|
+
climate_features=info['climate'],
|
282
|
+
agricultural_guidance=info['agriculture']
|
283
|
+
)
|
284
|
+
|
285
|
+
@classmethod
|
286
|
+
def get_all_solar_terms(cls, year: int) -> List[SolarTerm]:
|
287
|
+
"""获取指定年份的所有节气"""
|
288
|
+
return [cls.get_solar_term(year, i) for i in range(24)]
|
289
|
+
|
290
|
+
@classmethod
|
291
|
+
def get_solar_terms_by_season(cls, year: int, season: str) -> List[SolarTerm]:
|
292
|
+
"""获取指定年份某季节的节气"""
|
293
|
+
season_map = {
|
294
|
+
'春季': [0, 1, 2, 3, 4, 5],
|
295
|
+
'夏季': [6, 7, 8, 9, 10, 11],
|
296
|
+
'秋季': [12, 13, 14, 15, 16, 17],
|
297
|
+
'冬季': [18, 19, 20, 21, 22, 23]
|
298
|
+
}
|
299
|
+
|
300
|
+
if season not in season_map:
|
301
|
+
raise ValueError("季节必须是:春季、夏季、秋季、冬季")
|
302
|
+
|
303
|
+
return [cls.get_solar_term(year, i) for i in season_map[season]]
|
304
|
+
|
305
|
+
@classmethod
|
306
|
+
def find_solar_term_by_date(cls, date: datetime.date) -> Optional[SolarTerm]:
|
307
|
+
"""根据日期查找最接近的节气"""
|
308
|
+
year = date.year
|
309
|
+
all_terms = cls.get_all_solar_terms(year)
|
310
|
+
|
311
|
+
# 找到最接近的节气
|
312
|
+
closest_term = None
|
313
|
+
min_diff = float('inf')
|
314
|
+
|
315
|
+
for term in all_terms:
|
316
|
+
diff = abs((date - term.date).days)
|
317
|
+
if diff < min_diff:
|
318
|
+
min_diff = diff
|
319
|
+
closest_term = term
|
320
|
+
|
321
|
+
# 如果差距超过7天,可能不是当前节气
|
322
|
+
if min_diff <= 7:
|
323
|
+
return closest_term
|
324
|
+
return None
|
325
|
+
|
326
|
+
@classmethod
|
327
|
+
def find_solar_term_by_name(cls, year: int, name: str) -> Optional[SolarTerm]:
|
328
|
+
"""根据节气名称查找节气"""
|
329
|
+
for i, info in cls.SOLAR_TERMS_INFO.items():
|
330
|
+
if info['name'] == name:
|
331
|
+
return cls.get_solar_term(year, i)
|
332
|
+
return None
|
333
|
+
|
334
|
+
@classmethod
|
335
|
+
def get_next_solar_term(cls, date: datetime.date) -> SolarTerm:
|
336
|
+
"""获取指定日期之后的下一个节气"""
|
337
|
+
year = date.year
|
338
|
+
all_terms = cls.get_all_solar_terms(year)
|
339
|
+
|
340
|
+
# 查找下一个节气
|
341
|
+
for term in all_terms:
|
342
|
+
if term.date > date:
|
343
|
+
return term
|
344
|
+
|
345
|
+
# 如果当年没有找到,查找下一年的第一个节气
|
346
|
+
return cls.get_solar_term(year + 1, 0)
|
347
|
+
|
348
|
+
@classmethod
|
349
|
+
def get_previous_solar_term(cls, date: datetime.date) -> SolarTerm:
|
350
|
+
"""获取指定日期之前的上一个节气"""
|
351
|
+
year = date.year
|
352
|
+
all_terms = cls.get_all_solar_terms(year)
|
353
|
+
|
354
|
+
# 倒序查找上一个节气
|
355
|
+
for term in reversed(all_terms):
|
356
|
+
if term.date < date:
|
357
|
+
return term
|
358
|
+
|
359
|
+
# 如果当年没有找到,查找上一年的最后一个节气
|
360
|
+
return cls.get_solar_term(year - 1, 23)
|
361
|
+
|
362
|
+
@classmethod
|
363
|
+
def get_solar_term_period(cls, date: datetime.date) -> Tuple[SolarTerm, SolarTerm]:
|
364
|
+
"""获取指定日期所在的节气期间(当前节气和下一个节气)"""
|
365
|
+
current = cls.get_previous_solar_term(date + datetime.timedelta(days=1))
|
366
|
+
next_term = cls.get_next_solar_term(date)
|
367
|
+
return current, next_term
|
368
|
+
|
369
|
+
@classmethod
|
370
|
+
def is_solar_term_date(cls, date: datetime.date) -> bool:
|
371
|
+
"""判断指定日期是否是节气日"""
|
372
|
+
year = date.year
|
373
|
+
all_terms = cls.get_all_solar_terms(year)
|
374
|
+
return any(term.date == date for term in all_terms)
|
375
|
+
|
376
|
+
@classmethod
|
377
|
+
def get_days_to_next_solar_term(cls, date: datetime.date) -> int:
|
378
|
+
"""计算到下一个节气的天数"""
|
379
|
+
next_term = cls.get_next_solar_term(date)
|
380
|
+
return (next_term.date - date).days
|
381
|
+
|
382
|
+
@classmethod
|
383
|
+
def get_solar_term_names(cls) -> List[str]:
|
384
|
+
"""获取所有节气名称"""
|
385
|
+
return [info['name'] for info in cls.SOLAR_TERMS_INFO.values()]
|
386
|
+
|
387
|
+
@classmethod
|
388
|
+
def format_solar_term_info(cls, solar_term: SolarTerm, detailed: bool = False) -> str:
|
389
|
+
"""格式化节气信息"""
|
390
|
+
if not detailed:
|
391
|
+
return f"{solar_term.name}({solar_term.date.strftime('%m月%d日')})"
|
392
|
+
|
393
|
+
return f"""
|
394
|
+
{solar_term.name} - {solar_term.season}
|
395
|
+
日期: {solar_term.date.strftime('%Y年%m月%d日')}
|
396
|
+
描述: {solar_term.description}
|
397
|
+
气候特征: {solar_term.climate_features}
|
398
|
+
传统活动: {', '.join(solar_term.traditional_activities)}
|
399
|
+
农业指导: {solar_term.agricultural_guidance}
|
400
|
+
""".strip()
|
401
|
+
|
402
|
+
# 便捷函数
|
403
|
+
def get_solar_term_by_date(date: datetime.date) -> Optional[SolarTerm]:
|
404
|
+
"""根据日期获取节气(便捷函数)"""
|
405
|
+
return SolarTerms.find_solar_term_by_date(date)
|
406
|
+
|
407
|
+
def get_all_solar_terms(year: int) -> List[SolarTerm]:
|
408
|
+
"""获取指定年份所有节气(便捷函数)"""
|
409
|
+
return SolarTerms.get_all_solar_terms(year)
|
410
|
+
|
411
|
+
def get_next_solar_term(date: datetime.date) -> SolarTerm:
|
412
|
+
"""获取下一个节气(便捷函数)"""
|
413
|
+
return SolarTerms.get_next_solar_term(date)
|
414
|
+
|
415
|
+
def is_solar_term_today() -> bool:
|
416
|
+
"""判断今天是否是节气日(便捷函数)"""
|
417
|
+
return SolarTerms.is_solar_term_date(datetime.date.today())
|
@@ -0,0 +1,263 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
|
4
|
+
"""
|
5
|
+
Staran 时区支持模块 v1.0.10
|
6
|
+
==========================
|
7
|
+
|
8
|
+
提供完整的时区转换和处理功能,支持全球主要时区。
|
9
|
+
|
10
|
+
主要功能:
|
11
|
+
- 时区信息管理
|
12
|
+
- 时区转换
|
13
|
+
- 夏令时支持
|
14
|
+
- 时区感知的日期操作
|
15
|
+
"""
|
16
|
+
|
17
|
+
import datetime
|
18
|
+
import time
|
19
|
+
from typing import Dict, List, Tuple, Optional, Union
|
20
|
+
from dataclasses import dataclass
|
21
|
+
|
22
|
+
@dataclass
|
23
|
+
class TimezoneInfo:
|
24
|
+
"""时区信息类"""
|
25
|
+
code: str # 时区代码,如 'UTC', 'UTC+8', 'EST'
|
26
|
+
name: str # 时区名称
|
27
|
+
offset: float # UTC偏移(小时)
|
28
|
+
description: str # 时区描述
|
29
|
+
country: str # 国家/地区
|
30
|
+
cities: List[str] # 主要城市
|
31
|
+
dst_offset: Optional[float] = None # 夏令时偏移
|
32
|
+
dst_start: Optional[str] = None # 夏令时开始时间
|
33
|
+
dst_end: Optional[str] = None # 夏令时结束时间
|
34
|
+
|
35
|
+
class Timezone:
|
36
|
+
"""时区管理类"""
|
37
|
+
|
38
|
+
# 全球主要时区数据
|
39
|
+
TIMEZONE_DATA = {
|
40
|
+
# 协调世界时
|
41
|
+
'UTC': TimezoneInfo('UTC', 'Coordinated Universal Time', 0,
|
42
|
+
'协调世界时', 'Global', ['Greenwich']),
|
43
|
+
|
44
|
+
# 亚洲时区
|
45
|
+
'UTC+8': TimezoneInfo('UTC+8', 'China Standard Time', 8,
|
46
|
+
'中国标准时间', 'China', ['Beijing', 'Shanghai', 'Hong Kong']),
|
47
|
+
'JST': TimezoneInfo('JST', 'Japan Standard Time', 9,
|
48
|
+
'日本标准时间', 'Japan', ['Tokyo', 'Osaka']),
|
49
|
+
'KST': TimezoneInfo('KST', 'Korea Standard Time', 9,
|
50
|
+
'韩国标准时间', 'Korea', ['Seoul']),
|
51
|
+
'IST': TimezoneInfo('IST', 'India Standard Time', 5.5,
|
52
|
+
'印度标准时间', 'India', ['New Delhi', 'Mumbai']),
|
53
|
+
|
54
|
+
# 欧洲时区
|
55
|
+
'CET': TimezoneInfo('CET', 'Central European Time', 1,
|
56
|
+
'中欧时间', 'Europe', ['Paris', 'Berlin', 'Rome'],
|
57
|
+
dst_offset=2, dst_start='3月最后一个周日', dst_end='10月最后一个周日'),
|
58
|
+
'GMT': TimezoneInfo('GMT', 'Greenwich Mean Time', 0,
|
59
|
+
'格林威治标准时间', 'UK', ['London'],
|
60
|
+
dst_offset=1, dst_start='3月最后一个周日', dst_end='10月最后一个周日'),
|
61
|
+
'EET': TimezoneInfo('EET', 'Eastern European Time', 2,
|
62
|
+
'东欧时间', 'Europe', ['Helsinki', 'Kiev'],
|
63
|
+
dst_offset=3, dst_start='3月最后一个周日', dst_end='10月最后一个周日'),
|
64
|
+
|
65
|
+
# 美洲时区
|
66
|
+
'EST': TimezoneInfo('EST', 'Eastern Standard Time', -5,
|
67
|
+
'美国东部标准时间', 'USA', ['New York', 'Washington'],
|
68
|
+
dst_offset=-4, dst_start='3月第二个周日', dst_end='11月第一个周日'),
|
69
|
+
'CST': TimezoneInfo('CST', 'Central Standard Time', -6,
|
70
|
+
'美国中部标准时间', 'USA', ['Chicago', 'Dallas'],
|
71
|
+
dst_offset=-5, dst_start='3月第二个周日', dst_end='11月第一个周日'),
|
72
|
+
'MST': TimezoneInfo('MST', 'Mountain Standard Time', -7,
|
73
|
+
'美国山地标准时间', 'USA', ['Denver', 'Phoenix'],
|
74
|
+
dst_offset=-6, dst_start='3月第二个周日', dst_end='11月第一个周日'),
|
75
|
+
'PST': TimezoneInfo('PST', 'Pacific Standard Time', -8,
|
76
|
+
'美国太平洋标准时间', 'USA', ['Los Angeles', 'San Francisco'],
|
77
|
+
dst_offset=-7, dst_start='3月第二个周日', dst_end='11月第一个周日'),
|
78
|
+
|
79
|
+
# 大洋洲时区
|
80
|
+
'AEST': TimezoneInfo('AEST', 'Australian Eastern Standard Time', 10,
|
81
|
+
'澳大利亚东部标准时间', 'Australia', ['Sydney', 'Melbourne'],
|
82
|
+
dst_offset=11, dst_start='10月第一个周日', dst_end='4月第一个周日'),
|
83
|
+
'AWST': TimezoneInfo('AWST', 'Australian Western Standard Time', 8,
|
84
|
+
'澳大利亚西部标准时间', 'Australia', ['Perth']),
|
85
|
+
'NZST': TimezoneInfo('NZST', 'New Zealand Standard Time', 12,
|
86
|
+
'新西兰标准时间', 'New Zealand', ['Auckland', 'Wellington'],
|
87
|
+
dst_offset=13, dst_start='9月最后一个周日', dst_end='4月第一个周日'),
|
88
|
+
}
|
89
|
+
|
90
|
+
@classmethod
|
91
|
+
def get_timezone_info(cls, timezone_code: str) -> Optional[TimezoneInfo]:
|
92
|
+
"""获取时区信息"""
|
93
|
+
return cls.TIMEZONE_DATA.get(timezone_code.upper())
|
94
|
+
|
95
|
+
@classmethod
|
96
|
+
def list_timezones(cls) -> List[str]:
|
97
|
+
"""列出所有支持的时区"""
|
98
|
+
return list(cls.TIMEZONE_DATA.keys())
|
99
|
+
|
100
|
+
@classmethod
|
101
|
+
def find_timezone_by_city(cls, city: str) -> List[str]:
|
102
|
+
"""根据城市查找时区"""
|
103
|
+
result = []
|
104
|
+
city_lower = city.lower()
|
105
|
+
for code, info in cls.TIMEZONE_DATA.items():
|
106
|
+
if any(city_lower in c.lower() for c in info.cities):
|
107
|
+
result.append(code)
|
108
|
+
return result
|
109
|
+
|
110
|
+
@classmethod
|
111
|
+
def find_timezone_by_country(cls, country: str) -> List[str]:
|
112
|
+
"""根据国家查找时区"""
|
113
|
+
result = []
|
114
|
+
country_lower = country.lower()
|
115
|
+
for code, info in cls.TIMEZONE_DATA.items():
|
116
|
+
if country_lower in info.country.lower():
|
117
|
+
result.append(code)
|
118
|
+
return result
|
119
|
+
|
120
|
+
@classmethod
|
121
|
+
def convert_timezone(cls, dt: datetime.datetime, from_tz: str, to_tz: str,
|
122
|
+
consider_dst: bool = True) -> datetime.datetime:
|
123
|
+
"""时区转换"""
|
124
|
+
from_info = cls.get_timezone_info(from_tz)
|
125
|
+
to_info = cls.get_timezone_info(to_tz)
|
126
|
+
|
127
|
+
if not from_info or not to_info:
|
128
|
+
raise ValueError(f"不支持的时区: {from_tz} 或 {to_tz}")
|
129
|
+
|
130
|
+
# 计算有效偏移(考虑夏令时)
|
131
|
+
from_offset = from_info.offset
|
132
|
+
to_offset = to_info.offset
|
133
|
+
|
134
|
+
if consider_dst:
|
135
|
+
if cls.is_dst_active(dt, from_info):
|
136
|
+
from_offset = from_info.dst_offset or from_info.offset
|
137
|
+
if cls.is_dst_active(dt, to_info):
|
138
|
+
to_offset = to_info.dst_offset or to_info.offset
|
139
|
+
|
140
|
+
# 转换时间
|
141
|
+
offset_diff = to_offset - from_offset
|
142
|
+
return dt + datetime.timedelta(hours=offset_diff)
|
143
|
+
|
144
|
+
@classmethod
|
145
|
+
def is_dst_active(cls, dt: datetime.datetime, tz_info: TimezoneInfo) -> bool:
|
146
|
+
"""判断指定日期是否在夏令时期间"""
|
147
|
+
if not tz_info.dst_offset or not tz_info.dst_start or not tz_info.dst_end:
|
148
|
+
return False
|
149
|
+
|
150
|
+
# 简化的夏令时判断(实际实现会更复杂)
|
151
|
+
year = dt.year
|
152
|
+
|
153
|
+
# 解析夏令时开始和结束规则
|
154
|
+
dst_start_date = cls._parse_dst_rule(tz_info.dst_start, year)
|
155
|
+
dst_end_date = cls._parse_dst_rule(tz_info.dst_end, year)
|
156
|
+
|
157
|
+
if dst_start_date and dst_end_date:
|
158
|
+
if dst_start_date <= dst_end_date:
|
159
|
+
# 北半球夏令时
|
160
|
+
return dst_start_date <= dt.date() <= dst_end_date
|
161
|
+
else:
|
162
|
+
# 南半球夏令时
|
163
|
+
return dt.date() >= dst_start_date or dt.date() <= dst_end_date
|
164
|
+
|
165
|
+
return False
|
166
|
+
|
167
|
+
@classmethod
|
168
|
+
def _parse_dst_rule(cls, rule: str, year: int) -> Optional[datetime.date]:
|
169
|
+
"""解析夏令时规则"""
|
170
|
+
try:
|
171
|
+
if '月最后一个周日' in rule:
|
172
|
+
month = int(rule.split('月')[0])
|
173
|
+
# 找到该月最后一个周日
|
174
|
+
last_day = datetime.date(year, month + 1, 1) - datetime.timedelta(days=1)
|
175
|
+
while last_day.weekday() != 6: # 6 表示周日
|
176
|
+
last_day -= datetime.timedelta(days=1)
|
177
|
+
return last_day
|
178
|
+
elif '月第' in rule and '个周日' in rule:
|
179
|
+
parts = rule.split('月第')
|
180
|
+
month = int(parts[0])
|
181
|
+
week_num = int(parts[1].split('个周日')[0])
|
182
|
+
# 找到该月第N个周日
|
183
|
+
first_day = datetime.date(year, month, 1)
|
184
|
+
days_to_sunday = (6 - first_day.weekday()) % 7
|
185
|
+
first_sunday = first_day + datetime.timedelta(days=days_to_sunday)
|
186
|
+
target_sunday = first_sunday + datetime.timedelta(weeks=week_num - 1)
|
187
|
+
return target_sunday
|
188
|
+
except:
|
189
|
+
pass
|
190
|
+
return None
|
191
|
+
|
192
|
+
@classmethod
|
193
|
+
def get_current_offset(cls, timezone_code: str) -> float:
|
194
|
+
"""获取当前时区偏移(考虑夏令时)"""
|
195
|
+
tz_info = cls.get_timezone_info(timezone_code)
|
196
|
+
if not tz_info:
|
197
|
+
raise ValueError(f"不支持的时区: {timezone_code}")
|
198
|
+
|
199
|
+
now = datetime.datetime.now()
|
200
|
+
if cls.is_dst_active(now, tz_info):
|
201
|
+
return tz_info.dst_offset or tz_info.offset
|
202
|
+
return tz_info.offset
|
203
|
+
|
204
|
+
@classmethod
|
205
|
+
def format_timezone_offset(cls, offset: float) -> str:
|
206
|
+
"""格式化时区偏移"""
|
207
|
+
if offset == 0:
|
208
|
+
return "UTC"
|
209
|
+
elif offset > 0:
|
210
|
+
hours = int(offset)
|
211
|
+
minutes = int((offset - hours) * 60)
|
212
|
+
if minutes == 0:
|
213
|
+
return f"UTC+{hours}"
|
214
|
+
else:
|
215
|
+
return f"UTC+{hours}:{minutes:02d}"
|
216
|
+
else:
|
217
|
+
hours = int(-offset)
|
218
|
+
minutes = int((-offset - hours) * 60)
|
219
|
+
if minutes == 0:
|
220
|
+
return f"UTC-{hours}"
|
221
|
+
else:
|
222
|
+
return f"UTC-{hours}:{minutes:02d}"
|
223
|
+
|
224
|
+
@classmethod
|
225
|
+
def get_timezone_display_info(cls, timezone_code: str) -> Dict[str, any]:
|
226
|
+
"""获取时区显示信息"""
|
227
|
+
tz_info = cls.get_timezone_info(timezone_code)
|
228
|
+
if not tz_info:
|
229
|
+
raise ValueError(f"不支持的时区: {timezone_code}")
|
230
|
+
|
231
|
+
current_offset = cls.get_current_offset(timezone_code)
|
232
|
+
now = datetime.datetime.now()
|
233
|
+
is_dst = cls.is_dst_active(now, tz_info)
|
234
|
+
|
235
|
+
return {
|
236
|
+
'code': tz_info.code,
|
237
|
+
'name': tz_info.name,
|
238
|
+
'description': tz_info.description,
|
239
|
+
'country': tz_info.country,
|
240
|
+
'cities': tz_info.cities,
|
241
|
+
'current_offset': current_offset,
|
242
|
+
'offset_string': cls.format_timezone_offset(current_offset),
|
243
|
+
'is_dst_active': is_dst,
|
244
|
+
'standard_offset': tz_info.offset,
|
245
|
+
'dst_offset': tz_info.dst_offset
|
246
|
+
}
|
247
|
+
|
248
|
+
# 便捷函数
|
249
|
+
def get_timezone_info(timezone_code: str) -> Optional[TimezoneInfo]:
|
250
|
+
"""获取时区信息(便捷函数)"""
|
251
|
+
return Timezone.get_timezone_info(timezone_code)
|
252
|
+
|
253
|
+
def list_timezones() -> List[str]:
|
254
|
+
"""列出所有支持的时区(便捷函数)"""
|
255
|
+
return Timezone.list_timezones()
|
256
|
+
|
257
|
+
def convert_timezone(dt: datetime.datetime, from_tz: str, to_tz: str) -> datetime.datetime:
|
258
|
+
"""时区转换(便捷函数)"""
|
259
|
+
return Timezone.convert_timezone(dt, from_tz, to_tz)
|
260
|
+
|
261
|
+
def find_timezone_by_city(city: str) -> List[str]:
|
262
|
+
"""根据城市查找时区(便捷函数)"""
|
263
|
+
return Timezone.find_timezone_by_city(city)
|