const TUESDAY = 2 const THURSDAY = 4 const WEEK = 7 // for easier mocking in tests var today = new Date() export default function NextTopic() { // don't put the nextTopic date in the staticly generated html // because it would be outdated rather quickly const isSSR = typeof window === 'undefined' if (isSSR) { test() return '' } return formatDateInfo(getNextTopicDate()) } // javascript dates are not nice function getNextTopicDate() { // first thursday and third tuesday in month const nextTopic = zeroizeTime(today) // first thursday if (calculatePriorWeekdays(nextTopic, THURSDAY) === 0) { addDays(nextTopic, getDaysUntilNext(THURSDAY, nextTopic)) return nextTopic } // third tuesday const priorTuesdays = calculatePriorWeekdays(nextTopic, TUESDAY) if (priorTuesdays <= 2) { addDays(nextTopic, getDaysUntilNext(TUESDAY, nextTopic)) addDays(nextTopic, WEEK * (2 - priorTuesdays)) return nextTopic } // first thursday next month const currentMonth = today.getMonth() addDays(nextTopic, getDaysUntilNext(THURSDAY, nextTopic)) while (nextTopic.getMonth() === currentMonth) { addDays(nextTopic, WEEK) } return nextTopic } /** * calculate how many of the given weekday this month already had. * for example: how many tuesdays were in this month already */ function calculatePriorWeekdays(date, weekday) { const testDate = new Date(date) testDate.setDate(1) var priorWeekdays = 0 while (testDate < date) { if (testDate.getDay() === weekday) { priorWeekdays++ } testDate.setDate(testDate.getDate() + 1) } return priorWeekdays } /** * how many days are there until the next starting from */ function getDaysUntilNext(weekday, date) { return mod(weekday - date.getDay(), WEEK) } /** * just the modulo function, but always return the positive result */ function mod(n, m) { return ((n % m) + m) % m } /** * add days to the * but do it in a way that ignores daylight savings time */ function addDays(date, days) { date.setDate(date.getDate() + days) if (date.getHours() > 12) { date.setDate(date.getDate() + 1) } else if (date.getHours() !== 0) { date.setDate(date.getDate() - 1) } } /** * return a human readable representation of the date */ function formatDateInfo(date) { const dayNames = { 2: 'Dienstag', 4: 'Donnerstag', } const dayName = dayNames[date.getDay()] const isoDate = getISODateString(date) const weeks = weeksBetween(today, date) if (weeks === 0 && date.getDay() === today.getDay()) { return `Heute, ${isoDate}` } else if (weeks === 0) { return `Diese Woche ${dayName}, ${isoDate}` } else if (weeks === 1) { return `Nächste Woche ${dayName}, ${isoDate}` } else { return `${dayName} in ${weeks} Wochen, ${isoDate}` } } /** * how many sunday to monday transitions are between the two dates */ function weeksBetween(datetime1, datetime2) { const date1 = zeroizeTime(datetime1) const date2 = zeroizeTime(datetime2) const MILLISECONDS_IN_WEEK = 7 * 24 * 60 * 60 * 1000 var weeks = Math.floor((date2 - date1) / MILLISECONDS_IN_WEEK) // if there is a sunday to monday transition between if (mod(date1.getDay() - 1, WEEK) > mod(date2.getDay() - 1, WEEK)) { weeks += 1 } return weeks } function getISODateString(date) { const year = date.getFullYear() const month = date.getMonth() + 1 const day = date.getDate() const monthPadded = (month < 10 ? '0' : '') + month const dayPadded = (day < 10 ? '0' : '') + day return `${year}-${monthPadded}-${dayPadded}` } function zeroizeTime(date) { const copy = new Date(date) copy.setHours(0) copy.setMinutes(0) copy.setSeconds(0) copy.setMilliseconds(0) return copy } // test, because this is complicated function test() { testLateSunday() testYear2020() // reset to correct value today = new Date() } function testLateSunday() { today = new Date('2020-01-19T23:59:59+01:00') const result = formatDateInfo(getNextTopicDate()) console.assert( result === 'Nächste Woche Dienstag, 2020-01-21', `starting at ${getISODateString( today )}: was ${result}, expected "Nächste Woche Dienstag, 2020-01-21"` ) } function testYear2020() { const topicsIn2020 = [ '2020-01-02', '2020-01-21', '2020-02-06', '2020-02-18', '2020-03-05', '2020-03-17', '2020-04-02', '2020-04-21', '2020-05-07', '2020-05-19', '2020-06-04', '2020-06-16', '2020-07-02', '2020-07-21', '2020-08-06', '2020-08-18', '2020-09-03', '2020-09-15', '2020-10-01', '2020-10-20', '2020-11-05', '2020-11-17', '2020-12-03', '2020-12-15', ] today = zeroizeTime(new Date('2020-01-01')) let currentIndex = 0 while (today <= new Date('2020-12-15')) { const result = getISODateString(getNextTopicDate()) const expect = topicsIn2020[currentIndex] console.assert( result === expect, `starting at ${getISODateString( today )}: was ${result}, expected ${expect}` ) if (getISODateString(today) === result) { currentIndex++ } addDays(today, 1) } }