mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-21 02:20:12 +00:00
fix(tasks): correct biweekly recurring task week boundary calculation (#6298)
getDiffInWeeks used Math.round(ms / weekMs) which rounded Friday (day 4 of 7) into the next week. Switch to day-first calculation with Math.floor(days / 7) to respect week boundaries while preserving DST safety via Math.round on the day count.
This commit is contained in:
parent
4eb06b3db6
commit
875341f04a
3 changed files with 103 additions and 4 deletions
|
|
@ -335,6 +335,42 @@ describe('getNewestPossibleDueDate()', () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe('Biweekly with weekdays (#6298)', () => {
|
||||
// Issue #6298: "Every 2 weeks Mon-Fri" starting Monday Feb 2, 2026
|
||||
const START_DATE = new Date(2026, 1, 2); // Monday Feb 2, 2026
|
||||
|
||||
const biweeklyCfg = (lastCreationDay: string): TaskRepeatCfg =>
|
||||
dummyRepeatable('BIWEEKLY', {
|
||||
repeatCycle: 'WEEKLY',
|
||||
repeatEvery: 2,
|
||||
monday: true,
|
||||
tuesday: true,
|
||||
wednesday: true,
|
||||
thursday: true,
|
||||
friday: true,
|
||||
lastTaskCreationDay: lastCreationDay,
|
||||
});
|
||||
|
||||
it('should find Friday of week 0 when searching backward from that Friday', () => {
|
||||
// Friday Feb 6 is in week 0 (active) — backward search should find it
|
||||
const cfg = biweeklyCfg('1970-01-01');
|
||||
testCase(cfg, new Date(2026, 1, 6), START_DATE, new Date(2026, 1, 6)); // Fri Feb 6
|
||||
});
|
||||
|
||||
it('should NOT find Friday of week 1 (off-week)', () => {
|
||||
// Friday Feb 13 is in week 1 (off) — backward search should find Thu Feb 5 at most
|
||||
// but since last creation is already Feb 5, it returns null
|
||||
const cfg = biweeklyCfg(getDbDateStr(new Date(2026, 1, 6)));
|
||||
testCase(cfg, new Date(2026, 1, 13), START_DATE, null);
|
||||
});
|
||||
|
||||
it('should find Monday of week 2 when today is that Monday', () => {
|
||||
// Monday Feb 16 is in week 2 (active)
|
||||
const cfg = biweeklyCfg(getDbDateStr(new Date(2026, 1, 6)));
|
||||
testCase(cfg, new Date(2026, 1, 16), START_DATE, new Date(2026, 1, 16));
|
||||
});
|
||||
});
|
||||
|
||||
describe('MONTHLY', () => {
|
||||
const testCases = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -717,6 +717,69 @@ describe('getNextRepeatOccurrence()', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Biweekly with weekdays (#6298)', () => {
|
||||
// Issue #6298: "Every 2 weeks Mon-Fri" starting Monday Feb 2, 2026
|
||||
// Friday was incorrectly shifted to the next week due to Math.round in getDiffInWeeks
|
||||
const START_DATE = new Date(2026, 1, 2); // Monday Feb 2, 2026
|
||||
|
||||
const biweeklyCfg = (lastCreationDay: Date): TaskRepeatCfg =>
|
||||
dummyRepeatable('BIWEEKLY', {
|
||||
repeatCycle: 'WEEKLY',
|
||||
repeatEvery: 2,
|
||||
monday: true,
|
||||
tuesday: true,
|
||||
wednesday: true,
|
||||
thursday: true,
|
||||
friday: true,
|
||||
lastTaskCreationDay: getDbDateStr(lastCreationDay),
|
||||
});
|
||||
|
||||
it('should include Friday of week 0 (same week as start)', () => {
|
||||
// After Thursday Feb 5, next should be Friday Feb 6 (still week 0)
|
||||
const cfg = biweeklyCfg(new Date(2026, 1, 5)); // Thu Feb 5
|
||||
testCase(cfg, new Date(2026, 1, 5), START_DATE, new Date(2026, 1, 6)); // Fri Feb 6
|
||||
});
|
||||
|
||||
it('should NOT include any day in week 1 (off-week)', () => {
|
||||
// After Friday Feb 6 (end of week 0), next should jump to Monday Feb 16 (week 2)
|
||||
const cfg = biweeklyCfg(new Date(2026, 1, 6)); // Fri Feb 6
|
||||
testCase(cfg, new Date(2026, 1, 6), START_DATE, new Date(2026, 1, 16)); // Mon Feb 16
|
||||
});
|
||||
|
||||
it('should produce correct 4-week schedule from Monday start', () => {
|
||||
// Week 0 (active): Mon Feb 2 - Fri Feb 6
|
||||
// Week 1 (off): no tasks
|
||||
// Week 2 (active): Mon Feb 16 - Fri Feb 20
|
||||
// Week 3 (off): no tasks
|
||||
|
||||
// Starting from the start date, walk through the schedule
|
||||
let cfg = biweeklyCfg(new Date(2026, 0, 1)); // before start
|
||||
testCase(cfg, new Date(2026, 1, 1), START_DATE, new Date(2026, 1, 2)); // Mon Feb 2
|
||||
|
||||
cfg = biweeklyCfg(new Date(2026, 1, 2));
|
||||
testCase(cfg, new Date(2026, 1, 2), START_DATE, new Date(2026, 1, 3)); // Tue Feb 3
|
||||
|
||||
cfg = biweeklyCfg(new Date(2026, 1, 3));
|
||||
testCase(cfg, new Date(2026, 1, 3), START_DATE, new Date(2026, 1, 4)); // Wed Feb 4
|
||||
|
||||
cfg = biweeklyCfg(new Date(2026, 1, 4));
|
||||
testCase(cfg, new Date(2026, 1, 4), START_DATE, new Date(2026, 1, 5)); // Thu Feb 5
|
||||
|
||||
cfg = biweeklyCfg(new Date(2026, 1, 5));
|
||||
testCase(cfg, new Date(2026, 1, 5), START_DATE, new Date(2026, 1, 6)); // Fri Feb 6
|
||||
|
||||
// Skip week 1 entirely, jump to week 2
|
||||
cfg = biweeklyCfg(new Date(2026, 1, 6));
|
||||
testCase(cfg, new Date(2026, 1, 6), START_DATE, new Date(2026, 1, 16)); // Mon Feb 16
|
||||
|
||||
cfg = biweeklyCfg(new Date(2026, 1, 16));
|
||||
testCase(cfg, new Date(2026, 1, 16), START_DATE, new Date(2026, 1, 17)); // Tue Feb 17
|
||||
|
||||
cfg = biweeklyCfg(new Date(2026, 1, 20));
|
||||
testCase(cfg, new Date(2026, 1, 20), START_DATE, new Date(2026, 2, 2)); // Mon Mar 2 (week 4)
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should return null for unknown repeat cycle', () => {
|
||||
const cfg = dummyRepeatable('ID1', {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ export const getDiffInWeeks = (d1: Date, d2: Date): number => {
|
|||
// NOTE we want the diff regarding the dates not the absolute one
|
||||
d1Copy.setHours(0, 0, 0, 0);
|
||||
d2Copy.setHours(0, 0, 0, 0);
|
||||
let diff = (d2Copy.getTime() - d1Copy.getTime()) / 1000;
|
||||
diff /= 60 * 60 * 24 * 7;
|
||||
// NOTE: we use math round to avoid JS math weirdness and summer winter time problems
|
||||
return Math.round(diff);
|
||||
const diffMs = d2Copy.getTime() - d1Copy.getTime();
|
||||
// Round days to handle DST (167h vs 168h), then floor weeks to respect week boundaries
|
||||
const diffInDays = Math.round(diffMs / (1000 * 60 * 60 * 24));
|
||||
return Math.floor(diffInDays / 7);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue