"""月份 / 季节 / 临近节日上下文(按国家),供动态种子词生成的 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), }