simplify timestamp_friends: single tag reply, bare hour fallback, shared utils in tz_data
This commit is contained in:
1 parent
7018366068
commit
697012963a
2 files changed
+85
-59
No files matched your search
+23
-59
@@ -11,12 +11,12 @@ Friend map: TIMESTAMP_FRIEND_{DISCORD_USER_ID}=Country in credentials.env
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import timezone, timedelta
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
|
||||||
from .tz_data import find_offset
|
from .tz_data import find_offset, has_ampm, parse_bare_hours
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -35,33 +35,17 @@ except ImportError:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# trigger phrase & morning-indicator regex
|
# trigger phrase
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
_TRIGGER_RE = re.compile(r"my time", re.IGNORECASE)
|
_TRIGGER_RE = re.compile(r"my time", re.IGNORECASE)
|
||||||
|
|
||||||
# Morning indicators that prevent the PM default from kicking in.
|
|
||||||
# (?:\b|(?<=\d)) handles "3 am" (space), "3am" (digit-prefixed), and
|
|
||||||
# "a.m." at sentence start — \b alone fails when am/a.m. follows a digit.
|
|
||||||
_MORNING_RE = re.compile(
|
|
||||||
r"(?:\b|(?<=\d))(?:am|a\.m|morning|dawn|sunrise|early\s*morning)\b",
|
|
||||||
re.IGNORECASE,
|
|
||||||
)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# helpers
|
# helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _all_formats(unix_ts: int) -> list[tuple[str, str]]:
|
def _tag(unix_ts: int) -> str:
|
||||||
"""Return every Discord timestamp format for *unix_ts*."""
|
"""Return a single Discord Short Date+Time tag."""
|
||||||
return [
|
return f"<t:{unix_ts}:f>"
|
||||||
("Short Time", f"<t:{unix_ts}:t>"),
|
|
||||||
("Long Time", f"<t:{unix_ts}:T>"),
|
|
||||||
("Short Date", f"<t:{unix_ts}:d>"),
|
|
||||||
("Long Date", f"<t:{unix_ts}:D>"),
|
|
||||||
("Short Date+Time", f"<t:{unix_ts}:f>"),
|
|
||||||
("Long Date+Time", f"<t:{unix_ts}:F>"),
|
|
||||||
("Relative", f"<t:{unix_ts}:R>"),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def _load_friend_map() -> dict[int, tuple[str, float]]:
|
def _load_friend_map() -> dict[int, tuple[str, float]]:
|
||||||
@@ -91,18 +75,6 @@ def _load_friend_map() -> dict[int, tuple[str, float]]:
|
|||||||
return friends
|
return friends
|
||||||
|
|
||||||
|
|
||||||
def _apply_pm_default(
|
|
||||||
dt: datetime, matched_text: str,
|
|
||||||
) -> datetime:
|
|
||||||
"""If *matched_text* has no morning indicator and the hour is 1–11,
|
|
||||||
bump by 12 h to treat the bare number as PM."""
|
|
||||||
if _MORNING_RE.search(matched_text):
|
|
||||||
return dt
|
|
||||||
if 1 <= dt.hour <= 11:
|
|
||||||
return dt + timedelta(hours=12)
|
|
||||||
return dt
|
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# COG
|
# COG
|
||||||
# ==========================================
|
# ==========================================
|
||||||
@@ -148,49 +120,41 @@ class TimestampFriends(commands.Cog):
|
|||||||
except Exception:
|
except Exception:
|
||||||
log.debug("timestamp_friends: search_dates failed", exc_info=True)
|
log.debug("timestamp_friends: search_dates failed", exc_info=True)
|
||||||
return # quiet fail
|
return # quiet fail
|
||||||
if not raw_matches:
|
|
||||||
return # quiet fail — nothing parseable
|
|
||||||
|
|
||||||
country, offset = self.friends[message.author.id]
|
country, offset = self.friends[message.author.id]
|
||||||
|
|
||||||
|
if not raw_matches:
|
||||||
|
# --- fallback: bare hour numbers dateparser missed ---
|
||||||
|
results = parse_bare_hours(cleaned, offset)
|
||||||
|
if results:
|
||||||
|
tags = [_tag(ts) for ts in results]
|
||||||
|
await message.reply("\n".join(tags), mention_author=False)
|
||||||
|
return
|
||||||
|
|
||||||
tz = timezone(timedelta(hours=offset))
|
tz = timezone(timedelta(hours=offset))
|
||||||
|
|
||||||
# --- process each match ---
|
# --- process each match ---
|
||||||
results: list[tuple[datetime, int]] = [] # (local_dt, unix_ts)
|
results: list[int] = []
|
||||||
seen: set[int] = set() # dedupe by unix ts
|
seen: set[int] = set() # dedupe by unix ts
|
||||||
|
|
||||||
for matched_text, dt_naive in raw_matches:
|
for matched_text, dt_naive in raw_matches:
|
||||||
# PM default when no meridian given
|
# bare times without AM/PM default to PM
|
||||||
dt_adjusted = _apply_pm_default(dt_naive, matched_text)
|
if not has_ampm(matched_text) and 1 <= dt_naive.hour <= 11:
|
||||||
|
dt_naive = dt_naive + timedelta(hours=12)
|
||||||
|
|
||||||
# attach friend's timezone, then convert to UTC
|
# attach friend's timezone, then convert to UTC
|
||||||
dt_aware = dt_adjusted.replace(tzinfo=tz)
|
dt_aware = dt_naive.replace(tzinfo=tz)
|
||||||
unix_ts = int(dt_aware.astimezone(timezone.utc).timestamp())
|
unix_ts = int(dt_aware.astimezone(timezone.utc).timestamp())
|
||||||
|
|
||||||
if unix_ts not in seen:
|
if unix_ts not in seen:
|
||||||
seen.add(unix_ts)
|
seen.add(unix_ts)
|
||||||
results.append((dt_adjusted, unix_ts))
|
results.append(unix_ts)
|
||||||
|
|
||||||
if not results:
|
if not results:
|
||||||
return # quiet fail
|
return # quiet fail
|
||||||
|
|
||||||
# --- build reply ---
|
tags = [_tag(ts) for ts in results]
|
||||||
lines: list[str] = []
|
await message.reply("\n".join(tags), mention_author=False)
|
||||||
if len(results) > 1:
|
|
||||||
lines.append(f"*{len(results)} times found*")
|
|
||||||
|
|
||||||
for local_dt, unix_ts in results:
|
|
||||||
if lines:
|
|
||||||
lines.append("")
|
|
||||||
lines.append(f"**{local_dt.strftime('%d %B at %H:%M')}**")
|
|
||||||
lines.append(f"Unix: `{unix_ts}`")
|
|
||||||
lines.append("")
|
|
||||||
for label, tag in _all_formats(unix_ts):
|
|
||||||
lines.append(f"`{tag}` ← {label}")
|
|
||||||
|
|
||||||
lines.append("")
|
|
||||||
lines.append("*Times display in each viewer's local timezone.*")
|
|
||||||
|
|
||||||
await message.reply("\n".join(lines), mention_author=False)
|
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|||||||
@@ -2,8 +2,70 @@
|
|||||||
######################################################################
|
######################################################################
|
||||||
Used by the calendar, /timestamp, and timestamp-friends cogs.
|
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
|
# fmt: off
|
||||||
COUNTRY_TZ: dict[str, float] = {
|
COUNTRY_TZ: dict[str, float] = {
|
||||||
# Europe
|
# Europe
|
||||||
|
|||||||
Reference in new issue
Block a user