Add in natural language time period input and tweak layout for financial overview
This commit is contained in:
File diff suppressed because it is too large
Load Diff
384
inventory/src/utils/naturalLanguagePeriod.ts
Normal file
384
inventory/src/utils/naturalLanguagePeriod.ts
Normal file
@@ -0,0 +1,384 @@
|
||||
export type MonthPeriod = {
|
||||
type: "month";
|
||||
startYear: number;
|
||||
startMonth: number;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export type QuarterPeriod = {
|
||||
type: "quarter";
|
||||
startYear: number;
|
||||
startQuarter: number;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export type YearPeriod = {
|
||||
type: "year";
|
||||
startYear: number;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export type CustomPeriod = MonthPeriod | QuarterPeriod | YearPeriod;
|
||||
|
||||
export type NaturalLanguagePeriodResult = CustomPeriod | "last30days" | null;
|
||||
|
||||
const MONTH_NAMES = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
] as const;
|
||||
|
||||
const QUARTER_NAMES = ["Q1", "Q2", "Q3", "Q4"] as const;
|
||||
|
||||
const MONTH_MAP: Record<string, number> = {
|
||||
jan: 0,
|
||||
january: 0,
|
||||
feb: 1,
|
||||
february: 1,
|
||||
mar: 2,
|
||||
march: 2,
|
||||
apr: 3,
|
||||
april: 3,
|
||||
may: 4,
|
||||
jun: 5,
|
||||
june: 5,
|
||||
jul: 6,
|
||||
july: 6,
|
||||
aug: 7,
|
||||
august: 7,
|
||||
sep: 8,
|
||||
sept: 8,
|
||||
september: 8,
|
||||
oct: 9,
|
||||
october: 9,
|
||||
nov: 10,
|
||||
november: 10,
|
||||
dec: 11,
|
||||
december: 11,
|
||||
};
|
||||
|
||||
const stripDelimiters = (value: string) => value.toLowerCase().trim().replace(/[,]/g, "");
|
||||
|
||||
const parseYear = (yearStr: string): number => {
|
||||
const year = parseInt(yearStr, 10);
|
||||
if (yearStr.length === 2) {
|
||||
if (year <= 30) {
|
||||
return 2000 + year;
|
||||
}
|
||||
return 1900 + year;
|
||||
}
|
||||
return year;
|
||||
};
|
||||
|
||||
const findMonth = (monthStr: string): number | null => {
|
||||
const month = MONTH_MAP[monthStr.toLowerCase()];
|
||||
return month ?? null;
|
||||
};
|
||||
|
||||
export const parseNaturalLanguagePeriod = (
|
||||
input: string,
|
||||
referenceDate: Date = new Date()
|
||||
): NaturalLanguagePeriodResult => {
|
||||
const lower = stripDelimiters(input);
|
||||
const now = referenceDate;
|
||||
const currentYear = now.getFullYear();
|
||||
const currentMonth = now.getMonth();
|
||||
const currentQuarter = Math.floor(currentMonth / 3);
|
||||
|
||||
if (/^(last\s*)?30\s*days?$/i.test(lower)) {
|
||||
return "last30days";
|
||||
}
|
||||
|
||||
if (/^this\s+month$/i.test(lower)) {
|
||||
return {
|
||||
type: "month",
|
||||
startYear: currentYear,
|
||||
startMonth: currentMonth,
|
||||
count: 1,
|
||||
};
|
||||
}
|
||||
|
||||
if (/^last\s+month$/i.test(lower)) {
|
||||
const lastMonth = currentMonth === 0 ? 11 : currentMonth - 1;
|
||||
const lastMonthYear = currentMonth === 0 ? currentYear - 1 : currentYear;
|
||||
return {
|
||||
type: "month",
|
||||
startYear: lastMonthYear,
|
||||
startMonth: lastMonth,
|
||||
count: 1,
|
||||
};
|
||||
}
|
||||
|
||||
if (/^this\s+quarter$/i.test(lower)) {
|
||||
return {
|
||||
type: "quarter",
|
||||
startYear: currentYear,
|
||||
startQuarter: currentQuarter,
|
||||
count: 1,
|
||||
};
|
||||
}
|
||||
|
||||
if (/^last\s+quarter$/i.test(lower)) {
|
||||
const lastQuarter = currentQuarter === 0 ? 3 : currentQuarter - 1;
|
||||
const lastQuarterYear = currentQuarter === 0 ? currentYear - 1 : currentYear;
|
||||
return {
|
||||
type: "quarter",
|
||||
startYear: lastQuarterYear,
|
||||
startQuarter: lastQuarter,
|
||||
count: 1,
|
||||
};
|
||||
}
|
||||
|
||||
if (/^this\s+year$/i.test(lower)) {
|
||||
return {
|
||||
type: "year",
|
||||
startYear: currentYear,
|
||||
count: 1,
|
||||
};
|
||||
}
|
||||
|
||||
if (/^last\s+year$/i.test(lower)) {
|
||||
return {
|
||||
type: "year",
|
||||
startYear: currentYear - 1,
|
||||
count: 1,
|
||||
};
|
||||
}
|
||||
|
||||
const lastMatch = lower.match(/^last\s+(\d+)\s*(months?|quarters?|years?|mos?|qtrs?|yrs?)$/i);
|
||||
if (lastMatch) {
|
||||
const count = parseInt(lastMatch[1], 10);
|
||||
const unit = lastMatch[2].toLowerCase();
|
||||
|
||||
let type: "month" | "quarter" | "year";
|
||||
if (/^(months?|mos?)$/i.test(unit)) {
|
||||
type = "month";
|
||||
} else if (/^(quarters?|qtrs?)$/i.test(unit)) {
|
||||
type = "quarter";
|
||||
} else {
|
||||
type = "year";
|
||||
}
|
||||
|
||||
if (type === "month") {
|
||||
const startMonth = currentMonth - count + 1;
|
||||
const startYear = currentYear + Math.floor(startMonth / 12);
|
||||
return {
|
||||
type: "month",
|
||||
startYear,
|
||||
startMonth: ((startMonth % 12) + 12) % 12,
|
||||
count,
|
||||
};
|
||||
}
|
||||
|
||||
if (type === "quarter") {
|
||||
const startQuarter = currentQuarter - count + 1;
|
||||
const startYear = currentYear + Math.floor(startQuarter / 4);
|
||||
return {
|
||||
type: "quarter",
|
||||
startYear,
|
||||
startQuarter: ((startQuarter % 4) + 4) % 4,
|
||||
count,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: "year",
|
||||
startYear: currentYear - count + 1,
|
||||
count,
|
||||
};
|
||||
}
|
||||
|
||||
const quarterMatch = lower.match(/^q([1-4])(?:\s+|-)(\d{2,4})$/);
|
||||
if (quarterMatch) {
|
||||
return {
|
||||
type: "quarter",
|
||||
startYear: parseYear(quarterMatch[2]),
|
||||
startQuarter: parseInt(quarterMatch[1], 10) - 1,
|
||||
count: 1,
|
||||
};
|
||||
}
|
||||
|
||||
const quarterRangeMatch = lower.match(/^q([1-4])\s*(?:-|–|—|to|through|until|thru)\s*q([1-4])(?:\s+|-)(\d{2,4})$/);
|
||||
if (quarterRangeMatch) {
|
||||
const startQ = parseInt(quarterRangeMatch[1], 10) - 1;
|
||||
const endQ = parseInt(quarterRangeMatch[2], 10) - 1;
|
||||
const year = parseYear(quarterRangeMatch[3]);
|
||||
|
||||
let count = endQ - startQ + 1;
|
||||
if (count <= 0) {
|
||||
count += 4;
|
||||
}
|
||||
|
||||
return {
|
||||
type: "quarter",
|
||||
startYear: year,
|
||||
startQuarter: startQ,
|
||||
count,
|
||||
};
|
||||
}
|
||||
|
||||
const quarterRangeAcrossYearsMatch = lower.match(
|
||||
/^q([1-4])(?:\s+|-)(\d{2,4})\s*(?:-|–|—|to|through|until|thru)\s*q([1-4])(?:\s+|-)(\d{2,4})$/
|
||||
);
|
||||
if (quarterRangeAcrossYearsMatch) {
|
||||
const startQ = parseInt(quarterRangeAcrossYearsMatch[1], 10) - 1;
|
||||
const startYear = parseYear(quarterRangeAcrossYearsMatch[2]);
|
||||
const endQ = parseInt(quarterRangeAcrossYearsMatch[3], 10) - 1;
|
||||
const endYear = parseYear(quarterRangeAcrossYearsMatch[4]);
|
||||
|
||||
const totalQuarters = (endYear - startYear) * 4 + (endQ - startQ) + 1;
|
||||
|
||||
if (totalQuarters > 0 && totalQuarters <= 20) {
|
||||
return {
|
||||
type: "quarter",
|
||||
startYear,
|
||||
startQuarter: startQ,
|
||||
count: totalQuarters,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const yearMatch = lower.match(/^(\d{2,4})$/);
|
||||
if (yearMatch) {
|
||||
return {
|
||||
type: "year",
|
||||
startYear: parseYear(yearMatch[1]),
|
||||
count: 1,
|
||||
};
|
||||
}
|
||||
|
||||
const yearRangeMatch = lower.match(/^(\d{2,4})\s*(?:-|–|—|to|through|until|thru)\s*(\d{2,4})$/);
|
||||
if (yearRangeMatch) {
|
||||
const startYear = parseYear(yearRangeMatch[1]);
|
||||
const endYear = parseYear(yearRangeMatch[2]);
|
||||
const count = endYear - startYear + 1;
|
||||
|
||||
if (count > 0 && count <= 10) {
|
||||
return {
|
||||
type: "year",
|
||||
startYear,
|
||||
count,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const singleMonthMatch = lower.match(/^([a-z]+)(?:\s+|-)(\d{2,4})$/);
|
||||
if (singleMonthMatch) {
|
||||
const monthIndex = findMonth(singleMonthMatch[1]);
|
||||
if (monthIndex !== null) {
|
||||
return {
|
||||
type: "month",
|
||||
startYear: parseYear(singleMonthMatch[2]),
|
||||
startMonth: monthIndex,
|
||||
count: 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const monthRangeWithYearMatch = lower.match(
|
||||
/^([a-z]+)\s*(?:-|–|—|to|through|until|thru)\s*([a-z]+)(?:\s+|-)(\d{2,4})$/
|
||||
);
|
||||
if (monthRangeWithYearMatch) {
|
||||
const startMonthIndex = findMonth(monthRangeWithYearMatch[1]);
|
||||
const endMonthIndex = findMonth(monthRangeWithYearMatch[2]);
|
||||
const year = parseYear(monthRangeWithYearMatch[3]);
|
||||
|
||||
if (startMonthIndex !== null && endMonthIndex !== null) {
|
||||
let count = endMonthIndex - startMonthIndex + 1;
|
||||
if (count <= 0) {
|
||||
count += 12;
|
||||
}
|
||||
|
||||
return {
|
||||
type: "month",
|
||||
startYear: year,
|
||||
startMonth: startMonthIndex,
|
||||
count,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const monthRangeAcrossYearsMatch = lower.match(
|
||||
/^([a-z]+)(?:\s+|-)(\d{2,4})\s*(?:-|–|—|to|through|until|thru)\s*([a-z]+)(?:\s+|-)(\d{2,4})$/
|
||||
);
|
||||
if (monthRangeAcrossYearsMatch) {
|
||||
const startMonthIndex = findMonth(monthRangeAcrossYearsMatch[1]);
|
||||
const startYear = parseYear(monthRangeAcrossYearsMatch[2]);
|
||||
const endMonthIndex = findMonth(monthRangeAcrossYearsMatch[3]);
|
||||
const endYear = parseYear(monthRangeAcrossYearsMatch[4]);
|
||||
|
||||
if (startMonthIndex !== null && endMonthIndex !== null) {
|
||||
const totalMonths = (endYear - startYear) * 12 + (endMonthIndex - startMonthIndex) + 1;
|
||||
|
||||
if (totalMonths > 0 && totalMonths <= 60) {
|
||||
return {
|
||||
type: "month",
|
||||
startYear,
|
||||
startMonth: startMonthIndex,
|
||||
count: totalMonths,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const generateNaturalLanguagePreview = (
|
||||
parsed: NaturalLanguagePeriodResult
|
||||
): string | null => {
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parsed === "last30days") {
|
||||
return "Last 30 days";
|
||||
}
|
||||
|
||||
if (parsed.type === "year") {
|
||||
if (parsed.count === 1) {
|
||||
return `${parsed.startYear}`;
|
||||
}
|
||||
const endYear = parsed.startYear + parsed.count - 1;
|
||||
return `${parsed.startYear} to ${endYear}`;
|
||||
}
|
||||
|
||||
if (parsed.type === "quarter") {
|
||||
if (parsed.count === 1) {
|
||||
return `${QUARTER_NAMES[parsed.startQuarter]} ${parsed.startYear}`;
|
||||
}
|
||||
const endQuarter = (parsed.startQuarter + parsed.count - 1) % 4;
|
||||
const endYear = parsed.startYear + Math.floor((parsed.startQuarter + parsed.count - 1) / 4);
|
||||
|
||||
if (parsed.startYear === endYear) {
|
||||
return `${QUARTER_NAMES[parsed.startQuarter]} to ${QUARTER_NAMES[endQuarter]} ${parsed.startYear}`;
|
||||
}
|
||||
|
||||
return `${QUARTER_NAMES[parsed.startQuarter]} ${parsed.startYear} to ${QUARTER_NAMES[endQuarter]} ${endYear}`;
|
||||
}
|
||||
|
||||
if (parsed.type === "month") {
|
||||
if (parsed.count === 1) {
|
||||
return `${MONTH_NAMES[parsed.startMonth]} ${parsed.startYear}`;
|
||||
}
|
||||
const endMonth = (parsed.startMonth + parsed.count - 1) % 12;
|
||||
const endYear = parsed.startYear + Math.floor((parsed.startMonth + parsed.count - 1) / 12);
|
||||
|
||||
if (parsed.startYear === endYear) {
|
||||
return `${MONTH_NAMES[parsed.startMonth]} to ${MONTH_NAMES[endMonth]} ${parsed.startYear}`;
|
||||
}
|
||||
|
||||
return `${MONTH_NAMES[parsed.startMonth]} ${parsed.startYear} to ${MONTH_NAMES[endMonth]} ${endYear}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
Reference in New Issue
Block a user