feat: enhance relative time parsing and clean up reminder text handling

This commit is contained in:
2025-09-28 01:37:55 -05:00
parent 809128e2cc
commit ffa69f2330

View File

@@ -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}`);
}
};