fix timestamp_friends: normalize N am/pm, fix 3:00 double-colon, AM fallback when PM past

This commit is contained in:
esef committed 2026-07-20 22:15:45 +03:00
1 parent 697012963a
commit bbee62f2d9
2 files changed
+57 -16

No files matched your search

+8 -5
View File
@@ -16,7 +16,7 @@ from datetime import timezone, timedelta
import discord
from discord.ext import commands
from .tz_data import find_offset, has_ampm, parse_bare_hours
from .tz_data import find_offset, has_ampm, normalize_bare_hours, parse_bare_hours
log = logging.getLogger(__name__)
@@ -103,10 +103,13 @@ class TimestampFriends(commands.Cog):
return
# --- strip the trigger phrase ---
cleaned = _TRIGGER_RE.sub(" ", message.content).strip()
if not cleaned:
original = _TRIGGER_RE.sub(" ", message.content).strip()
if not original:
return # message was literally just "my time"
# --- normalize bare hours so dateparser doesn't mistake them for dates ---
cleaned = normalize_bare_hours(original)
# --- parse time expressions ---
try:
raw_matches = _search_dates(
@@ -123,8 +126,8 @@ class TimestampFriends(commands.Cog):
country, offset = self.friends[message.author.id]
if not raw_matches:
# --- fallback: bare hour numbers dateparser missed ---
results = parse_bare_hours(cleaned, offset)
# --- fallback: use original text (not normalized) ---
results = parse_bare_hours(original, offset)
if results:
tags = [_tag(ts) for ts in results]
await message.reply("\n".join(tags), mention_author=False)
+49 -11
View File
@@ -20,20 +20,41 @@ def has_ampm(text: str) -> bool:
# ---------------------------------------------------------------------------
# bare hour fallback — for when dateparser ignores bare "3" or "at 11"
# bare hour handling — two regexes: one for normalize, one for fallback
# ---------------------------------------------------------------------------
_BARE_HOUR_RE = re.compile(
r"\b(\d{1,2})\b(?!\s*(?:am|a\.m\.?|pm|p\.m\.?|:))",
# Matches a bare hour number (1-24) NOT preceded by colon (excludes minutes
# in "3:00") and NOT followed by colon (excludes "3:00" hours). Captures
# an optional am/pm suffix separately.
_HOUR_RE = re.compile(
r"(?<!:)\b(\d{1,2})(?!\d)(?!\s*:)(?:\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.
def normalize_bare_hours(text: str) -> str:
"""Insert ``:00`` before optional am/pm so dateparser reads times correctly.
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.
``sunday at 6`` → ``sunday at 6:00``
``sunday at 6 am`` → ``sunday at 6:00 am``
``3pm`` → ``3:00 pm``
"""
raw = _BARE_HOUR_RE.findall(text)
def _repl(m: re.Match) -> str:
h = m.group(1)
mer = m.group(2)
if mer:
return f"{h}:00 {mer}"
return f"{h}:00"
return _HOUR_RE.sub(_repl, text)
def parse_bare_hours(text: str, offset: float) -> list[int]:
"""Find hour patterns in *text*, convert to UTC unix timestamps.
Handles bare hours (``3``, ``at 11``), ``3pm``, ``3 am``, ``14``, etc.
No-AM/PM hours 1-11 default to PM; if past, falls back to AM before
pushing to tomorrow.
"""
raw: list[tuple[str, str | None]] = _HOUR_RE.findall(text)
if not raw:
return []
@@ -43,14 +64,31 @@ def parse_bare_hours(text: str, offset: float) -> list[int]:
results: list[int] = []
seen: set[int] = set()
for hour_str in raw:
for hour_str, meridian in raw:
h = int(hour_str)
if h < 1 or h > 24:
continue
if 1 <= h <= 11 and not has_ampm(text):
# Determine AM/PM from meridian or default
if meridian:
is_pm = meridian.lower().startswith("p")
if is_pm and 1 <= h <= 11:
h += 12
if h == 24:
elif not is_pm and h == 12:
h = 0
elif 1 <= h <= 11:
# No meridian — try PM first, fall back to AM if past
h_pm = h + 12
dt_pm = now_local.replace(hour=h_pm, minute=0, second=0, microsecond=0)
if dt_pm > now_local:
h = h_pm
else:
dt_am = now_local.replace(hour=h, minute=0, second=0, microsecond=0)
if dt_am <= now_local:
pass # both past, keep AM, push tomorrow below
elif h == 24:
h = 0
dt = now_local.replace(hour=h, minute=0, second=0, microsecond=0)
if dt <= now_local:
dt += timedelta(days=1)