Files
pod_trend_agent/graph/seeds/holidays.py
T

258 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""月份 / 季节 / 临近节日上下文(按国家),供动态种子词生成的 LLM 上下文使用。
提供 build_holiday_context(country, now=None) -> dict
{
"date": "2026-08-21",
"year": 2026,
"month": 8,
"season": "Summer",
"month_themes": ["back to school", "late summer", "outdoor adventure"],
"upcoming_holidays": ["Back to School", "Summer Solstice"]
}
- month_themes:固定月度灵感,作为种子词生成的稳定基础。
- upcoming_holidays:按国家节日表,用窗口计算临近(含刚过的)固定/浮动节日,
让 LLM 注入"当前该国的可能节日",生成对应节日主题种子。
"""
import datetime
from typing import Dict, List, Optional
# 北半球季节(南半球可反向扩展)
SEASON_BY_MONTH = {
12: "Winter", 1: "Winter", 2: "Winter",
3: "Spring", 4: "Spring", 5: "Spring",
6: "Summer", 7: "Summer", 8: "Summer",
9: "Autumn", 10: "Autumn", 11: "Autumn",
}
# 月度主题词(通用印花设计灵感)
MONTH_THEMES: Dict[int, List[str]] = {
1: ["new year", "winter cozy", "resolution"],
2: ["valentine", "love", "heart"],
3: ["spring bloom", "st patrick", "fresh start"],
4: ["easter", "spring garden", "pastel"],
5: ["mother day", "flower", "spring outdoor"],
6: ["pride", "summer start", "beach"],
7: ["summer vibe", "travel", "festival"],
8: ["back to school", "late summer", "outdoor adventure"],
9: ["autumn equinox", "harvest", "cozy"],
10: ["halloween", "autumn goth", "spooky"],
11: ["thanksgiving", "gratitude", "autumn warm"],
12: ["christmas", "winter holiday", "cozy festive"],
}
# (name, month, day, rule, window_days)
# rule: None=固定日; "mother"=第2周日; "father"=第3周日; "thanks"=第4周四; "easter"=Computus; "bf"=thanks+1
_HOLIDAYS_BY_COUNTRY: Dict[str, List[tuple]] = {
"US": [
("New Year", 1, 1, None, 14),
("Valentine's Day", 2, 14, None, 21),
("St Patrick's Day", 3, 17, None, 14),
("Easter", 0, 0, "easter", 21),
("Mother's Day", 5, 0, "mother", 14),
("Father's Day", 6, 0, "father", 14),
("Pride Month", 6, 1, None, 7),
("Independence Day", 7, 4, None, 21),
("Summer Solstice", 6, 21, None, 14),
("Back to School", 8, 15, None, 30),
("Labor Day", 9, 0, "labor", 14),
("Halloween", 10, 31, None, 30),
("Thanksgiving (US)", 11, 0, "thanks", 21),
("Black Friday", 11, 0, "bf", 14),
("Christmas", 12, 25, None, 30),
("Winter Solstice", 12, 21, None, 14),
],
"GB": [
("New Year", 1, 1, None, 14),
("Valentine's Day", 2, 14, None, 21),
("St Patrick's Day", 3, 17, None, 14),
("Easter", 0, 0, "easter", 21),
("Mother's Day (UK)", 3, 0, "mother_uk", 14),
("Father's Day", 6, 0, "father", 14),
("Summer Bank Holiday", 8, 25, None, 14),
("Halloween", 10, 31, None, 30),
("Bonfire Night", 11, 5, None, 21),
("Remembrance Day", 11, 11, None, 14),
("Christmas", 12, 25, None, 30),
("Boxing Day", 12, 26, None, 14),
],
"JP": [
("New Year", 1, 1, None, 14),
("Valentine's Day", 2, 14, None, 21),
("Hinamatsuri", 3, 3, None, 14), # 雏祭
("Hanami", 4, 1, None, 21), # 花见(樱花季)
("Golden Week", 4, 29, None, 21),
("Children's Day", 5, 5, None, 14), # 子供の日
("Tanabata", 7, 7, None, 14), # 七夕
("Fireworks Season", 8, 1, None, 30), # 花火大会
("Obon", 8, 13, None, 21), # お盆
("Halloween", 10, 31, None, 30),
("Christmas", 12, 25, None, 30),
("New Year Eve", 12, 31, None, 14), # 大晦日
],
"AU": [
("New Year", 1, 1, None, 14),
("Australia Day", 1, 26, None, 21),
("Valentine's Day", 2, 14, None, 21),
("Easter", 0, 0, "easter", 21),
("Anzac Day", 4, 25, None, 21),
("Mother's Day (AU)", 5, 0, "mother", 14),
("Father's Day (AU)", 9, 0, "father", 14),
("Summer Christmas", 12, 25, None, 30),
("Boxing Day", 12, 26, None, 21),
("Halloween", 10, 31, None, 21),
],
"DE": [
("New Year", 1, 1, None, 14),
("Valentine's Day", 2, 14, None, 21),
("Easter", 0, 0, "easter", 21),
("Oktoberfest", 9, 21, "third_saturday", 30), # 啤酒节(9 月第三个周六)
("German Unity Day", 10, 3, None, 21),
("Halloween", 10, 31, None, 30),
("Advent Season", 11, 30, None, 30),
("Christmas", 12, 25, None, 30),
("Boxing Day", 12, 26, None, 14),
],
"BR": [
("New Year", 1, 1, None, 14),
("Carnival", 2, 15, None, 30), # 狂欢节(浮动,2 月近似)
("Easter", 0, 0, "easter", 21),
("Tiradentes Day", 4, 21, None, 14),
("Independence Day (BR)", 9, 7, None, 21),
("Children's Day (BR)", 10, 12, None, 21),
("Halloween", 10, 31, None, 30),
("Christmas", 12, 25, None, 30),
("New Year Eve", 12, 31, None, 14),
],
"SA": [
("New Year", 1, 1, None, 14),
("Saudi Founding Day", 2, 22, None, 21),
("Saudi National Day", 9, 23, None, 30),
("Winter Season", 12, 1, None, 30),
],
"PL": [
("New Year", 1, 1, None, 14),
("Valentine's Day", 2, 14, None, 21),
("Easter", 0, 0, "easter", 21),
("Constitution Day (PL)", 5, 3, None, 21),
("Halloween", 10, 31, None, 30),
("Independence Day (PL)", 11, 11, None, 21),
("Christmas", 12, 25, None, 30),
("Boxing Day", 12, 26, None, 14),
],
"ES": [
("Three Kings Day", 1, 6, None, 14),
("Valentine's Day", 2, 14, None, 21),
("Easter (Semana Santa)", 0, 0, "easter", 21),
("Feria de Abril", 4, 15, None, 21),
("La Tomatina", 8, 27, None, 21),
("Halloween", 10, 31, None, 30),
("Christmas", 12, 25, None, 30),
("New Year Eve", 12, 31, None, 14),
],
"IT": [
("New Year", 1, 1, None, 14),
("Epiphany", 1, 6, None, 14),
("Valentine's Day", 2, 14, None, 21),
("Carnevale", 2, 15, None, 21),
("Easter", 0, 0, "easter", 21),
("Ferragosto", 8, 15, None, 21),
("Halloween", 10, 31, None, 30),
("Christmas", 12, 25, None, 30),
],
"CA": [
("New Year", 1, 1, None, 14),
("Valentine's Day", 2, 14, None, 21),
("Easter", 0, 0, "easter", 21),
("Canada Day", 7, 1, None, 21),
("Thanksgiving (CA)", 10, 12, None, 21), # 10 月第 2 周一(近似固定日)
("Halloween", 10, 31, None, 30),
("Christmas", 12, 25, None, 30),
("Boxing Day", 12, 26, None, 14),
],
}
def _easter(year: int) -> datetime.date:
a = year % 19
b = year // 100
c = year % 100
d = b // 4
e = b % 4
f = (b + 8) // 25
g = (b - f + 1) // 3
h = (19 * a + b - d - g + 15) % 30
i = c // 4
k = c % 4
l = (32 + 2 * e + 2 * i - h - k) % 7
m = (a + 11 * h + 22 * l) // 451
month = (h + l - 7 * m + 114) // 31
day = ((h + l - 7 * m + 114) % 31) + 1
return datetime.date(year, month, day)
def _resolve(name: str, month: int, day: int, rule, year: int) -> Optional[datetime.date]:
if rule is None:
return datetime.date(year, month, day)
if rule == "easter":
return _easter(year)
if rule in ("mother", "mother_uk"):
# 第2个周日(UK 用"母亲节"但实际 3 月第4周日前的第4大斋期周日——简化为 3 月第2周日)
if rule == "mother_uk":
month, week = 3, 2
else:
month, week = month, 2
first = datetime.date(year, month, 1)
return first + datetime.timedelta(days=(6 - first.weekday()) % 7 + (week - 1) * 7)
if rule == "father":
first = datetime.date(year, month, 1)
return first + datetime.timedelta(days=(6 - first.weekday()) % 7 + 2 * 7)
if rule == "thanks":
first = datetime.date(year, month, 1)
return first + datetime.timedelta(days=(3 - first.weekday()) % 7 + 3 * 7)
if rule == "bf":
t = _resolve("", 11, 0, "thanks", year)
return t + datetime.timedelta(days=1) if t else None
if rule == "labor":
first = datetime.date(year, month, 1)
return first + datetime.timedelta(days=(0 - first.weekday()) % 7)
if rule == "third_saturday":
first = datetime.date(year, month, 1)
return first + datetime.timedelta(days=(5 - first.weekday()) % 7 + 2 * 7)
return None
def holidays_for(country: str = "US") -> List[tuple]:
key = (country or "US").upper()
return _HOLIDAYS_BY_COUNTRY.get(key, _HOLIDAYS_BY_COUNTRY["US"])
def upcoming_holidays(now: Optional[datetime.date] = None, country: str = "US",
lower: int = -10) -> List[str]:
"""返回该国临近(未来 window 内,或刚过 lower 天内)的节日名。"""
now = now or datetime.date.today()
out: List[str] = []
for name, month, day, rule, window in holidays_for(country):
try:
d = _resolve(name, month, day, rule, now.year)
except Exception:
continue
if d is None:
continue
delta = (d - now).days
if lower <= delta <= window:
out.append(name)
return out
def build_holiday_context(country: str = "US",
now: Optional[datetime.date] = None) -> Dict[str, object]:
now = now or datetime.date.today()
return {
"date": now.isoformat(),
"year": now.year,
"month": now.month,
"season": SEASON_BY_MONTH.get(now.month, ""),
"month_themes": MONTH_THEMES.get(now.month, []),
"upcoming_holidays": upcoming_holidays(now, country),
}