A little better but still broken. I need easier tasks for a while.
This commit is contained in:
parent
02442f5135
commit
8caaf33cb9
@ -13,6 +13,9 @@ import {
|
||||
daysInclusive,
|
||||
addDaysStr,
|
||||
formatDateRange,
|
||||
getMondayOfISOWeek,
|
||||
getOccurrenceIndex,
|
||||
getVirtualOccurrenceEndDate,
|
||||
} from '@/utils/date'
|
||||
import { toLocalString, fromLocalString } from '@/utils/date'
|
||||
|
||||
@ -153,25 +156,11 @@ function createWeek(virtualWeek) {
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const dateStr = toLocalString(cur)
|
||||
const storedEvents = []
|
||||
const idSet = calendarStore.dates.get(dateStr)
|
||||
if (idSet) {
|
||||
// Support Set or Array; ignore unexpected shapes
|
||||
if (idSet instanceof Set) {
|
||||
idSet.forEach((id) => {
|
||||
const ev = calendarStore.events.get(id)
|
||||
if (ev) storedEvents.push(ev)
|
||||
})
|
||||
} else if (Array.isArray(idSet)) {
|
||||
for (const id of idSet) {
|
||||
const ev = calendarStore.events.get(id)
|
||||
if (ev) storedEvents.push(ev)
|
||||
}
|
||||
} else if (typeof idSet === 'object' && idSet !== null) {
|
||||
// If mistakenly hydrated as plain object {id:true,...}
|
||||
for (const id of Object.keys(idSet)) {
|
||||
const ev = calendarStore.events.get(id)
|
||||
if (ev) storedEvents.push(ev)
|
||||
}
|
||||
|
||||
// Find all non-repeating events that occur on this date
|
||||
for (const ev of calendarStore.events.values()) {
|
||||
if (!ev.isRepeating && dateStr >= ev.startDate && dateStr <= ev.endDate) {
|
||||
storedEvents.push(ev)
|
||||
}
|
||||
}
|
||||
// Build day events starting with stored (base/spanning) then virtual occurrences
|
||||
@ -179,57 +168,46 @@ function createWeek(virtualWeek) {
|
||||
for (const base of repeatingBases) {
|
||||
// Skip if the base itself already on this date (already in storedEvents)
|
||||
if (dateStr >= base.startDate && dateStr <= base.endDate) continue
|
||||
if (calendarStore.occursOnDate(base, dateStr)) {
|
||||
// Determine sequential occurrence index: base event = 0, first repeat = 1, etc.
|
||||
let recurrenceIndex = 0
|
||||
try {
|
||||
if (base.repeat === 'weeks') {
|
||||
const pattern = base.repeatWeekdays || []
|
||||
const interval = base.repeatInterval || 1
|
||||
const baseStart = new Date(base.startDate + 'T00:00:00')
|
||||
const baseEnd = new Date(base.endDate + 'T00:00:00')
|
||||
const target = new Date(dateStr + 'T00:00:00')
|
||||
const WEEK_MS = 7 * 86400000
|
||||
const baseBlockStart = new Date(baseStart)
|
||||
baseBlockStart.setDate(baseStart.getDate() - baseStart.getDay())
|
||||
function isAligned(d) {
|
||||
const blk = new Date(d)
|
||||
blk.setDate(d.getDate() - d.getDay())
|
||||
const diff = Math.floor((blk - baseBlockStart) / WEEK_MS)
|
||||
return diff % interval === 0
|
||||
}
|
||||
// Count valid occurrences after base end and before target
|
||||
let count = 0
|
||||
const cursor = new Date(baseEnd)
|
||||
cursor.setDate(cursor.getDate() + 1)
|
||||
while (cursor < target) {
|
||||
if (pattern[cursor.getDay()] && isAligned(cursor)) count++
|
||||
cursor.setDate(cursor.getDate() + 1)
|
||||
}
|
||||
// Target itself is guaranteed valid (occursOnDate passed), so its index is count+1
|
||||
recurrenceIndex = count + 1
|
||||
} else if (base.repeat === 'months') {
|
||||
const baseStart = new Date(base.startDate + 'T00:00:00')
|
||||
const target = new Date(dateStr + 'T00:00:00')
|
||||
const interval = base.repeatInterval || 1
|
||||
const diffMonths =
|
||||
(target.getFullYear() - baseStart.getFullYear()) * 12 +
|
||||
(target.getMonth() - baseStart.getMonth())
|
||||
// diffMonths should be multiple of interval; sequential index = diffMonths/interval
|
||||
recurrenceIndex = diffMonths / interval
|
||||
}
|
||||
} catch {
|
||||
recurrenceIndex = 0
|
||||
}
|
||||
|
||||
// Check if any occurrence of this repeating event spans through this date
|
||||
const baseStart = fromLocalString(base.startDate)
|
||||
const baseEnd = fromLocalString(base.endDate)
|
||||
const spanDays = Math.max(0, Math.round((baseEnd - baseStart) / (24 * 60 * 60 * 1000)))
|
||||
const currentDate = fromLocalString(dateStr)
|
||||
|
||||
let occurrenceFound = false
|
||||
|
||||
// Check dates going backwards to find an occurrence that might span to this date
|
||||
for (let offset = 0; offset <= spanDays && !occurrenceFound; offset++) {
|
||||
const candidateStart = new Date(currentDate)
|
||||
candidateStart.setDate(candidateStart.getDate() - offset)
|
||||
const candidateStartStr = toLocalString(candidateStart)
|
||||
|
||||
const occurrenceIndex = getOccurrenceIndex(base, candidateStartStr)
|
||||
if (occurrenceIndex !== null) {
|
||||
// Calculate the end date of this occurrence
|
||||
const virtualEndDate = getVirtualOccurrenceEndDate(base, candidateStartStr)
|
||||
|
||||
// Check if this occurrence spans through the current date
|
||||
if (dateStr >= candidateStartStr && dateStr <= virtualEndDate) {
|
||||
// Create virtual occurrence (if not already created)
|
||||
const virtualId = base.id + '_v_' + candidateStartStr
|
||||
const alreadyExists = dayEvents.some((ev) => ev.id === virtualId)
|
||||
|
||||
if (!alreadyExists) {
|
||||
dayEvents.push({
|
||||
...base,
|
||||
id: base.id + '_v_' + dateStr,
|
||||
startDate: dateStr,
|
||||
endDate: dateStr,
|
||||
_recurrenceIndex: recurrenceIndex,
|
||||
id: virtualId,
|
||||
startDate: candidateStartStr,
|
||||
endDate: virtualEndDate,
|
||||
_recurrenceIndex: occurrenceIndex,
|
||||
_baseId: base.id,
|
||||
})
|
||||
}
|
||||
occurrenceFound = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const dow = cur.getDay()
|
||||
const isFirst = cur.getDate() === 1
|
||||
|
@ -3,7 +3,7 @@ import { useCalendarStore } from '@/stores/CalendarStore'
|
||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import WeekdaySelector from './WeekdaySelector.vue'
|
||||
import Numeric from './Numeric.vue'
|
||||
import { addDaysStr } from '@/utils/date'
|
||||
import { addDaysStr, getMondayOfISOWeek } from '@/utils/date'
|
||||
|
||||
const props = defineProps({
|
||||
selection: { type: Object, default: () => ({ startDate: null, dayCount: 0 }) },
|
||||
@ -173,11 +173,9 @@ function openEditDialog(payload) {
|
||||
// Count valid repeat occurrences (pattern + interval alignment) AFTER the base span
|
||||
const interval = event.repeatInterval || 1
|
||||
const WEEK_MS = 7 * 86400000
|
||||
const baseBlockStart = new Date(baseStart)
|
||||
baseBlockStart.setDate(baseStart.getDate() - baseStart.getDay())
|
||||
const baseBlockStart = getMondayOfISOWeek(baseStart)
|
||||
function isAligned(d) {
|
||||
const blk = new Date(d)
|
||||
blk.setDate(d.getDate() - d.getDay())
|
||||
const blk = getMondayOfISOWeek(d)
|
||||
const diff = Math.floor((blk - baseBlockStart) / WEEK_MS)
|
||||
return diff % interval === 0
|
||||
}
|
||||
|
@ -1,5 +1,15 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { toLocalString, fromLocalString, getLocaleWeekendDays } from '@/utils/date'
|
||||
import {
|
||||
toLocalString,
|
||||
fromLocalString,
|
||||
getLocaleWeekendDays,
|
||||
getMondayOfISOWeek,
|
||||
getOccurrenceIndex,
|
||||
getWeeklyOccurrenceIndex,
|
||||
getMonthlyOccurrenceIndex,
|
||||
getVirtualOccurrenceEndDate,
|
||||
occursOnOrSpansDate,
|
||||
} from '@/utils/date'
|
||||
|
||||
const MIN_YEAR = 1900
|
||||
const MAX_YEAR = 2100
|
||||
@ -9,7 +19,6 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
today: toLocalString(new Date()),
|
||||
now: new Date().toISOString(), // store as ISO string
|
||||
events: new Map(), // id -> event object (primary)
|
||||
dates: new Map(), // dateStr -> Set of event ids
|
||||
weekend: getLocaleWeekendDays(),
|
||||
config: {
|
||||
select_days: 1000,
|
||||
@ -28,74 +37,7 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
actions: {
|
||||
// Determine if a repeating event occurs on a specific date (YYYY-MM-DD) without iterating all occurrences.
|
||||
occursOnDate(event, dateStr) {
|
||||
if (!event || !event.isRepeating || event.repeat === 'none') return false
|
||||
// Quick bounds: event cannot occur before its base start
|
||||
if (dateStr < event.startDate) return false
|
||||
// For multi-day spanning events, we treat start date as anchor; UI handles span painting separately.
|
||||
if (event.repeat === 'weeks') {
|
||||
const pattern = event.repeatWeekdays || []
|
||||
if (!pattern.some(Boolean)) return false
|
||||
// Day of week must match
|
||||
const d = fromLocalString(dateStr)
|
||||
const dow = d.getDay()
|
||||
if (!pattern[dow]) return false
|
||||
// Compute week distance blocks respecting interval by counting ISO weeks since anchor Monday of base.
|
||||
const baseStart = fromLocalString(event.startDate)
|
||||
// If date is before base anchor weekday match, ensure anchor alignment
|
||||
// Count days since base start; ensure that number of matching weekdays encountered equals occurrence index < repeatCount
|
||||
// Optimized approach: approximate max occurrences cap first.
|
||||
const interval = event.repeatInterval || 1
|
||||
// Check if date resides in a week block that aligns with interval
|
||||
const baseBlockStart = new Date(baseStart)
|
||||
baseBlockStart.setDate(baseStart.getDate() - baseStart.getDay())
|
||||
const currentBlockStart = new Date(d)
|
||||
currentBlockStart.setDate(d.getDate() - d.getDay())
|
||||
const WEEK_MS = 7 * 86400000
|
||||
const blocksDiff = Math.floor((currentBlockStart - baseBlockStart) / WEEK_MS)
|
||||
if (blocksDiff < 0 || blocksDiff % interval !== 0) return false
|
||||
// Count occurrences up to this date to enforce repeatCount finite limits
|
||||
if (event.repeatCount !== 'unlimited') {
|
||||
const targetTime = d.getTime()
|
||||
let occs = 0
|
||||
const cursor = new Date(baseStart)
|
||||
const limit = parseInt(event.repeatCount, 10)
|
||||
const safetyLimit = Math.min(limit + 1, 10000)
|
||||
while (cursor.getTime() <= targetTime && occs < safetyLimit) {
|
||||
if (pattern[cursor.getDay()]) {
|
||||
if (cursor.getTime() === targetTime) {
|
||||
// This is the occurrence. Validate occs < limit
|
||||
return occs < limit
|
||||
}
|
||||
occs++
|
||||
}
|
||||
cursor.setDate(cursor.getDate() + 1)
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else if (event.repeat === 'months') {
|
||||
const baseStart = fromLocalString(event.startDate)
|
||||
const d = fromLocalString(dateStr)
|
||||
const diffMonths =
|
||||
(d.getFullYear() - baseStart.getFullYear()) * 12 + (d.getMonth() - baseStart.getMonth())
|
||||
if (diffMonths < 0) return false
|
||||
const interval = event.repeatInterval || 1
|
||||
if (diffMonths % interval !== 0) return false
|
||||
// Check day match (clamped for shorter months). Base day might exceed target month length.
|
||||
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 false
|
||||
if (event.repeatCount !== 'unlimited') {
|
||||
const limit = parseInt(event.repeatCount, 10)
|
||||
if (isNaN(limit)) return false
|
||||
// Base is occurrence 0; diffMonths/interval gives occurrence index
|
||||
const occurrenceIndex = diffMonths / interval
|
||||
return occurrenceIndex < limit
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return getOccurrenceIndex(event, dateStr) !== null
|
||||
},
|
||||
updateCurrentDate() {
|
||||
const d = new Date()
|
||||
@ -136,7 +78,6 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
}
|
||||
|
||||
this.events.set(event.id, { ...event, isSpanning: event.startDate < event.endDate })
|
||||
this._indexEventDates(event.id)
|
||||
return event.id
|
||||
},
|
||||
|
||||
@ -170,14 +111,6 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
},
|
||||
|
||||
deleteEvent(eventId) {
|
||||
if (!this.events.has(eventId)) return
|
||||
// Remove id from all date sets
|
||||
for (const [dateStr, set] of this.dates) {
|
||||
if (set.has(eventId)) {
|
||||
set.delete(eventId)
|
||||
if (set.size === 0) this.dates.delete(dateStr)
|
||||
}
|
||||
}
|
||||
this.events.delete(eventId)
|
||||
},
|
||||
|
||||
@ -213,11 +146,9 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
let found = 0
|
||||
let safety = 0
|
||||
const WEEK_MS = 7 * 86400000
|
||||
const baseBlockStart = new Date(baseStart)
|
||||
baseBlockStart.setDate(baseStart.getDate() - baseStart.getDay())
|
||||
const baseBlockStart = getMondayOfISOWeek(baseStart)
|
||||
function isAligned(d) {
|
||||
const blk = new Date(d)
|
||||
blk.setDate(d.getDate() - d.getDay())
|
||||
const blk = getMondayOfISOWeek(d)
|
||||
const diff = Math.floor((blk - baseBlockStart) / WEEK_MS)
|
||||
return diff % interval === 0
|
||||
}
|
||||
@ -236,12 +167,10 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
|
||||
// Count occurrences BEFORE target (always include the base occurrence as first)
|
||||
const baseStart = new Date(base.startDate + 'T00:00:00')
|
||||
const baseBlockStart = new Date(baseStart)
|
||||
baseBlockStart.setDate(baseStart.getDate() - baseStart.getDay())
|
||||
const baseBlockStart = getMondayOfISOWeek(baseStart)
|
||||
const WEEK_MS = 7 * 86400000
|
||||
function isAligned(d) {
|
||||
const block = new Date(d)
|
||||
block.setDate(d.getDate() - d.getDay())
|
||||
const block = getMondayOfISOWeek(d)
|
||||
const diff = Math.floor((block - baseBlockStart) / WEEK_MS)
|
||||
return diff % interval === 0
|
||||
}
|
||||
@ -473,43 +402,6 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
return ev ? { ...ev } : null
|
||||
},
|
||||
|
||||
_removeEventFromAllDatesById(eventId) {
|
||||
for (const [dateStr, set] of this.dates) {
|
||||
if (set.has(eventId)) {
|
||||
set.delete(eventId)
|
||||
if (set.size === 0) this.dates.delete(dateStr)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_addEventToDateRangeWithId(eventId, baseData, startDate, endDate) {
|
||||
// Update base data
|
||||
this.events.set(eventId, {
|
||||
...baseData,
|
||||
id: eventId,
|
||||
startDate,
|
||||
endDate,
|
||||
isSpanning: startDate < endDate,
|
||||
})
|
||||
this._indexEventDates(eventId)
|
||||
},
|
||||
|
||||
_indexEventDates(eventId) {
|
||||
const ev = this.events.get(eventId)
|
||||
if (!ev) return
|
||||
// remove old date references first
|
||||
for (const [, set] of this.dates) set.delete(eventId)
|
||||
const s = fromLocalString(ev.startDate)
|
||||
const e = fromLocalString(ev.endDate)
|
||||
const cur = new Date(s)
|
||||
while (cur <= e) {
|
||||
const dateStr = toLocalString(cur)
|
||||
if (!this.dates.has(dateStr)) this.dates.set(dateStr, new Set())
|
||||
this.dates.get(dateStr).add(ev.id)
|
||||
cur.setDate(cur.getDate() + 1)
|
||||
}
|
||||
},
|
||||
|
||||
// expandRepeats removed: no physical occurrence expansion
|
||||
|
||||
// Adjust start/end range of a base event (non-generated) and reindex occurrences
|
||||
@ -566,10 +458,13 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
snapshot.repeatWeekdays = rotated
|
||||
}
|
||||
}
|
||||
// Reindex
|
||||
this._removeEventFromAllDatesById(eventId)
|
||||
this._addEventToDateRangeWithId(eventId, snapshot, snapshot.startDate, snapshot.endDate)
|
||||
// no expansion
|
||||
// Update the event directly
|
||||
this.events.set(eventId, {
|
||||
...snapshot,
|
||||
startDate: snapshot.startDate,
|
||||
endDate: snapshot.endDate,
|
||||
isSpanning: snapshot.startDate < snapshot.endDate,
|
||||
})
|
||||
},
|
||||
|
||||
// Split a repeating series at a specific occurrence date and move that occurrence (and future) to new range
|
||||
@ -596,11 +491,9 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
const pattern = base.repeatWeekdays || []
|
||||
if (!pattern.some(Boolean)) return
|
||||
const WEEK_MS = 7 * 86400000
|
||||
const blockStartBase = new Date(baseStart)
|
||||
blockStartBase.setDate(blockStartBase.getDate() - blockStartBase.getDay())
|
||||
const blockStartBase = getMondayOfISOWeek(baseStart)
|
||||
function isAligned(d) {
|
||||
const blk = new Date(d)
|
||||
blk.setDate(d.getDate() - d.getDay())
|
||||
const blk = getMondayOfISOWeek(d)
|
||||
const diff = Math.floor((blk - blockStartBase) / WEEK_MS)
|
||||
return diff % interval === 0
|
||||
}
|
||||
@ -694,12 +587,6 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
return newId
|
||||
},
|
||||
|
||||
_reindexBaseEvent(eventId, snapshot, startDate, endDate) {
|
||||
if (!snapshot) return
|
||||
this._removeEventFromAllDatesById(eventId)
|
||||
this._addEventToDateRangeWithId(eventId, snapshot, startDate, endDate)
|
||||
},
|
||||
|
||||
_terminateRepeatSeriesAtIndex(baseId, index) {
|
||||
const ev = this.events.get(baseId)
|
||||
if (!ev || !ev.isRepeating) return
|
||||
@ -713,18 +600,13 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
|
||||
// _findEventInAnyList removed (direct map access)
|
||||
|
||||
_addEventToDateRange(event) {
|
||||
this.events.set(event.id, { ...event, isSpanning: event.startDate < event.endDate })
|
||||
this._indexEventDates(event.id)
|
||||
},
|
||||
|
||||
// NOTE: legacy dynamic getEventById for synthetic occurrences removed.
|
||||
},
|
||||
persist: {
|
||||
key: 'calendar-store',
|
||||
storage: localStorage,
|
||||
// Persist new structures; keep legacy 'events' only for transitional restore if needed
|
||||
paths: ['today', 'config', 'events', 'dates'],
|
||||
// Persist only events map, no dates indexing
|
||||
paths: ['today', 'config', 'events'],
|
||||
serializer: {
|
||||
serialize(value) {
|
||||
return JSON.stringify(value, (_k, v) => {
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user