This commit is contained in:
parent
a86eee52ce
commit
7fed715d6b
2 changed files with 66 additions and 27 deletions
|
|
@ -1,11 +1,12 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { describeRecurrence } from './recurrenceDescription'
|
||||
|
||||
// Reference: 2026-04-13 local time = Monday, 2nd Monday of April 2026.
|
||||
const tuesdayFirstOfMonth = new Date(2026, 3, 7, 18, 30).toISOString() // 2026-04-07 is a Tuesday, 1st of month
|
||||
const monday2ndOfMonth = new Date(2026, 3, 13, 18, 30).toISOString() // 2026-04-13
|
||||
const sunday13thOfMonth = new Date(2026, 8, 13, 10, 0).toISOString() // 2026-09-13 is a Sunday
|
||||
const monday5thOfMonth = new Date(2026, 2, 30, 20, 0).toISOString() // 2026-03-30
|
||||
// All test dates fall inside EU DST (Mar 29 – Oct 25, 2026), so Berlin is UTC+2.
|
||||
// We spell the UTC instant explicitly so tests are timezone-independent.
|
||||
const tuesdayFirstOfMonth = '2026-04-07T16:30:00.000Z' // Tue 2026-04-07 18:30 Berlin
|
||||
const monday2ndOfMonth = '2026-04-13T16:30:00.000Z' // Mon 2026-04-13 18:30 Berlin
|
||||
const sunday13thOfMonth = '2026-09-13T08:00:00.000Z' // Sun 2026-09-13 10:00 Berlin
|
||||
const monday5thOfMonth = '2026-03-30T18:00:00.000Z' // Mon 2026-03-30 20:00 Berlin
|
||||
|
||||
describe('describeRecurrence', () => {
|
||||
it('returns undefined for none / missing recurrenceType', () => {
|
||||
|
|
@ -14,7 +15,7 @@ describe('describeRecurrence', () => {
|
|||
})
|
||||
|
||||
it('describes daily with time from event.date', () => {
|
||||
const dateAt10 = new Date(2026, 3, 13, 10, 0).toISOString()
|
||||
const dateAt10 = '2026-04-13T08:00:00.000Z' // 10:00 Berlin
|
||||
expect(describeRecurrence({ recurrenceType: 'daily', date: dateAt10 })).toBe(
|
||||
'Täglich um 10:00 Uhr',
|
||||
)
|
||||
|
|
@ -85,7 +86,7 @@ describe('describeRecurrence', () => {
|
|||
})
|
||||
|
||||
it('pads minutes correctly (08:05, not 8:5)', () => {
|
||||
const dateEarly = new Date(2026, 3, 13, 8, 5).toISOString()
|
||||
const dateEarly = '2026-04-13T06:05:00.000Z' // 08:05 Berlin
|
||||
expect(describeRecurrence({ recurrenceType: 'daily', date: dateEarly })).toBe(
|
||||
'Täglich um 08:05 Uhr',
|
||||
)
|
||||
|
|
|
|||
|
|
@ -28,18 +28,55 @@ const WEEK_OF_MONTH_LABEL: Record<string, string> = {
|
|||
last: 'letzten',
|
||||
}
|
||||
|
||||
// Match the clamp in eventRecurrence.ts so the label on the Event page
|
||||
// agrees with the rule the generator actually produces.
|
||||
const weekOfMonthN = (d: Date): number => Math.min(Math.ceil(d.getDate() / 7), 4)
|
||||
const BERLIN_TZ = 'Europe/Berlin'
|
||||
|
||||
const formatTime = (d: Date): string => {
|
||||
const hh = String(d.getHours()).padStart(2, '0')
|
||||
const mm = String(d.getMinutes()).padStart(2, '0')
|
||||
return `${hh}:${mm}`
|
||||
const WEEKDAY_EN_TO_INDEX: Record<string, number> = {
|
||||
Sunday: 0,
|
||||
Monday: 1,
|
||||
Tuesday: 2,
|
||||
Wednesday: 3,
|
||||
Thursday: 4,
|
||||
Friday: 5,
|
||||
Saturday: 6,
|
||||
}
|
||||
|
||||
const withTime = (description: string, d: Date | null): string =>
|
||||
d ? `${description} um ${formatTime(d)} Uhr` : description
|
||||
const berlinFormatter = new Intl.DateTimeFormat('en-GB', {
|
||||
timeZone: BERLIN_TZ,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
day: 'numeric',
|
||||
weekday: 'long',
|
||||
hourCycle: 'h23',
|
||||
})
|
||||
|
||||
type BerlinParts = {
|
||||
hour: string
|
||||
minute: string
|
||||
day: number
|
||||
weekdayIndex: number
|
||||
}
|
||||
|
||||
const getBerlinParts = (d: Date): BerlinParts => {
|
||||
const parts = berlinFormatter.formatToParts(d)
|
||||
const get = (type: string) =>
|
||||
parts.find((p) => p.type === type)?.value ?? ''
|
||||
return {
|
||||
hour: get('hour'),
|
||||
minute: get('minute'),
|
||||
day: Number(get('day')),
|
||||
weekdayIndex: WEEKDAY_EN_TO_INDEX[get('weekday')] ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Match the clamp in eventRecurrence.ts so the label on the Event page
|
||||
// agrees with the rule the generator actually produces.
|
||||
const weekOfMonthN = (dayOfMonth: number): number =>
|
||||
Math.min(Math.ceil(dayOfMonth / 7), 4)
|
||||
|
||||
const formatTime = (p: BerlinParts): string => `${p.hour}:${p.minute}`
|
||||
|
||||
const withTime = (description: string, p: BerlinParts | null): string =>
|
||||
p ? `${description} um ${formatTime(p)} Uhr` : description
|
||||
|
||||
type RecurrenceRule = NonNullable<Event['recurrenceRules']>[number]
|
||||
|
||||
|
|
@ -78,33 +115,34 @@ export const describeRecurrence = (
|
|||
|
||||
const parsed = event.date ? new Date(event.date) : null
|
||||
const d = parsed && !Number.isNaN(parsed.getTime()) ? parsed : null
|
||||
const p = d ? getBerlinParts(d) : null
|
||||
|
||||
switch (type) {
|
||||
case 'daily':
|
||||
return withTime('Täglich', d)
|
||||
return withTime('Täglich', p)
|
||||
case 'weekly':
|
||||
return withTime(d ? `Jeden ${WEEKDAYS_DE[d.getDay()]}` : 'Wöchentlich', d)
|
||||
return withTime(p ? `Jeden ${WEEKDAYS_DE[p.weekdayIndex]}` : 'Wöchentlich', p)
|
||||
case 'biweekly':
|
||||
return withTime(
|
||||
d ? `Alle 2 Wochen, ${WEEKDAYS_DE[d.getDay()]}` : 'Alle 2 Wochen',
|
||||
d,
|
||||
p ? `Alle 2 Wochen, ${WEEKDAYS_DE[p.weekdayIndex]}` : 'Alle 2 Wochen',
|
||||
p,
|
||||
)
|
||||
case 'monthlyByDate':
|
||||
return withTime(d ? `Monatlich am ${d.getDate()}.` : 'Monatlich', d)
|
||||
return withTime(p ? `Monatlich am ${p.day}.` : 'Monatlich', p)
|
||||
case 'monthlyByWeekday':
|
||||
return withTime(
|
||||
d
|
||||
? `Monatlich am ${weekOfMonthN(d)}. ${WEEKDAYS_DE[d.getDay()]}`
|
||||
p
|
||||
? `Monatlich am ${weekOfMonthN(p.day)}. ${WEEKDAYS_DE[p.weekdayIndex]}`
|
||||
: 'Monatlich',
|
||||
d,
|
||||
p,
|
||||
)
|
||||
case 'custom': {
|
||||
const rules = event.recurrenceRules ?? []
|
||||
const parts = rules
|
||||
.map(describeCustomRule)
|
||||
.filter((p): p is string => Boolean(p))
|
||||
if (parts.length === 0) return withTime('Benutzerdefiniert', d)
|
||||
return withTime(parts.join(' · '), d)
|
||||
.filter((part): part is string => Boolean(part))
|
||||
if (parts.length === 0) return withTime('Benutzerdefiniert', p)
|
||||
return withTime(parts.join(' · '), p)
|
||||
}
|
||||
default:
|
||||
return undefined
|
||||
|
|
|
|||
Loading…
Reference in a new issue