From ffa69f233037a2caef3911b8c8e4f2e889edf115 Mon Sep 17 00:00:00 2001 From: shni Date: Sun, 28 Sep 2025 01:37:55 -0500 Subject: [PATCH] feat: enhance relative time parsing and clean up reminder text handling --- src/commands/messages/others/recordar.ts | 123 +++++++++++++++++++---- 1 file changed, 103 insertions(+), 20 deletions(-) diff --git a/src/commands/messages/others/recordar.ts b/src/commands/messages/others/recordar.ts index be80290..82621f1 100644 --- a/src/commands/messages/others/recordar.ts +++ b/src/commands/messages/others/recordar.ts @@ -13,60 +13,135 @@ function humanizeDate(d: Date) { return d.toLocaleString('es-ES', { timeZone: 'UTC', hour12: false }); } +function formatRelativeEs(when: Date): string { + const diffMs = when.getTime() - Date.now(); + const diffSec = Math.max(0, Math.round(diffMs / 1000)); + if (diffSec < 60) { + const s = Math.max(1, diffSec); + return `en ${s} segundo${s === 1 ? '' : 's'}`; + } + const diffMin = Math.round(diffSec / 60); + if (diffMin < 60) { + const m = Math.max(1, diffMin); + return `en ${m} minuto${m === 1 ? '' : 's'}`; + } + const diffH = Math.round(diffMin / 60); + if (diffH < 24) { + const h = Math.max(1, diffH); + return `en ${h} hora${h === 1 ? '' : 's'}`; + } + const d = Math.max(1, Math.round(diffH / 24)); + return `en ${d} día${d === 1 ? '' : 's'}`; +} + +function cleanSpaces(s: string): string { + return s.replace(/\s{2,}/g, ' ').replace(/^\s+|\s+$/g, ''); +} + function parseRelativeDelay(text: string): { when: Date, reminderText: string } | null { const lower = text.toLowerCase(); + // 0) Helpers de unidades + const minUnit = 'm(?:in(?:uto(?:s)?)?)?'; + const hourUnit = 'h(?:ora(?:s)?)?'; + const dayUnit = 'd(?:[ií]a(?:s)?)?|d'; // dia, días, dias, día, d + // 1) "en menos de 1h" -> 59 minutos - let m = lower.match(/en\s+menos\s+de\s+1\s*h(ora(s)?)?/i); + let m = lower.match(new RegExp(`en\\s+menos\\s+de\\s+1\\s*${hourUnit}`, 'i')); if (m) { const minutes = 59; const when = new Date(Date.now() + minutes * 60 * 1000); - const reminderText = text.replace(m[0], '').trim() || text; + const reminderText = cleanSpaces(text.replace(m[0], '')) || text; return { when, reminderText }; } // 2) "en menos de X min" -> (X-1) minutos - m = lower.match(/en\s+menos\s+de\s+(\d+)\s*m(in(utos)?)?/i); + m = lower.match(new RegExp(`en\\s+menos\\s+de\\s+(\\d+)\\s*${minUnit}`, 'i')); if (m) { const minutes = Math.max(1, parseInt(m[1], 10) - 1); const when = new Date(Date.now() + minutes * 60 * 1000); - const reminderText = text.replace(m[0], '').trim() || text; + const reminderText = cleanSpaces(text.replace(m[0], '')) || text; return { when, reminderText }; } // 3) "en X minutos" / "en Xm" - m = lower.match(/en\s+(\d+)\s*m(in(utos)?)?/i); + m = lower.match(new RegExp(`en\\s+(\\d+)\\s*${minUnit}`, 'i')); if (m) { const minutes = Math.max(1, parseInt(m[1], 10)); const when = new Date(Date.now() + minutes * 60 * 1000); - const reminderText = text.replace(m[0], '').trim() || text; + const reminderText = cleanSpaces(text.replace(m[0], '')) || text; return { when, reminderText }; } // 4) "en X horas" / "en Xh" - m = lower.match(/en\s+(\d+)\s*h(ora(s)?)?/i); + m = lower.match(new RegExp(`en\\s+(\\d+)\\s*${hourUnit}`, 'i')); if (m) { const hours = Math.max(1, parseInt(m[1], 10)); const when = new Date(Date.now() + hours * 60 * 60 * 1000); - const reminderText = text.replace(m[0], '').trim() || text; + const reminderText = cleanSpaces(text.replace(m[0], '')) || text; return { when, reminderText }; } - // 5) Post-fijo corto: "15m" o "45 min" al final - m = lower.match(/(\d+)\s*m(in(utos)?)?$/i); + // 5) "en X días" / "en Xd" + m = lower.match(new RegExp(`en\\s+(\\d+)\\s*${dayUnit}`, 'i')); + if (m) { + const days = Math.max(1, parseInt(m[1], 10)); + const when = new Date(Date.now() + days * 24 * 60 * 60 * 1000); + const reminderText = cleanSpaces(text.replace(m[0], '')) || text; + return { when, reminderText }; + } + + // 6) "dentro de X minutos" + m = lower.match(new RegExp(`dentro\\s+de\\s+(\\d+)\\s*${minUnit}`, 'i')); if (m) { const minutes = Math.max(1, parseInt(m[1], 10)); const when = new Date(Date.now() + minutes * 60 * 1000); - const reminderText = text.slice(0, m.index).trim() || text; + const reminderText = cleanSpaces(text.replace(m[0], '')) || text; return { when, reminderText }; } - // 6) Post-fijo corto: "1h" o "2 horas" al final - m = lower.match(/(\d+)\s*h(ora(s)?)?$/i); + // 7) "dentro de X horas" + m = lower.match(new RegExp(`dentro\\s+de\\s+(\\d+)\\s*${hourUnit}`, 'i')); if (m) { const hours = Math.max(1, parseInt(m[1], 10)); const when = new Date(Date.now() + hours * 60 * 60 * 1000); - const reminderText = text.slice(0, m.index).trim() || text; + const reminderText = cleanSpaces(text.replace(m[0], '')) || text; + return { when, reminderText }; + } + + // 8) "dentro de X días" + m = lower.match(new RegExp(`dentro\\s+de\\s+(\\d+)\\s*${dayUnit}`, 'i')); + if (m) { + const days = Math.max(1, parseInt(m[1], 10)); + const when = new Date(Date.now() + days * 24 * 60 * 60 * 1000); + const reminderText = cleanSpaces(text.replace(m[0], '')) || text; + return { when, reminderText }; + } + + // 9) Post-fijo corto al final: "15m" o "45 min" + m = lower.match(new RegExp(`(\\d+)\\s*${minUnit}$`, 'i')); + if (m) { + const minutes = Math.max(1, parseInt(m[1], 10)); + const when = new Date(Date.now() + minutes * 60 * 1000); + const reminderText = cleanSpaces(text.slice(0, (m.index ?? text.length))).trim() || text; + return { when, reminderText }; + } + + // 10) Post-fijo corto: "1h" o "2 horas" al final + m = lower.match(new RegExp(`(\\d+)\\s*${hourUnit}$`, 'i')); + if (m) { + const hours = Math.max(1, parseInt(m[1], 10)); + const when = new Date(Date.now() + hours * 60 * 60 * 1000); + const reminderText = cleanSpaces(text.slice(0, (m.index ?? text.length))).trim() || text; + return { when, reminderText }; + } + + // 11) Post-fijo corto: "7d" o "7 dias" al final + m = lower.match(new RegExp(`(\\d+)\\s*${dayUnit}$`, 'i')); + if (m) { + const days = Math.max(1, parseInt(m[1], 10)); + const when = new Date(Date.now() + days * 24 * 60 * 60 * 1000); + const reminderText = cleanSpaces(text.slice(0, (m.index ?? text.length))).trim() || text; return { when, reminderText }; } @@ -93,12 +168,12 @@ export const command: CommandMessage = { } // 1) Soporte rápido para tiempos relativos: "en 10 minutos", "15m", "1h", "en menos de 1h" - const rel = parseRelativeDelay(text); + const relParsed = parseRelativeDelay(text); let when: Date | null = null; let reminderText = text; - if (rel) { - when = rel.when; - reminderText = rel.reminderText; + if (relParsed) { + when = relParsed.when; + reminderText = relParsed.reminderText; } // 2) Si no hubo match relativo, usar parser natural (chrono) en español @@ -122,7 +197,14 @@ export const command: CommandMessage = { if (matched) { const idx = text.toLowerCase().indexOf(matched.toLowerCase()); if (idx >= 0) { - reminderText = (text.slice(0, idx) + text.slice(idx + matched.length)).trim() || text; + // Intentar eliminar también un conector previo como "en" o "dentro de" + let start = idx; + const before = text.slice(0, idx); + const lead = before.match(/(?:en|dentro\s+de)\s*$/i); + if (lead) start = idx - lead[0].length; + + let reminderTextCandidate = text.slice(0, start) + text.slice(idx + matched.length); + reminderText = cleanSpaces(reminderTextCandidate) || text; } } } @@ -150,6 +232,7 @@ export const command: CommandMessage = { } const whenHuman = humanizeDate(when); - await message.reply(`✅ Recordatorio guardado para: ${whenHuman} UTC\nMensaje: ${reminderText}`); + const relText = formatRelativeEs(when); + await message.reply(`✅ Recordatorio guardado: ${relText} (${whenHuman} UTC)\nMensaje: ${reminderText}`); } };