"""Shared timezone data — country → UTC offset, fuzzy lookup, autocomplete. ###################################################################### Used by the calendar, /timestamp, and timestamp-friends cogs. """ import re from datetime import datetime, timezone, timedelta ###################################################################### # --------------------------------------------------------------------------- # AM/PM detection — used to avoid overriding explicit meridian indicators # --------------------------------------------------------------------------- _AMPM_RE = re.compile( r"(?:\b|(?<=\d))(?:am|a\.m\.?|pm|p\.m\.?)\b", re.IGNORECASE ) def has_ampm(text: str) -> bool: """Return True if *text* contains an explicit am/pm indicator.""" return bool(_AMPM_RE.search(text)) # --------------------------------------------------------------------------- # bare hour fallback — for when dateparser ignores bare "3" or "at 11" # --------------------------------------------------------------------------- _BARE_HOUR_RE = re.compile( r"\b(\d{1,2})\b(?!\s*(?:am|a\.m\.?|pm|p\.m\.?|:))", re.IGNORECASE, ) def parse_bare_hours(text: str, offset: float) -> list[int]: """Find bare hour numbers (1-24) in *text*, convert to UTC unix timestamps. Bare hours without AM/PM default to PM. If the resulting time is already past today in the given UTC offset, it's pushed to tomorrow. """ raw = _BARE_HOUR_RE.findall(text) if not raw: return [] now_local = datetime.now(timezone.utc).astimezone( timezone(timedelta(hours=offset)) ) results: list[int] = [] seen: set[int] = set() for hour_str in raw: h = int(hour_str) if h < 1 or h > 24: continue if 1 <= h <= 11 and not has_ampm(text): h += 12 if h == 24: h = 0 dt = now_local.replace(hour=h, minute=0, second=0, microsecond=0) if dt <= now_local: dt += timedelta(days=1) unix_ts = int(dt.astimezone(timezone.utc).timestamp()) if unix_ts not in seen: seen.add(unix_ts) results.append(unix_ts) return results # --------------------------------------------------------------------------- # country offset data # --------------------------------------------------------------------------- # fmt: off COUNTRY_TZ: dict[str, float] = { # Europe "Turkey": 3, "UK": 0, "Germany": 2, "France": 2, "Netherlands": 2, "Belgium": 2, "Poland": 2, "Italy": 2, "Spain": 2, "Sweden": 2, "Norway": 2, "Denmark": 2, "Switzerland": 2, "Austria": 2, "Czechia": 2, "Czech Republic": 2, "Czech": 2, "Greece": 3, "Finland": 3, "Romania": 3, "Ukraine": 3, "Portugal": 1, "Ireland": 1, "Iceland": 0, # Americas "US East": -5, "US Central": -6, "US Mountain": -7, "US West": -8, "US Alaska": -9, "US Hawaii": -10, "Canada East": -5, "Canada Central": -6, "Canada West": -8, "Mexico": -6, "Brazil": -3, "Argentina": -3, "Colombia": -5, "Chile": -4, # Asia / Pacific "Japan": 9, "Korea": 9, "China": 8, "Taiwan": 8, "Hong Kong": 8, "Singapore": 8, "Malaysia": 8, "Philippines": 8, "Indonesia West": 7, "Thailand": 7, "Vietnam": 7, "India": 5.5, "Pakistan": 5, "Bangladesh": 6, "Iran": 3.5, "Israel": 3, "Saudi Arabia": 3, "UAE": 4, # Oceania "Australia East": 11, "Australia Central": 10.5, "Australia West": 8, "New Zealand": 13, # Africa "South Africa": 2, "Egypt": 3, "Nigeria": 1, "Kenya": 3, "Morocco": 1, } # fmt: on def find_offset(country: str) -> float | None: """Look up a country's UTC offset. Case-insensitive fuzzy match.""" c = country.strip().lower() if not c: return None for name, offset in COUNTRY_TZ.items(): if c == name.lower(): return offset for name, offset in COUNTRY_TZ.items(): if name.lower().startswith(c): return offset for name, offset in COUNTRY_TZ.items(): if c in name.lower(): return offset return None async def country_autocomplete( _interaction, current: str, ) -> list: """Return matching countries for slash-command autocomplete.""" from discord import app_commands current = current.lower() matches = [ app_commands.Choice(name=c, value=c) for c in sorted(COUNTRY_TZ) if current in c.lower() ] return matches[:25] # Discord limits to 25 suggestions