A little better but still broken. I need easier tasks for a while.
This commit is contained in:
1 parent
02442f5135
commit
8caaf33cb9
4 files changed
+345
-216
No files matched your search
@@ -51,6 +51,18 @@ function fromLocalString(dateString) {
|
||||
return new Date(year, month - 1, day)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Monday of the ISO week for a given date
|
||||
* @param {Date} date - The date to get the Monday for
|
||||
* @returns {Date} Date object representing the Monday of the ISO week
|
||||
*/
|
||||
function getMondayOfISOWeek(date) {
|
||||
const d = new Date(date)
|
||||
const dayOfWeek = (d.getDay() + 6) % 7 // Convert to Monday=0, Sunday=6
|
||||
d.setDate(d.getDate() - dayOfWeek)
|
||||
return d
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the index of Monday for a given date (0-6, where Monday = 0)
|
||||
* @param {Date} d - The date
|
||||
@@ -59,6 +71,259 @@ function fromLocalString(dateString) {
|
||||
const mondayIndex = (d) => (d.getDay() + 6) % 7
|
||||
|
||||
/**
|
||||
* Calculate the occurrence index for a repeating weekly event on a specific date
|
||||
* @param {Object} event - The event object with repeat info
|
||||
* @param {string} dateStr - The date string (YYYY-MM-DD) to check
|
||||
* @returns {number|null} The occurrence index (0=base, 1=first repeat) or null if not valid
|
||||
*/
|
||||
function getWeeklyOccurrenceIndex(event, dateStr) {
|
||||
if (!event.isRepeating || event.repeat !== 'weeks') return null
|
||||
|
||||
const pattern = event.repeatWeekdays || []
|
||||
if (!pattern.some(Boolean)) return null
|
||||
|
||||
const d = fromLocalString(dateStr)
|
||||
const dow = d.getDay()
|
||||
if (!pattern[dow]) return null
|
||||
|
||||
const baseStart = fromLocalString(event.startDate)
|
||||
const interval = event.repeatInterval || 1
|
||||
|
||||
// Check if date resides in a week block that aligns with interval
|
||||
const baseBlockStart = getMondayOfISOWeek(baseStart)
|
||||
const currentBlockStart = getMondayOfISOWeek(d)
|
||||
const WEEK_MS = 7 * 86400000
|
||||
const blocksDiff = Math.floor((currentBlockStart - baseBlockStart) / WEEK_MS)
|
||||
|
||||
if (blocksDiff < 0 || blocksDiff % interval !== 0) return null
|
||||
|
||||
// For same week as base start, count from base start to target
|
||||
if (currentBlockStart.getTime() === baseBlockStart.getTime()) {
|
||||
let occurrenceIndex = 0
|
||||
const cursor = new Date(baseStart)
|
||||
while (cursor < d) {
|
||||
if (pattern[cursor.getDay()]) occurrenceIndex++
|
||||
cursor.setDate(cursor.getDate() + 1)
|
||||
}
|
||||
|
||||
// Check against repeat count limit
|
||||
if (event.repeatCount !== 'unlimited') {
|
||||
const limit = parseInt(event.repeatCount, 10)
|
||||
if (isNaN(limit) || occurrenceIndex >= limit) return null
|
||||
}
|
||||
|
||||
return occurrenceIndex
|
||||
}
|
||||
|
||||
// For different weeks, calculate based on complete intervals
|
||||
const weekdaysPerInterval = pattern.filter(Boolean).length
|
||||
const completeIntervals = blocksDiff / interval
|
||||
let occurrenceIndex = completeIntervals * weekdaysPerInterval
|
||||
|
||||
// Add occurrences from the current week up to the target date
|
||||
const cursor = new Date(currentBlockStart)
|
||||
while (cursor < d) {
|
||||
if (pattern[cursor.getDay()]) occurrenceIndex++
|
||||
cursor.setDate(cursor.getDate() + 1)
|
||||
}
|
||||
|
||||
// Check against repeat count limit
|
||||
if (event.repeatCount !== 'unlimited') {
|
||||
const limit = parseInt(event.repeatCount, 10)
|
||||
if (isNaN(limit) || occurrenceIndex >= limit) return null
|
||||
}
|
||||
|
||||
return occurrenceIndex
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the occurrence index for a repeating monthly event on a specific date
|
||||
* @param {Object} event - The event object with repeat info
|
||||
* @param {string} dateStr - The date string (YYYY-MM-DD) to check
|
||||
* @returns {number|null} The occurrence index (0=base, 1=first repeat) or null if not valid
|
||||
*/
|
||||
function getMonthlyOccurrenceIndex(event, dateStr) {
|
||||
if (!event.isRepeating || event.repeat !== 'months') return null
|
||||
|
||||
const baseStart = fromLocalString(event.startDate)
|
||||
const d = fromLocalString(dateStr)
|
||||
const diffMonths =
|
||||
(d.getFullYear() - baseStart.getFullYear()) * 12 + (d.getMonth() - baseStart.getMonth())
|
||||
|
||||
if (diffMonths < 0) return null
|
||||
|
||||
const interval = event.repeatInterval || 1
|
||||
if (diffMonths % interval !== 0) return null
|
||||
|
||||
// Check day match (clamped for shorter months)
|
||||
const baseDay = baseStart.getDate()
|
||||
const daysInMonth = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate()
|
||||
const effectiveDay = Math.min(baseDay, daysInMonth)
|
||||
if (d.getDate() !== effectiveDay) return null
|
||||
|
||||
const occurrenceIndex = diffMonths / interval
|
||||
|
||||
// Check against repeat count limit
|
||||
if (event.repeatCount !== 'unlimited') {
|
||||
const limit = parseInt(event.repeatCount, 10)
|
||||
if (isNaN(limit) || occurrenceIndex >= limit) return null
|
||||
}
|
||||
|
||||
return occurrenceIndex
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a repeating event occurs on a specific date and return occurrence index
|
||||
* @param {Object} event - The event object with repeat info
|
||||
* @param {string} dateStr - The date string (YYYY-MM-DD) to check
|
||||
* @returns {number|null} The occurrence index (0=base, 1=first repeat) or null if not occurring
|
||||
*/
|
||||
function getOccurrenceIndex(event, dateStr) {
|
||||
if (!event || !event.isRepeating || event.repeat === 'none') return null
|
||||
if (dateStr < event.startDate) return null
|
||||
|
||||
if (event.repeat === 'weeks') {
|
||||
return getWeeklyOccurrenceIndex(event, dateStr)
|
||||
} else if (event.repeat === 'months') {
|
||||
return getMonthlyOccurrenceIndex(event, dateStr)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the end date for a virtual occurrence of a repeating event
|
||||
* @param {Object} event - The base event object
|
||||
* @param {string} occurrenceStartDate - The start date of the occurrence (YYYY-MM-DD)
|
||||
* @returns {string} The end date of the occurrence (YYYY-MM-DD)
|
||||
*/
|
||||
function getVirtualOccurrenceEndDate(event, occurrenceStartDate) {
|
||||
const baseStart = fromLocalString(event.startDate)
|
||||
const baseEnd = fromLocalString(event.endDate)
|
||||
const spanDays = Math.max(0, Math.round((baseEnd - baseStart) / (24 * 60 * 60 * 1000)))
|
||||
|
||||
const occurrenceStart = fromLocalString(occurrenceStartDate)
|
||||
const occurrenceEnd = new Date(occurrenceStart)
|
||||
occurrenceEnd.setDate(occurrenceEnd.getDate() + spanDays)
|
||||
|
||||
return toLocalString(occurrenceEnd)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a repeating event occurs on or spans through a specific date
|
||||
* @param {Object} event - The event object with repeat info
|
||||
* @param {string} dateStr - The date string (YYYY-MM-DD) to check
|
||||
* @returns {boolean} True if the event occurs on or spans through the date
|
||||
*/
|
||||
function occursOnOrSpansDate(event, dateStr) {
|
||||
if (!event || !event.isRepeating || event.repeat === 'none') return false
|
||||
|
||||
// Check if this is the base event spanning naturally
|
||||
if (dateStr >= event.startDate && dateStr <= event.endDate) return true
|
||||
|
||||
// For virtual occurrences, we need to check if any occurrence spans through this date
|
||||
const baseStart = fromLocalString(event.startDate)
|
||||
const baseEnd = fromLocalString(event.endDate)
|
||||
const spanDays = Math.max(0, Math.round((baseEnd - baseStart) / (24 * 60 * 60 * 1000)))
|
||||
|
||||
if (spanDays === 0) {
|
||||
// Single day event - just check if it occurs on this date
|
||||
return getOccurrenceIndex(event, dateStr) !== null
|
||||
}
|
||||
|
||||
// Multi-day event - check if any occurrence's span includes this date
|
||||
const targetDate = fromLocalString(dateStr)
|
||||
|
||||
if (event.repeat === 'weeks') {
|
||||
const pattern = event.repeatWeekdays || []
|
||||
if (!pattern.some(Boolean)) return false
|
||||
|
||||
const interval = event.repeatInterval || 1
|
||||
const baseBlockStart = getMondayOfISOWeek(baseStart)
|
||||
const WEEK_MS = 7 * 86400000
|
||||
|
||||
// Check a reasonable range of weeks around the target date
|
||||
for (
|
||||
let weekOffset = -Math.ceil(spanDays / 7) - 1;
|
||||
weekOffset <= Math.ceil(spanDays / 7) + 1;
|
||||
weekOffset++
|
||||
) {
|
||||
const weekStart = new Date(baseBlockStart)
|
||||
weekStart.setDate(weekStart.getDate() + weekOffset * 7)
|
||||
|
||||
// Check if this week aligns with the interval
|
||||
const blocksDiff = Math.floor((weekStart - baseBlockStart) / WEEK_MS)
|
||||
if (blocksDiff < 0 || blocksDiff % interval !== 0) continue
|
||||
|
||||
// Check each day in this week
|
||||
for (let day = 0; day < 7; day++) {
|
||||
const candidateStart = new Date(weekStart)
|
||||
candidateStart.setDate(candidateStart.getDate() + day)
|
||||
|
||||
// Skip if before base start
|
||||
if (candidateStart < baseStart) continue
|
||||
|
||||
// Check if this day matches the pattern
|
||||
if (!pattern[candidateStart.getDay()]) continue
|
||||
|
||||
// Check repeat count limit
|
||||
const occIndex = getWeeklyOccurrenceIndex(event, toLocalString(candidateStart))
|
||||
if (occIndex === null) continue
|
||||
|
||||
// Calculate end date for this occurrence
|
||||
const candidateEnd = new Date(candidateStart)
|
||||
candidateEnd.setDate(candidateEnd.getDate() + spanDays)
|
||||
|
||||
// Check if target date falls within this occurrence's span
|
||||
if (targetDate >= candidateStart && targetDate <= candidateEnd) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (event.repeat === 'months') {
|
||||
const interval = event.repeatInterval || 1
|
||||
const baseDay = baseStart.getDate()
|
||||
|
||||
// Check a reasonable range of months around the target date
|
||||
const targetYear = targetDate.getFullYear()
|
||||
const targetMonth = targetDate.getMonth()
|
||||
const baseYear = baseStart.getFullYear()
|
||||
const baseMonth = baseStart.getMonth()
|
||||
|
||||
for (let monthOffset = -spanDays * 2; monthOffset <= spanDays * 2; monthOffset++) {
|
||||
const candidateYear = baseYear + Math.floor((baseMonth + monthOffset) / 12)
|
||||
const candidateMonth = (baseMonth + monthOffset + 12) % 12
|
||||
|
||||
// Check if this month aligns with the interval
|
||||
const diffMonths = (candidateYear - baseYear) * 12 + (candidateMonth - baseMonth)
|
||||
if (diffMonths < 0 || diffMonths % interval !== 0) continue
|
||||
|
||||
// Calculate the actual day (clamped for shorter months)
|
||||
const daysInMonth = new Date(candidateYear, candidateMonth + 1, 0).getDate()
|
||||
const effectiveDay = Math.min(baseDay, daysInMonth)
|
||||
|
||||
const candidateStart = new Date(candidateYear, candidateMonth, effectiveDay)
|
||||
|
||||
// Skip if before base start
|
||||
if (candidateStart < baseStart) continue
|
||||
|
||||
// Check repeat count limit
|
||||
const occIndex = getMonthlyOccurrenceIndex(event, toLocalString(candidateStart))
|
||||
if (occIndex === null) continue
|
||||
|
||||
// Calculate end date for this occurrence
|
||||
const candidateEnd = new Date(candidateStart)
|
||||
candidateEnd.setDate(candidateEnd.getDate() + spanDays)
|
||||
|
||||
// Check if target date falls within this occurrence's span
|
||||
if (targetDate >= candidateStart && targetDate <= candidateEnd) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
} /**
|
||||
* Pad a number with leading zeros to make it 2 digits
|
||||
* @param {number} n - Number to pad
|
||||
* @returns {string} Padded string
|
||||
@@ -207,6 +472,12 @@ export {
|
||||
isoWeekInfo,
|
||||
toLocalString,
|
||||
fromLocalString,
|
||||
getMondayOfISOWeek,
|
||||
getWeeklyOccurrenceIndex,
|
||||
getMonthlyOccurrenceIndex,
|
||||
getOccurrenceIndex,
|
||||
getVirtualOccurrenceEndDate,
|
||||
occursOnOrSpansDate,
|
||||
mondayIndex,
|
||||
pad,
|
||||
daysInclusive,
|
||||
|
||||
Reference in new issue
Block a user