feat: extraer parseNueva a módulo dedicado y usarlo desde CommandService
Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>main
parent
f7229d14d4
commit
d591697402
@ -0,0 +1,128 @@
|
||||
export function parseNueva(message: string, _mentionsNormalized: string[]): {
|
||||
action: string;
|
||||
description: string;
|
||||
dueDate: string | null;
|
||||
selfAssign: boolean;
|
||||
} {
|
||||
const parts = (message || '').trim().split(/\s+/);
|
||||
const action = (parts[1] || '').toLowerCase();
|
||||
|
||||
// Zona horaria configurable (por defecto Europe/Madrid)
|
||||
const TZ = process.env.TZ && process.env.TZ.trim() ? process.env.TZ : 'Europe/Madrid';
|
||||
|
||||
// Utilidades locales para operar con fechas en la TZ elegida sin depender del huso del host
|
||||
const ymdFromDateInTZ = (d: Date): string => {
|
||||
const fmt = new Intl.DateTimeFormat('es-ES', {
|
||||
timeZone: TZ,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
}).formatToParts(d);
|
||||
const get = (t: string) => fmt.find(p => p.type === t)?.value || '';
|
||||
return `${get('year')}-${get('month')}-${get('day')}`;
|
||||
};
|
||||
|
||||
const addDaysToYMD = (ymd: string, days: number): string => {
|
||||
const [Y, M, D] = ymd.split('-').map(n => parseInt(n, 10));
|
||||
const base = new Date(Date.UTC(Y, (M || 1) - 1, D || 1));
|
||||
base.setUTCDate(base.getUTCDate() + days);
|
||||
return ymdFromDateInTZ(base);
|
||||
};
|
||||
|
||||
const todayYMD = ymdFromDateInTZ(new Date());
|
||||
|
||||
// Helpers para validar y normalizar fechas explícitas
|
||||
const isLeap = (y: number) => (y % 4 === 0 && y % 100 !== 0) || (y % 400 === 0);
|
||||
const daysInMonth = (y: number, m: number) => {
|
||||
if (m === 2) return isLeap(y) ? 29 : 28;
|
||||
return [31, -1, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][m - 1];
|
||||
};
|
||||
const isValidYMD = (ymd: string): boolean => {
|
||||
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd);
|
||||
if (!m) return false;
|
||||
const Y = parseInt(m[1], 10);
|
||||
const MM = parseInt(m[2], 10);
|
||||
const DD = parseInt(m[3], 10);
|
||||
if (MM < 1 || MM > 12) return false;
|
||||
const dim = daysInMonth(Y, MM);
|
||||
if (!dim || DD < 1 || DD > dim) return false;
|
||||
return true;
|
||||
};
|
||||
const normalizeDateToken = (t: string): string | null => {
|
||||
// YYYY-MM-DD
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(t)) {
|
||||
return isValidYMD(t) ? t : null;
|
||||
}
|
||||
// YY-MM-DD -> 20YY-MM-DD
|
||||
const m = /^(\d{2})-(\d{2})-(\d{2})$/.exec(t);
|
||||
if (m) {
|
||||
const yy = parseInt(m[1], 10);
|
||||
const mm = m[2];
|
||||
const dd = m[3];
|
||||
const yyyy = 2000 + yy;
|
||||
const ymd = `${String(yyyy)}-${mm}-${dd}`;
|
||||
return isValidYMD(ymd) ? ymd : null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
type DateCandidate = { index: number; ymd: string };
|
||||
const dateCandidates: DateCandidate[] = [];
|
||||
const dateTokenIndexes = new Set<number>();
|
||||
const selfTokenIndexes = new Set<number>();
|
||||
let selfAssign = false;
|
||||
|
||||
for (let i = 2; i < parts.length; i++) {
|
||||
// Normalizar token: minúsculas y sin puntuación adyacente simple
|
||||
const raw = parts[i];
|
||||
const low = raw.toLowerCase().replace(/^[([{¿¡"']+/, '').replace(/[.,;:!?)\]}¿¡"'']+$/, '');
|
||||
|
||||
// Fecha explícita en formatos permitidos: YYYY-MM-DD o YY-MM-DD (expandido a 20YY)
|
||||
{
|
||||
const norm = normalizeDateToken(low);
|
||||
if (norm && norm >= todayYMD) {
|
||||
dateCandidates.push({ index: i, ymd: norm });
|
||||
dateTokenIndexes.add(i);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Tokens naturales "hoy"/"mañana" (con o sin acento)
|
||||
if (low === 'hoy') {
|
||||
dateCandidates.push({ index: i, ymd: todayYMD });
|
||||
dateTokenIndexes.add(i);
|
||||
continue;
|
||||
}
|
||||
if (low === 'mañana' || low === 'manana') {
|
||||
dateCandidates.push({ index: i, ymd: addDaysToYMD(todayYMD, 1) });
|
||||
dateTokenIndexes.add(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Autoasignación: detectar 'yo' o '@yo' como palabra aislada (insensible a mayúsculas; ignora puntuación simple)
|
||||
if (low === 'yo' || low === '@yo') {
|
||||
selfAssign = true;
|
||||
selfTokenIndexes.add(i);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const dueDate = dateCandidates.length > 0
|
||||
? dateCandidates[dateCandidates.length - 1].ymd
|
||||
: null;
|
||||
|
||||
const isMentionToken = (token: string) => token.startsWith('@');
|
||||
|
||||
const descriptionTokens: string[] = [];
|
||||
for (let i = 2; i < parts.length; i++) {
|
||||
if (dateTokenIndexes.has(i)) continue;
|
||||
if (selfTokenIndexes.has(i)) continue;
|
||||
const token = parts[i];
|
||||
if (isMentionToken(token)) continue;
|
||||
descriptionTokens.push(token);
|
||||
}
|
||||
|
||||
const description = descriptionTokens.join(' ').trim();
|
||||
|
||||
return { action, description, dueDate, selfAssign };
|
||||
}
|
||||
Loading…
Reference in New Issue