fix: 5th week day of the month
Some checks are pending
Deploy / deploy (push) Waiting to run

This commit is contained in:
Benno Tielen 2026-08-28 15:03:46 +02:00
parent e139b35018
commit 925c5a66a6
9 changed files with 27734 additions and 6 deletions

View file

@ -133,13 +133,14 @@ export const Churches: CollectionConfig = {
{ label: '2.', value: 'second' }, { label: '2.', value: 'second' },
{ label: '3.', value: 'third' }, { label: '3.', value: 'third' },
{ label: '4.', value: 'fourth' }, { label: '4.', value: 'fourth' },
{ label: '5.', value: 'fifth' },
{ label: 'Letzter', value: 'last' }, { label: 'Letzter', value: 'last' },
], ],
admin: { admin: {
condition: (_data, siblingData) => condition: (_data, siblingData) =>
siblingData?.frequency === 'monthlyByWeekday', siblingData?.frequency === 'monthlyByWeekday',
description: description:
'z. B. „3.“ + „Sonntag“ = jeden 3. Sonntag im Monat', 'z. B. „3.“ + „Sonntag“ = jeden 3. Sonntag im Monat.',
}, },
}, },
{ {

View file

@ -325,6 +325,7 @@ export const Events: CollectionConfig = {
{ label: '2.', value: 'second' }, { label: '2.', value: 'second' },
{ label: '3.', value: 'third' }, { label: '3.', value: 'third' },
{ label: '4.', value: 'fourth' }, { label: '4.', value: 'fourth' },
{ label: '5.', value: 'fifth' },
{ label: 'Letzter', value: 'last' }, { label: 'Letzter', value: 'last' },
], ],
admin: { admin: {

View file

@ -142,6 +142,37 @@ describe('generateOccurrenceDates monthlyByWeekday', () => {
expect(result.map((x) => x.getDate())).toEqual([23]) expect(result.map((x) => x.getDate())).toEqual([23])
}) })
it('yields the 5th thursday only in months that have one', () => {
const entry: ScheduleEntry = {
frequency: 'monthlyByWeekday',
day: 'thursday',
weekOfMonth: 'fifth',
}
// 2026: Jul has 5 Thursdays (2,9,16,23,30), Aug 4 (6,13,20,27), Sep 4, Oct 5 (1,8,15,22,29), Nov 4, Dec 5 (3,10,17,24,31)
const result = generateOccurrenceDates(entry, d(2026, 7, 1), d(2026, 12, 31))
expect(result.map((x) => `${x.getMonth() + 1}-${x.getDate()}`)).toEqual([
'7-30',
'10-29',
'12-31',
])
})
it('"fifth" and "last" differ in months with only 4 occurrences', () => {
// Aug 2026 Thursdays: 6, 13, 20, 27 — last = 27, fifth = none
const last = generateOccurrenceDates(
{ frequency: 'monthlyByWeekday', day: 'thursday', weekOfMonth: 'last' },
d(2026, 8, 1),
d(2026, 8, 31),
)
const fifth = generateOccurrenceDates(
{ frequency: 'monthlyByWeekday', day: 'thursday', weekOfMonth: 'fifth' },
d(2026, 8, 1),
d(2026, 8, 31),
)
expect(last.map((x) => x.getDate())).toEqual([27])
expect(fifth).toEqual([])
})
it('works when the start-of-window is after that month\'s occurrence', () => { it('works when the start-of-window is after that month\'s occurrence', () => {
// 3rd Sunday of April 2026 is 4/19. Start the window on 4/20. // 3rd Sunday of April 2026 is 4/19. Start the window on 4/20.
// April should be skipped; May should still appear. // April should be skipped; May should still appear.

View file

@ -14,7 +14,7 @@ export type ScheduleFrequency =
| 'monthlyByDate' | 'monthlyByDate'
| 'monthlyByWeekday' | 'monthlyByWeekday'
export type WeekOfMonth = 'first' | 'second' | 'third' | 'fourth' | 'last' export type WeekOfMonth = 'first' | 'second' | 'third' | 'fourth' | 'fifth' | 'last'
export interface ScheduleEntry { export interface ScheduleEntry {
frequency: ScheduleFrequency frequency: ScheduleFrequency
@ -40,6 +40,7 @@ const WEEK_OF_MONTH_N: Record<WeekOfMonth, number> = {
second: 2, second: 2,
third: 3, third: 3,
fourth: 4, fourth: 4,
fifth: 5,
last: -1, last: -1,
} }
@ -65,7 +66,7 @@ const firstOccurrenceOnOrAfter = (from: Date, targetDay: number): Date => {
/** /**
* For a given (year, month), return the date of the Nth occurrence of * For a given (year, month), return the date of the Nth occurrence of
* `targetDay`. `n` is 1..4 for first..fourth, or -1 for "last". * `targetDay`. `n` is 1..5 for first..fifth, or -1 for "last".
* Returns null if the month has no such occurrence (e.g. asking for the * Returns null if the month has no such occurrence (e.g. asking for the
* 5th Monday in a month that only has 4). * 5th Monday in a month that only has 4).
*/ */

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,27 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
ALTER TYPE "public"."enum_church_recurring_schedule_week_of_month" ADD VALUE 'fifth' BEFORE 'last';
ALTER TYPE "public"."enum_event_recurrence_rules_week_of_month" ADD VALUE 'fifth' BEFORE 'last';
ALTER TYPE "public"."enum__event_v_version_recurrence_rules_week_of_month" ADD VALUE 'fifth' BEFORE 'last';`)
}
export async function down({ db }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
UPDATE "church_recurring_schedule" SET "week_of_month" = 'last' WHERE "week_of_month" = 'fifth';
UPDATE "event_recurrence_rules" SET "week_of_month" = 'last' WHERE "week_of_month" = 'fifth';
UPDATE "_event_v_version_recurrence_rules" SET "week_of_month" = 'last' WHERE "week_of_month" = 'fifth';
ALTER TABLE "church_recurring_schedule" ALTER COLUMN "week_of_month" SET DATA TYPE text;
DROP TYPE "public"."enum_church_recurring_schedule_week_of_month";
CREATE TYPE "public"."enum_church_recurring_schedule_week_of_month" AS ENUM('first', 'second', 'third', 'fourth', 'last');
ALTER TABLE "church_recurring_schedule" ALTER COLUMN "week_of_month" SET DATA TYPE "public"."enum_church_recurring_schedule_week_of_month" USING "week_of_month"::"public"."enum_church_recurring_schedule_week_of_month";
ALTER TABLE "event_recurrence_rules" ALTER COLUMN "week_of_month" SET DATA TYPE text;
DROP TYPE "public"."enum_event_recurrence_rules_week_of_month";
CREATE TYPE "public"."enum_event_recurrence_rules_week_of_month" AS ENUM('first', 'second', 'third', 'fourth', 'last');
ALTER TABLE "event_recurrence_rules" ALTER COLUMN "week_of_month" SET DATA TYPE "public"."enum_event_recurrence_rules_week_of_month" USING "week_of_month"::"public"."enum_event_recurrence_rules_week_of_month";
ALTER TABLE "_event_v_version_recurrence_rules" ALTER COLUMN "week_of_month" SET DATA TYPE text;
DROP TYPE "public"."enum__event_v_version_recurrence_rules_week_of_month";
CREATE TYPE "public"."enum__event_v_version_recurrence_rules_week_of_month" AS ENUM('first', 'second', 'third', 'fourth', 'last');
ALTER TABLE "_event_v_version_recurrence_rules" ALTER COLUMN "week_of_month" SET DATA TYPE "public"."enum__event_v_version_recurrence_rules_week_of_month" USING "week_of_month"::"public"."enum__event_v_version_recurrence_rules_week_of_month";`)
}

View file

@ -54,6 +54,7 @@ import * as migration_20260611_084920_add_image_with_text_block from './20260611
import * as migration_20260716_090032_add_user_parish_page_assignments from './20260716_090032_add_user_parish_page_assignments'; import * as migration_20260716_090032_add_user_parish_page_assignments from './20260716_090032_add_user_parish_page_assignments';
import * as migration_20260716_113205_add_blog_pinned from './20260716_113205_add_blog_pinned'; import * as migration_20260716_113205_add_blog_pinned from './20260716_113205_add_blog_pinned';
import * as migration_20260721_090435_add_mass_times_range from './20260721_090435_add_mass_times_range'; import * as migration_20260721_090435_add_mass_times_range from './20260721_090435_add_mass_times_range';
import * as migration_20260828_100000_add_fifth_week_of_month from './20260828_100000_add_fifth_week_of_month';
export const migrations = [ export const migrations = [
{ {
@ -336,4 +337,9 @@ export const migrations = [
down: migration_20260721_090435_add_mass_times_range.down, down: migration_20260721_090435_add_mass_times_range.down,
name: '20260721_090435_add_mass_times_range' name: '20260721_090435_add_mass_times_range'
}, },
{
up: migration_20260828_100000_add_fifth_week_of_month.up,
down: migration_20260828_100000_add_fifth_week_of_month.down,
name: '20260828_100000_add_fifth_week_of_month',
},
]; ];

View file

@ -368,9 +368,9 @@ export interface Church {
day: 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday'; day: 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday';
time: string; time: string;
/** /**
* z. B. 3. + Sonntag = jeden 3. Sonntag im Monat * z. B. 3. + Sonntag = jeden 3. Sonntag im Monat.
*/ */
weekOfMonth?: ('first' | 'second' | 'third' | 'fourth' | 'last') | null; weekOfMonth?: ('first' | 'second' | 'third' | 'fourth' | 'fifth' | 'last') | null;
/** /**
* Ein Datum, an dem dieser Termin stattfindet. Davon ausgehend wird im 2-Wochen-Rhythmus weitergerechnet. * Ein Datum, an dem dieser Termin stattfindet. Davon ausgehend wird im 2-Wochen-Rhythmus weitergerechnet.
*/ */
@ -1231,7 +1231,7 @@ export interface Event {
| { | {
frequency: 'daily' | 'weekly' | 'monthlyByDate' | 'monthlyByWeekday'; frequency: 'daily' | 'weekly' | 'monthlyByDate' | 'monthlyByWeekday';
weekday?: ('monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday') | null; weekday?: ('monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday') | null;
weekOfMonth?: ('first' | 'second' | 'third' | 'fourth' | 'last') | null; weekOfMonth?: ('first' | 'second' | 'third' | 'fourth' | 'fifth' | 'last') | null;
dayOfMonth?: number | null; dayOfMonth?: number | null;
id?: string | null; id?: string | null;
}[] }[]

View file

@ -25,6 +25,7 @@ const WEEK_OF_MONTH_LABEL: Record<string, string> = {
second: '2.', second: '2.',
third: '3.', third: '3.',
fourth: '4.', fourth: '4.',
fifth: '5.',
last: 'letzten', last: 'letzten',
} }