Skip to content

Commit

Permalink
[IMP] autofill: support date autofill
Browse files Browse the repository at this point in the history
closes #5049

Task: 4173602
X-original-commit: a006d0b
Signed-off-by: Lucas Lefèvre (lul) <[email protected]>
  • Loading branch information
rrahir committed Oct 1, 2024
1 parent 5bdd38b commit ae9ec2a
Show file tree
Hide file tree
Showing 4 changed files with 379 additions and 13 deletions.
26 changes: 25 additions & 1 deletion src/registries/autofill_modifiers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { toJsDate } from "../functions/helpers";
import { jsDateToNumber } from "../helpers";
import { evaluateLiteral } from "../helpers/cells";
import { formatValue } from "../helpers/format/format";
import {
Expand All @@ -10,7 +12,7 @@ import {
IncrementModifier,
LiteralCell,
} from "../types/index";
import { AlphanumericIncrementModifier } from "./../types/autofill";
import { AlphanumericIncrementModifier, DateIncrementModifier } from "./../types/autofill";
import { Registry } from "./registry";

/**
Expand Down Expand Up @@ -55,6 +57,28 @@ autofillModifiersRegistry
};
},
})
.add("DATE_INCREMENT_MODIFIER", {
apply: (rule: DateIncrementModifier, data: AutofillData, getters: Getters) => {
const date = toJsDate(rule.current, getters.getLocale());
date.setFullYear(date.getFullYear() + rule.increment.years || 0);
date.setMonth(date.getMonth() + rule.increment.months || 0);
date.setDate(date.getDate() + rule.increment.days || 0);

const value = jsDateToNumber(date);
rule.current = value;
const locale = getters.getLocale();
const tooltipValue = formatValue(value, { format: data.cell?.format, locale });
return {
cellData: {
border: data.border,
style: data.cell && data.cell.style,
format: data.cell && data.cell.format,
content: value.toString(),
},
tooltip: value ? { props: { content: tooltipValue } } : undefined,
};
},
})
.add("COPY_MODIFIER", {
apply: (rule: CopyModifier, data: AutofillData, getters: Getters) => {
const content = data.cell?.content || "";
Expand Down
179 changes: 176 additions & 3 deletions src/registries/autofill_rules.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { isDateTimeFormat } from "../helpers";
import { toJsDate } from "../functions/helpers";
import {
DateTime,
getTimeDifferenceInWholeDays,
getTimeDifferenceInWholeMonths,
getTimeDifferenceInWholeYears,
isDateTimeFormat,
} from "../helpers";
import { evaluateLiteral } from "../helpers/cells";
import { AutofillModifier, Cell, CellValueType, DEFAULT_LOCALE } from "../types/index";
import { EvaluatedCell, LiteralCell } from "./../types/cells";
Expand All @@ -17,6 +24,12 @@ export interface AutofillRule {
sequence: number;
}

export interface CalendarDateInterval {
years: number;
months: number;
days: number;
}

export const autofillRulesRegistry = new Registry<AutofillRule>();

const numberPostfixRegExp = /(\d+)$/;
Expand All @@ -39,7 +52,9 @@ function getGroup(
found = true;
}
const cellValue =
x === undefined || x.isFormula ? undefined : evaluateLiteral(x, { locale: DEFAULT_LOCALE });
x === undefined || x.isFormula
? undefined
: evaluateLiteral(x, { locale: DEFAULT_LOCALE, format: x.format });
if (cellValue && filter(cellValue)) {
group.push(cellValue);
} else {
Expand Down Expand Up @@ -77,6 +92,86 @@ function calculateIncrementBasedOnGroup(group: number[]) {
return increment;
}

/**
* Iterates on a list of date intervals.
* if every interval is the same, return the interval
* Otherwise return undefined
*
*/
function getEqualInterval(intervals: CalendarDateInterval[]): CalendarDateInterval | undefined {
if (intervals.length < 2) {
return intervals[0] || { years: 0, months: 0, days: 0 };
}

const equal = intervals.every(
(interval) =>
interval.years === intervals[0].years &&
interval.months === intervals[0].months &&
interval.days === intervals[0].days
);
return equal ? intervals[0] : undefined;
}

/**
* Based on a group of dates, calculate the increment that should be applied
* to the next date.
*
* This will compute the date difference in calendar terms (years, months, days)
* In order to make abstraction of leap years and months with different number of days.
*
* In case the dates are not equidistant in calendar terms, no rule can be extrapolated
* In case of equidistant dates, we either have in that order:
* - exact date interval (e.g. +n year OR +n month OR +n day) in which case we increment by the same interval
* - exact day interval (e.g. +n days) in which case we increment by the same day interval
* - equidistant dates but not the same interval, in which case we return increment of the same interval
*
* */
function calculateDateIncrementBasedOnGroup(group: number[]) {
if (group.length < 2) {
return 1;
}

const jsDates = group.map((date) => toJsDate(date, DEFAULT_LOCALE));

const datesIntervals = getDateIntervals(jsDates);

const datesEquidistantInterval = getEqualInterval(datesIntervals);
if (datesEquidistantInterval === undefined) {
// dates are not equidistant in terms of years, months or days, thus no rule can be extrapolated
return undefined;
}

// The dates are apart by an exact interval of years, months or days
// but not a combination of them
const exactDateInterval =
Object.values(datesEquidistantInterval).filter((value) => value !== 0).length === 1;
const isSameDay = Object.values(datesEquidistantInterval).every((el) => el === 0); // handles time values (strict decimals)

if (!exactDateInterval || isSameDay) {
const timeIntervals = jsDates
.map((date, index) => {
if (index === 0) {
return 0;
}
const previous = jsDates[index - 1];
const days = Math.floor(date.getTime()) - Math.floor(previous.getTime());
return days;
})
.slice(1);
const equidistantDates = timeIntervals.every((interval) => interval === timeIntervals[0]);

if (equidistantDates) {
return group.length * (group[1] - group[0]);
}
}

return {
years: datesEquidistantInterval.years * group.length,
months: datesEquidistantInterval.months * group.length,
days: datesEquidistantInterval.days * group.length,
};
}

autofillRulesRegistry
.add("simple_value_copy", {
condition: (cell: Cell, cells: (Cell | undefined)[]) => {
Expand Down Expand Up @@ -134,6 +229,47 @@ autofillRulesRegistry
},
sequence: 30,
})
.add("increment_dates", {
condition: (cell: Cell, cells: (Cell | undefined)[]) => {
return (
!cell.isFormula &&
evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number &&
!!cell.format &&
isDateTimeFormat(cell.format)
);
},
generateRule: (cell: LiteralCell, cells: (Cell | undefined)[]) => {
const group = getGroup(
cell,
cells,
(evaluatedCell) =>
evaluatedCell.type === CellValueType.number &&
!!evaluatedCell.format &&
isDateTimeFormat(evaluatedCell.format)
).map((cell) => Number(cell.value));
const increment = calculateDateIncrementBasedOnGroup(group);
if (increment === undefined) {
return { type: "COPY_MODIFIER" };
}
/** requires to detect the current date (requires to be an integer value with the right format)
* detect if year or if month or if day then extrapolate increment required (+1 month, +1 year + 1 day)
*/
const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
if (typeof increment === "object") {
return {
type: "DATE_INCREMENT_MODIFIER",
increment,
current: evaluation.type === CellValueType.number ? evaluation.value : 0,
};
}
return {
type: "INCREMENT_MODIFIER",
increment,
current: evaluation.type === CellValueType.number ? evaluation.value : 0,
};
},
sequence: 25,
})
.add("increment_number", {
condition: (cell: Cell) =>
!cell.isFormula &&
Expand All @@ -142,7 +278,9 @@ autofillRulesRegistry
const group = getGroup(
cell,
cells,
(evaluatedCell) => evaluatedCell.type === CellValueType.number
(evaluatedCell) =>
evaluatedCell.type === CellValueType.number &&
!isDateTimeFormat(evaluatedCell.format || "")
).map((cell) => Number(cell.value));
const increment = calculateIncrementBasedOnGroup(group);
const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
Expand All @@ -154,3 +292,38 @@ autofillRulesRegistry
},
sequence: 40,
});

/**
* Returns the date intervals between consecutive dates of an array
* in the format of { years: number, months: number, days: number }
*
* The split is necessary to make abstraction of leap years and
* months with different number of days.
*
* @param dates
*/
function getDateIntervals(dates: DateTime[]): CalendarDateInterval[] {
if (dates.length < 2) {
return [{ years: 0, months: 0, days: 0 }];
}

const res = dates.map((date, index) => {
if (index === 0) {
return { years: 0, months: 0, days: 0 };
}
const previous = DateTime.fromTimestamp(dates[index - 1].getTime());
const years = getTimeDifferenceInWholeYears(previous, date);
const months = getTimeDifferenceInWholeMonths(previous, date) % 12;
previous.setFullYear(previous.getFullYear() + years);
previous.setMonth(previous.getMonth() + months);

const days = getTimeDifferenceInWholeDays(previous, date);

return {
years,
months,
days,
};
});
return res.slice(1);
}
13 changes: 12 additions & 1 deletion src/types/autofill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ export interface AlphanumericIncrementModifier {
numberPostfixLength: number; // the length of the number post fix string, e.g. "0001" is four but "1" is one
}

export interface DateIncrementModifier {
type: "DATE_INCREMENT_MODIFIER";
current: number;
increment: {
years: number;
months: number;
days: number;
};
}

export interface CopyModifier {
type: "COPY_MODIFIER";
}
Expand All @@ -40,7 +50,8 @@ export type AutofillModifier =
| IncrementModifier
| AlphanumericIncrementModifier
| CopyModifier
| FormulaModifier;
| FormulaModifier
| DateIncrementModifier;

export interface Tooltip {
props: any;
Expand Down
Loading

0 comments on commit ae9ec2a

Please sign in to comment.