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