Skip to content

Commit 394a22b

Browse files
authored
Merge pull request #27 from DumbWareio/Fix-EOM-Recurring-Event-Handling
Fixed issue #25 where recurring transactions set for the end of a month (like the 31st) weren't correctly calculating the appropriate date in shorter months. Changes: Improved the monthly recurring transaction logic to always use the last day of the month when the original transaction was on the last day of its month Fixed date rollover issues that caused transactions to appear on incorrect dates (e.g., Jan 31 -> Mar 3 instead of Feb 28/29) Added special handling for months with fewer days by using a safe approach that avoids JavaScript's automatic date adjustments Fixed edge cases for all months regardless of their length (30/31 days) The solution now correctly handles all cases: Jan 31 -> Feb 28/29 -> Mar 31 -> Apr 30 -> May 31 Apr 30 -> May 31 -> Jun 30 And preserves the "last day of month" intent when appropriate
2 parents a0c8436 + 6a0ef55 commit 394a22b

File tree

1 file changed

+52
-2
lines changed

1 file changed

+52
-2
lines changed

server.js

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -635,14 +635,64 @@ function generateRecurringInstances(transaction, startDate, endDate) {
635635
currentDate.setDate(currentDate.getDate() + (pattern.interval * 7));
636636
break;
637637
case 'month':
638-
currentDate.setMonth(currentDate.getMonth() + pattern.interval);
638+
// Don't modify the current date yet
639+
// Check if we need to handle end-of-month special case
640+
const originalDay = parseInt(transaction.date.split('-')[2]);
641+
const daysInCurrentMonth = new Date(
642+
currentDate.getFullYear(),
643+
currentDate.getMonth() + 1,
644+
0
645+
).getDate();
646+
647+
// Check if the original transaction was on the last day of its month
648+
const isLastDayOfMonth = originalDay >= daysInCurrentMonth;
649+
650+
// Get the number of days in the target month (after adding interval)
651+
const daysInTargetMonth = new Date(
652+
currentDate.getFullYear(),
653+
currentDate.getMonth() + pattern.interval + 1,
654+
0
655+
).getDate();
656+
657+
// First, create a new date for next month with a safe day value (1)
658+
const nextMonth = new Date(currentDate);
659+
nextMonth.setDate(1); // Set to first of month to avoid rollover
660+
nextMonth.setMonth(nextMonth.getMonth() + pattern.interval);
661+
662+
if (isLastDayOfMonth) {
663+
// If original was last day of month, set to last day of target month
664+
nextMonth.setDate(daysInTargetMonth);
665+
} else if (originalDay > daysInTargetMonth) {
666+
// If original day doesn't exist in target month, use last day
667+
nextMonth.setDate(daysInTargetMonth);
668+
} else {
669+
// Otherwise use the same day of month
670+
nextMonth.setDate(originalDay);
671+
}
672+
673+
// Update current date with our safely constructed date
674+
currentDate = nextMonth;
639675
break;
640676
case 'year':
641677
currentDate.setFullYear(currentDate.getFullYear() + pattern.interval);
642678
break;
643679
case 'monthday':
644-
// Move to the next month, keeping the same day of month
680+
// For monthday pattern, move to next month then set the day
645681
currentDate.setMonth(currentDate.getMonth() + 1);
682+
683+
// Get the last day of the target month
684+
const lastDayOfMonth = new Date(
685+
currentDate.getFullYear(),
686+
currentDate.getMonth() + 1,
687+
0
688+
).getDate();
689+
690+
// If the desired day exceeds the last day, use the last day instead
691+
if (pattern.dayOfMonth > lastDayOfMonth) {
692+
currentDate.setDate(lastDayOfMonth);
693+
} else {
694+
currentDate.setDate(pattern.dayOfMonth);
695+
}
646696
break;
647697
}
648698
}

0 commit comments

Comments
 (0)