Primeiro Commit v0.beta1
This commit is contained in:
340
scripts/lib/date-windows.mjs
Normal file
340
scripts/lib/date-windows.mjs
Normal file
@@ -0,0 +1,340 @@
|
||||
const ANNUAL_WEEK_START_MONTH_INDEX = 5;
|
||||
const ANNUAL_WEEK_START_DAY = 1;
|
||||
const CURRENT_PERIOD_TOKENS = new Set([
|
||||
"current",
|
||||
"current-week",
|
||||
"this-week",
|
||||
"week-current",
|
||||
"semana-atual",
|
||||
"week atual",
|
||||
]);
|
||||
const CURRENT_MONTH_TOKENS = new Set([
|
||||
"current",
|
||||
"current-month",
|
||||
"this-month",
|
||||
"month-current",
|
||||
"mes-atual",
|
||||
"mês-atual",
|
||||
"mes atual",
|
||||
"mês atual",
|
||||
]);
|
||||
const CURRENT_QUARTER_TOKENS = new Set([
|
||||
"current",
|
||||
"current-quarter",
|
||||
"this-quarter",
|
||||
"quarter-current",
|
||||
"quarter atual",
|
||||
"trimestre-atual",
|
||||
"trimestre atual",
|
||||
]);
|
||||
const CURRENT_FISCAL_YEAR_TOKENS = new Set([
|
||||
"current",
|
||||
"current-fy",
|
||||
"this-fy",
|
||||
"fy-current",
|
||||
"fy atual",
|
||||
"ano-fiscal-atual",
|
||||
"ano fiscal atual",
|
||||
]);
|
||||
|
||||
function quoteLiteral(value) {
|
||||
return `'${String(value).replace(/'/g, "''")}'`;
|
||||
}
|
||||
|
||||
function lastDayOfMonth(year, monthIndex) {
|
||||
return new Date(year, monthIndex + 1, 0);
|
||||
}
|
||||
|
||||
function formatIsoDate(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function addDays(date, days) {
|
||||
const next = new Date(date.getTime());
|
||||
next.setDate(next.getDate() + days);
|
||||
return next;
|
||||
}
|
||||
|
||||
function isLeapYear(year) {
|
||||
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
||||
}
|
||||
|
||||
function isValidIsoCalendarDate(raw) {
|
||||
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw);
|
||||
if (!match) return false;
|
||||
const [, yearText, monthText, dayText] = match;
|
||||
const year = Number(yearText);
|
||||
const month = Number(monthText);
|
||||
const day = Number(dayText);
|
||||
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) {
|
||||
return false;
|
||||
}
|
||||
if (month < 1 || month > 12) return false;
|
||||
const daysInMonth = [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||
return day >= 1 && day <= daysInMonth[month - 1];
|
||||
}
|
||||
|
||||
function resolveDateInput(value, fallback) {
|
||||
const raw = String(value || fallback || "").trim();
|
||||
if (!raw) {
|
||||
throw new Error("Date input is required.");
|
||||
}
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
|
||||
if (!isValidIsoCalendarDate(raw)) {
|
||||
throw new Error(`Invalid calendar date "${raw}". Use a real date in YYYY-MM-DD format.`);
|
||||
}
|
||||
return { input: raw, isoDate: raw };
|
||||
}
|
||||
if (raw.toLowerCase() === "today") {
|
||||
return { input: raw, isoDate: formatIsoDate(new Date()) };
|
||||
}
|
||||
const relativeMatch = /^(?:today)?([+-]\d+)d$/i.exec(raw);
|
||||
if (relativeMatch) {
|
||||
const days = Number(relativeMatch[1]);
|
||||
return { input: raw, isoDate: formatIsoDate(addDays(new Date(), days)) };
|
||||
}
|
||||
throw new Error(
|
||||
`Unsupported date token "${raw}". Use YYYY-MM-DD, today, +7d, -3d, or today+7d.`
|
||||
);
|
||||
}
|
||||
|
||||
function resolveMonthInput(value) {
|
||||
const raw = String(value || "").trim();
|
||||
if (CURRENT_MONTH_TOKENS.has(raw.toLowerCase())) {
|
||||
const today = new Date();
|
||||
const first = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
const last = lastDayOfMonth(today.getFullYear(), today.getMonth());
|
||||
return {
|
||||
input: raw,
|
||||
fromDate: formatIsoDate(first),
|
||||
toDate: formatIsoDate(last),
|
||||
};
|
||||
}
|
||||
if (!/^\d{4}-\d{2}$/.test(raw)) {
|
||||
throw new Error(`Unsupported month token "${raw}". Use YYYY-MM, for example 2026-04.`);
|
||||
}
|
||||
const [yearText, monthText] = raw.split("-");
|
||||
const year = Number(yearText);
|
||||
const month = Number(monthText);
|
||||
if (!Number.isInteger(year) || !Number.isInteger(month) || month < 1 || month > 12) {
|
||||
throw new Error(`Unsupported month token "${raw}". Use YYYY-MM, for example 2026-04.`);
|
||||
}
|
||||
const first = new Date(year, month - 1, 1);
|
||||
const last = lastDayOfMonth(year, month - 1);
|
||||
return {
|
||||
input: raw,
|
||||
fromDate: formatIsoDate(first),
|
||||
toDate: formatIsoDate(last),
|
||||
};
|
||||
}
|
||||
|
||||
function fiscalQuarterToDateWindow(fiscalYear, quarter) {
|
||||
const normalizedFiscalYear = String(fiscalYear || "").trim().toUpperCase();
|
||||
const normalizedQuarter = String(quarter || "").trim().toUpperCase();
|
||||
const fiscalMatch = /^FY(\d{2,4})$/.exec(normalizedFiscalYear);
|
||||
const quarterMatch = /^Q([1-4])$/.exec(normalizedQuarter);
|
||||
if (!fiscalMatch || !quarterMatch) {
|
||||
throw new Error(`Unsupported fiscal quarter token "${fiscalYear} ${quarter}". Use FY26-Q4 or Q4 FY26.`);
|
||||
}
|
||||
const endYear = fiscalMatch[1].length === 2 ? 2000 + Number(fiscalMatch[1]) : Number(fiscalMatch[1]);
|
||||
const quarterNumber = Number(quarterMatch[1]);
|
||||
const startYear = endYear - 1;
|
||||
const quarterWindows = {
|
||||
1: { from: new Date(startYear, 5, 1), to: lastDayOfMonth(startYear, 7) },
|
||||
2: { from: new Date(startYear, 8, 1), to: lastDayOfMonth(startYear, 10) },
|
||||
3: { from: new Date(startYear, 11, 1), to: lastDayOfMonth(endYear, 1) },
|
||||
4: { from: new Date(endYear, 2, 1), to: lastDayOfMonth(endYear, 4) },
|
||||
};
|
||||
return {
|
||||
fiscalYear: normalizedFiscalYear,
|
||||
quarter: `Q${quarterNumber}`,
|
||||
fromDate: formatIsoDate(quarterWindows[quarterNumber].from),
|
||||
toDate: formatIsoDate(quarterWindows[quarterNumber].to),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveQuarterInput(value) {
|
||||
const raw = String(value || "").trim();
|
||||
const normalized = raw.toLowerCase();
|
||||
if (CURRENT_QUARTER_TOKENS.has(normalized)) {
|
||||
const today = new Date();
|
||||
const cycleYear = resolveWeekYearInput("");
|
||||
const monthIndex = today.getMonth();
|
||||
let quarter = "Q1";
|
||||
if (monthIndex >= 5 && monthIndex <= 7) {
|
||||
quarter = "Q1";
|
||||
} else if (monthIndex >= 8 && monthIndex <= 10) {
|
||||
quarter = "Q2";
|
||||
} else if (monthIndex === 11 || monthIndex <= 1) {
|
||||
quarter = "Q3";
|
||||
} else {
|
||||
quarter = "Q4";
|
||||
}
|
||||
const fiscalYear = `FY${String(cycleYear + 1).padStart(2, "0").slice(-2)}`;
|
||||
return {
|
||||
input: raw,
|
||||
...fiscalQuarterToDateWindow(fiscalYear, quarter),
|
||||
};
|
||||
}
|
||||
const compactMatch = /^(FY\d{2,4})[-\s]Q([1-4])$/i.exec(raw);
|
||||
if (compactMatch) {
|
||||
return {
|
||||
input: raw,
|
||||
...fiscalQuarterToDateWindow(compactMatch[1], `Q${compactMatch[2]}`),
|
||||
};
|
||||
}
|
||||
const reversedMatch = /^Q([1-4])[-\s]?(FY\d{2,4})$/i.exec(raw);
|
||||
if (reversedMatch) {
|
||||
return {
|
||||
input: raw,
|
||||
...fiscalQuarterToDateWindow(reversedMatch[2], `Q${reversedMatch[1]}`),
|
||||
};
|
||||
}
|
||||
throw new Error(`Unsupported quarter token "${raw}". Use FY26-Q4, Q4 FY26, or current-quarter.`);
|
||||
}
|
||||
|
||||
export function resolveWeekYearInput(value) {
|
||||
const raw = String(value || "").trim();
|
||||
if (!raw || CURRENT_FISCAL_YEAR_TOKENS.has(raw.toLowerCase())) {
|
||||
const today = new Date();
|
||||
const cycleStart = new Date(today.getFullYear(), ANNUAL_WEEK_START_MONTH_INDEX, ANNUAL_WEEK_START_DAY);
|
||||
return today >= cycleStart ? today.getFullYear() : today.getFullYear() - 1;
|
||||
}
|
||||
if (!/^\d{4}$/.test(raw)) {
|
||||
throw new Error(`Unsupported week year token "${raw}". Use the cycle start year YYYY, for example 2025.`);
|
||||
}
|
||||
return Number(raw);
|
||||
}
|
||||
|
||||
export function resolveAnnualWeekInput(value, weekYear) {
|
||||
const raw = String(value || "").trim().toLowerCase();
|
||||
if (CURRENT_PERIOD_TOKENS.has(raw)) {
|
||||
const year = resolveWeekYearInput(weekYear);
|
||||
const cycleStart = new Date(year, ANNUAL_WEEK_START_MONTH_INDEX, ANNUAL_WEEK_START_DAY);
|
||||
const cycleEnd = new Date(year + 1, ANNUAL_WEEK_START_MONTH_INDEX, 0);
|
||||
const today = new Date();
|
||||
const boundedToday =
|
||||
today < cycleStart ? cycleStart : today > cycleEnd ? cycleEnd : today;
|
||||
const week = Math.floor((boundedToday - cycleStart) / (7 * 24 * 60 * 60 * 1000)) + 1;
|
||||
return resolveAnnualWeekInput(`w${week}`, year);
|
||||
}
|
||||
const match = raw.match(/^(?:w|week\s*)?([1-9]|[1-4]\d|5[0-3])$/);
|
||||
if (!match) {
|
||||
throw new Error(`Unsupported week token "${value}". Use 1..53, w1..w53, or week 1..week 53.`);
|
||||
}
|
||||
const week = Number(match[1]);
|
||||
const year = resolveWeekYearInput(weekYear);
|
||||
const cycleStart = new Date(year, ANNUAL_WEEK_START_MONTH_INDEX, ANNUAL_WEEK_START_DAY);
|
||||
const cycleEnd = new Date(year + 1, ANNUAL_WEEK_START_MONTH_INDEX, 0);
|
||||
const weekStart = addDays(cycleStart, (week - 1) * 7);
|
||||
if (weekStart > cycleEnd) {
|
||||
throw new Error(`Week ${week} is outside the ${year}-${year + 1} cycle. Use 1..53.`);
|
||||
}
|
||||
const provisionalWeekEnd = addDays(weekStart, 6);
|
||||
const weekEnd = provisionalWeekEnd > cycleEnd ? cycleEnd : provisionalWeekEnd;
|
||||
return {
|
||||
input: String(value || "").trim(),
|
||||
week,
|
||||
weekLabel: `W${week}`,
|
||||
year,
|
||||
fromDate: formatIsoDate(weekStart),
|
||||
toDate: formatIsoDate(weekEnd),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveDateWindow(args, defaults = { from: "today", to: "+7d" }) {
|
||||
const hasSingleDate = Boolean(args.date);
|
||||
const hasMonth = Boolean(args.month);
|
||||
const hasWeek = Boolean(args.week);
|
||||
const hasQuarter = Boolean(args.quarter);
|
||||
const hasExplicitWindow = Boolean(args["from-date"] || args["to-date"]);
|
||||
if (
|
||||
[hasSingleDate, hasMonth, hasWeek, hasQuarter].filter(Boolean).length > 1 ||
|
||||
((hasSingleDate || hasMonth || hasWeek || hasQuarter) && hasExplicitWindow)
|
||||
) {
|
||||
throw new Error(
|
||||
"Use either --date, --month, --quarter, --week, or the pair --from-date/--to-date for date-window queries."
|
||||
);
|
||||
}
|
||||
if (hasSingleDate) {
|
||||
const exact = resolveDateInput(args.date);
|
||||
return {
|
||||
fromDateInput: exact.input,
|
||||
toDateInput: exact.input,
|
||||
fromDate: exact.isoDate,
|
||||
toDate: exact.isoDate,
|
||||
fromDateSql: `DATE ${quoteLiteral(exact.isoDate)}`,
|
||||
toDateSql: `DATE ${quoteLiteral(exact.isoDate)}`,
|
||||
preset: "single-date",
|
||||
};
|
||||
}
|
||||
if (hasMonth) {
|
||||
const month = resolveMonthInput(args.month);
|
||||
return {
|
||||
fromDateInput: month.input,
|
||||
toDateInput: month.input,
|
||||
fromDate: month.fromDate,
|
||||
toDate: month.toDate,
|
||||
fromDateSql: `DATE ${quoteLiteral(month.fromDate)}`,
|
||||
toDateSql: `DATE ${quoteLiteral(month.toDate)}`,
|
||||
preset: "calendar-month",
|
||||
};
|
||||
}
|
||||
if (hasQuarter) {
|
||||
const quarter = resolveQuarterInput(args.quarter);
|
||||
return {
|
||||
fromDateInput: quarter.input,
|
||||
toDateInput: quarter.input,
|
||||
fromDate: quarter.fromDate,
|
||||
toDate: quarter.toDate,
|
||||
fromDateSql: `DATE ${quoteLiteral(quarter.fromDate)}`,
|
||||
toDateSql: `DATE ${quoteLiteral(quarter.toDate)}`,
|
||||
preset: "fiscal-quarter",
|
||||
fiscalYear: quarter.fiscalYear,
|
||||
fiscalQuarter: quarter.quarter,
|
||||
};
|
||||
}
|
||||
if (hasWeek) {
|
||||
const week = resolveAnnualWeekInput(args.week, args["week-year"]);
|
||||
return {
|
||||
fromDateInput: week.input,
|
||||
toDateInput: week.input,
|
||||
fromDate: week.fromDate,
|
||||
toDate: week.toDate,
|
||||
fromDateSql: `DATE ${quoteLiteral(week.fromDate)}`,
|
||||
toDateSql: `DATE ${quoteLiteral(week.toDate)}`,
|
||||
preset: "annual-week",
|
||||
week: week.week,
|
||||
weekLabel: week.weekLabel,
|
||||
weekYear: week.year,
|
||||
};
|
||||
}
|
||||
const from = resolveDateInput(args["from-date"], defaults.from);
|
||||
const to = resolveDateInput(args["to-date"], defaults.to);
|
||||
if (from.isoDate > to.isoDate) {
|
||||
throw new Error(`Invalid date window: from-date ${from.isoDate} is after to-date ${to.isoDate}.`);
|
||||
}
|
||||
return {
|
||||
fromDateInput: from.input,
|
||||
toDateInput: to.input,
|
||||
fromDate: from.isoDate,
|
||||
toDate: to.isoDate,
|
||||
fromDateSql: `DATE ${quoteLiteral(from.isoDate)}`,
|
||||
toDateSql: `DATE ${quoteLiteral(to.isoDate)}`,
|
||||
preset: "date-window",
|
||||
};
|
||||
}
|
||||
|
||||
export function hasAnyDateWindowArgs(args) {
|
||||
return Boolean(
|
||||
args.date ||
|
||||
args.month ||
|
||||
args.quarter ||
|
||||
args.week ||
|
||||
args["from-date"] ||
|
||||
args["to-date"]
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user