Use date-fns module for date manipulations.
This commit is contained in:
@@ -1,18 +1,38 @@
|
||||
// date-utils.js — Date handling utilities for the calendar
|
||||
const monthAbbr = [
|
||||
'jan',
|
||||
'feb',
|
||||
'mar',
|
||||
'apr',
|
||||
'may',
|
||||
'jun',
|
||||
'jul',
|
||||
'aug',
|
||||
'sep',
|
||||
'oct',
|
||||
'nov',
|
||||
'dec',
|
||||
]
|
||||
// date-utils.js — Date handling utilities for the calendar (refactored to use date-fns/date-fns-tz)
|
||||
import {
|
||||
addDays,
|
||||
differenceInCalendarDays,
|
||||
differenceInCalendarMonths,
|
||||
format,
|
||||
getDate,
|
||||
getDay,
|
||||
getDaysInMonth,
|
||||
getMonth,
|
||||
getYear,
|
||||
isAfter,
|
||||
isBefore,
|
||||
isEqual,
|
||||
parseISO,
|
||||
startOfDay,
|
||||
} from 'date-fns'
|
||||
import { fromZonedTime, toZonedTime } from 'date-fns-tz'
|
||||
|
||||
// We expose a simple alias TZDate for clarity. date-fns by default works with native Date objects.
|
||||
// Consumers can pass an optional timeZone; if omitted, local time zone is assumed.
|
||||
|
||||
const DEFAULT_TZ = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
|
||||
|
||||
// Helper to create a zoned date (keeps wall-clock components in provided TZ)
|
||||
function makeTZDate(year, monthIndex, day, timeZone = DEFAULT_TZ) {
|
||||
const iso = `${String(year).padStart(4, '0')}-${String(monthIndex + 1).padStart(2, '0')}-${String(
|
||||
day
|
||||
).padStart(2, '0')}`
|
||||
// Interpret as start of day in target zone
|
||||
const utcDate = fromZonedTime(`${iso}T00:00:00`, timeZone)
|
||||
return toZonedTime(utcDate, timeZone)
|
||||
}
|
||||
|
||||
const monthAbbr = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']
|
||||
const DAY_MS = 86400000
|
||||
const WEEK_MS = 7 * DAY_MS
|
||||
|
||||
@@ -22,7 +42,8 @@ const WEEK_MS = 7 * DAY_MS
|
||||
* @returns {Object} Object containing week number and year
|
||||
*/
|
||||
const isoWeekInfo = (date) => {
|
||||
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()))
|
||||
// ISO week: Thursday algorithm
|
||||
const d = new Date(Date.UTC(getYear(date), getMonth(date), getDate(date)))
|
||||
const day = d.getUTCDay() || 7
|
||||
d.setUTCDate(d.getUTCDate() + 4 - day)
|
||||
const year = d.getUTCFullYear()
|
||||
@@ -36,9 +57,8 @@ const isoWeekInfo = (date) => {
|
||||
* @param {Date} date - The date to convert (defaults to new Date())
|
||||
* @returns {string} Date string in YYYY-MM-DD format
|
||||
*/
|
||||
function toLocalString(date = new Date()) {
|
||||
const pad = (n) => String(Math.floor(Math.abs(n))).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
||||
function toLocalString(date = new Date(), timeZone = DEFAULT_TZ) {
|
||||
return format(toZonedTime(date, timeZone), 'yyyy-MM-dd')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,9 +66,10 @@ function toLocalString(date = new Date()) {
|
||||
* @param {string} dateString - Date string in YYYY-MM-DD format
|
||||
* @returns {Date} Date object
|
||||
*/
|
||||
function fromLocalString(dateString) {
|
||||
const [year, month, day] = dateString.split('-').map(Number)
|
||||
return new Date(year, month - 1, day)
|
||||
function fromLocalString(dateString, timeZone = DEFAULT_TZ) {
|
||||
const parsed = parseISO(dateString)
|
||||
const utcDate = fromZonedTime(`${dateString}T00:00:00`, timeZone)
|
||||
return toZonedTime(utcDate, timeZone) || parsed
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,11 +77,10 @@ function fromLocalString(dateString) {
|
||||
* @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
|
||||
function getMondayOfISOWeek(date, timeZone = DEFAULT_TZ) {
|
||||
const d = startOfDay(toZonedTime(date, timeZone))
|
||||
const dayOfWeek = (getDay(d) + 6) % 7
|
||||
return addDays(d, -dayOfWeek)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,7 +88,7 @@ function getMondayOfISOWeek(date) {
|
||||
* @param {Date} d - The date
|
||||
* @returns {number} Monday index (0-6)
|
||||
*/
|
||||
const mondayIndex = (d) => (d.getDay() + 6) % 7
|
||||
const mondayIndex = (d) => (getDay(d) + 6) % 7
|
||||
|
||||
/**
|
||||
* Calculate the occurrence index for a repeating weekly event on a specific date
|
||||
@@ -76,29 +96,29 @@ const mondayIndex = (d) => (d.getDay() + 6) % 7
|
||||
* @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) {
|
||||
function getWeeklyOccurrenceIndex(event, dateStr, timeZone = DEFAULT_TZ) {
|
||||
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()
|
||||
const d = fromLocalString(dateStr, timeZone)
|
||||
const dow = getDay(d)
|
||||
if (!pattern[dow]) return null
|
||||
|
||||
const baseStart = fromLocalString(event.startDate)
|
||||
const baseStart = fromLocalString(event.startDate, timeZone)
|
||||
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 baseBlockStart = getMondayOfISOWeek(baseStart, timeZone)
|
||||
const currentBlockStart = getMondayOfISOWeek(d, timeZone)
|
||||
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()) {
|
||||
if (isEqual(currentBlockStart, baseBlockStart)) {
|
||||
// Special handling for the first week - only count occurrences on or after base date
|
||||
if (d.getTime() === baseStart.getTime()) {
|
||||
return 0 // Base occurrence is always index 0
|
||||
@@ -108,18 +128,18 @@ function getWeeklyOccurrenceIndex(event, dateStr) {
|
||||
return null // Dates before base start in same week are not valid occurrences
|
||||
}
|
||||
|
||||
let occurrenceIndex = 0
|
||||
const cursor = new Date(baseStart)
|
||||
let occurrenceIndex = 0
|
||||
let cursor = new Date(baseStart)
|
||||
|
||||
// Count the base occurrence first
|
||||
if (pattern[cursor.getDay()]) occurrenceIndex++
|
||||
if (pattern[getDay(cursor)]) occurrenceIndex++
|
||||
|
||||
// Move to the next day and count until we reach the target
|
||||
cursor.setDate(cursor.getDate() + 1)
|
||||
while (cursor <= d) {
|
||||
if (pattern[cursor.getDay()]) occurrenceIndex++
|
||||
cursor.setDate(cursor.getDate() + 1)
|
||||
}
|
||||
// Move to the next day and count until we reach the target
|
||||
cursor = addDays(cursor, 1)
|
||||
while (cursor <= d) {
|
||||
if (pattern[getDay(cursor)]) occurrenceIndex++
|
||||
cursor = addDays(cursor, 1)
|
||||
}
|
||||
|
||||
// Subtract 1 because we want the index, not the count
|
||||
occurrenceIndex--
|
||||
@@ -136,13 +156,12 @@ function getWeeklyOccurrenceIndex(event, dateStr) {
|
||||
// For different weeks, calculate based on complete intervals
|
||||
// Calculate how many pattern days actually occur in the first week (from base start onward)
|
||||
let firstWeekPatternDays = 0
|
||||
const firstWeekCursor = new Date(baseStart)
|
||||
const firstWeekEnd = new Date(baseBlockStart)
|
||||
firstWeekEnd.setDate(firstWeekEnd.getDate() + 6) // End of first week (Sunday)
|
||||
let firstWeekCursor = new Date(baseStart)
|
||||
const firstWeekEnd = addDays(new Date(baseBlockStart), 6) // End of first week (Sunday)
|
||||
|
||||
while (firstWeekCursor <= firstWeekEnd) {
|
||||
if (pattern[firstWeekCursor.getDay()]) firstWeekPatternDays++
|
||||
firstWeekCursor.setDate(firstWeekCursor.getDate() + 1)
|
||||
if (pattern[getDay(firstWeekCursor)]) firstWeekPatternDays++
|
||||
firstWeekCursor = addDays(firstWeekCursor, 1)
|
||||
}
|
||||
|
||||
// For subsequent complete intervals, use the full pattern count
|
||||
@@ -153,10 +172,10 @@ function getWeeklyOccurrenceIndex(event, dateStr) {
|
||||
let occurrenceIndex = firstWeekPatternDays + (completeIntervals - 1) * fullWeekdaysPerInterval
|
||||
|
||||
// Add occurrences from the current week up to the target date
|
||||
const cursor = new Date(currentBlockStart)
|
||||
cursor = new Date(currentBlockStart)
|
||||
while (cursor < d) {
|
||||
if (pattern[cursor.getDay()]) occurrenceIndex++
|
||||
cursor.setDate(cursor.getDate() + 1)
|
||||
if (pattern[getDay(cursor)]) occurrenceIndex++
|
||||
cursor = addDays(cursor, 1)
|
||||
}
|
||||
|
||||
// Check against repeat count limit
|
||||
@@ -174,13 +193,12 @@ function getWeeklyOccurrenceIndex(event, dateStr) {
|
||||
* @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) {
|
||||
function getMonthlyOccurrenceIndex(event, dateStr, timeZone = DEFAULT_TZ) {
|
||||
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())
|
||||
const baseStart = fromLocalString(event.startDate, timeZone)
|
||||
const d = fromLocalString(dateStr, timeZone)
|
||||
const diffMonths = differenceInCalendarMonths(d, baseStart)
|
||||
|
||||
if (diffMonths < 0) return null
|
||||
|
||||
@@ -188,10 +206,10 @@ function getMonthlyOccurrenceIndex(event, dateStr) {
|
||||
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 baseDay = getDate(baseStart)
|
||||
const daysInMonth = getDaysInMonth(d)
|
||||
const effectiveDay = Math.min(baseDay, daysInMonth)
|
||||
if (d.getDate() !== effectiveDay) return null
|
||||
if (getDate(d) !== effectiveDay) return null
|
||||
|
||||
const occurrenceIndex = diffMonths / interval
|
||||
|
||||
@@ -210,14 +228,14 @@ function getMonthlyOccurrenceIndex(event, dateStr) {
|
||||
* @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) {
|
||||
function getOccurrenceIndex(event, dateStr, timeZone = DEFAULT_TZ) {
|
||||
if (!event || !event.isRepeating || event.repeat === 'none') return null
|
||||
if (dateStr < event.startDate) return null
|
||||
|
||||
if (event.repeat === 'weeks') {
|
||||
return getWeeklyOccurrenceIndex(event, dateStr)
|
||||
return getWeeklyOccurrenceIndex(event, dateStr, timeZone)
|
||||
} else if (event.repeat === 'months') {
|
||||
return getMonthlyOccurrenceIndex(event, dateStr)
|
||||
return getMonthlyOccurrenceIndex(event, dateStr, timeZone)
|
||||
}
|
||||
|
||||
return null
|
||||
@@ -229,16 +247,13 @@ function getOccurrenceIndex(event, dateStr) {
|
||||
* @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)
|
||||
function getVirtualOccurrenceEndDate(event, occurrenceStartDate, timeZone = DEFAULT_TZ) {
|
||||
const baseStart = fromLocalString(event.startDate, timeZone)
|
||||
const baseEnd = fromLocalString(event.endDate, timeZone)
|
||||
const spanDays = Math.max(0, differenceInCalendarDays(baseEnd, baseStart))
|
||||
const occurrenceStart = fromLocalString(occurrenceStartDate, timeZone)
|
||||
const occurrenceEnd = addDays(occurrenceStart, spanDays)
|
||||
return toLocalString(occurrenceEnd, timeZone)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,16 +262,16 @@ function getVirtualOccurrenceEndDate(event, occurrenceStartDate) {
|
||||
* @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) {
|
||||
function occursOnOrSpansDate(event, dateStr, timeZone = DEFAULT_TZ) {
|
||||
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)))
|
||||
const baseStart = fromLocalString(event.startDate, timeZone)
|
||||
const baseEnd = fromLocalString(event.endDate, timeZone)
|
||||
const spanDays = Math.max(0, differenceInCalendarDays(baseEnd, baseStart))
|
||||
|
||||
if (spanDays === 0) {
|
||||
// Single day event - just check if it occurs on this date
|
||||
@@ -264,14 +279,14 @@ function occursOnOrSpansDate(event, dateStr) {
|
||||
}
|
||||
|
||||
// Multi-day event - check if any occurrence's span includes this date
|
||||
const targetDate = fromLocalString(dateStr)
|
||||
const targetDate = fromLocalString(dateStr, timeZone)
|
||||
|
||||
if (event.repeat === 'weeks') {
|
||||
const pattern = event.repeatWeekdays || []
|
||||
const pattern = event.repeatWeekdays || []
|
||||
if (!pattern.some(Boolean)) return false
|
||||
|
||||
const interval = event.repeatInterval || 1
|
||||
const baseBlockStart = getMondayOfISOWeek(baseStart)
|
||||
const interval = event.repeatInterval || 1
|
||||
const baseBlockStart = getMondayOfISOWeek(baseStart, timeZone)
|
||||
const WEEK_MS = 7 * 86400000
|
||||
|
||||
// Check a reasonable range of weeks around the target date
|
||||
@@ -280,75 +295,69 @@ function occursOnOrSpansDate(event, dateStr) {
|
||||
weekOffset <= Math.ceil(spanDays / 7) + 1;
|
||||
weekOffset++
|
||||
) {
|
||||
const weekStart = new Date(baseBlockStart)
|
||||
weekStart.setDate(weekStart.getDate() + weekOffset * 7)
|
||||
const weekStart = addDays(baseBlockStart, weekOffset * 7)
|
||||
|
||||
// Check if this week aligns with the interval
|
||||
const blocksDiff = Math.floor((weekStart - baseBlockStart) / WEEK_MS)
|
||||
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)
|
||||
const candidateStart = addDays(weekStart, day)
|
||||
|
||||
// Skip if before base start
|
||||
if (candidateStart < baseStart) continue
|
||||
if (isBefore(candidateStart, baseStart)) continue
|
||||
|
||||
// Check if this day matches the pattern
|
||||
if (!pattern[candidateStart.getDay()]) continue
|
||||
if (!pattern[getDay(candidateStart)]) continue
|
||||
|
||||
// Check repeat count limit
|
||||
const occIndex = getWeeklyOccurrenceIndex(event, toLocalString(candidateStart))
|
||||
const occIndex = getWeeklyOccurrenceIndex(event, toLocalString(candidateStart, timeZone), timeZone)
|
||||
if (occIndex === null) continue
|
||||
|
||||
// Calculate end date for this occurrence
|
||||
const candidateEnd = new Date(candidateStart)
|
||||
candidateEnd.setDate(candidateEnd.getDate() + spanDays)
|
||||
const candidateEnd = addDays(candidateStart, spanDays)
|
||||
|
||||
// Check if target date falls within this occurrence's span
|
||||
if (targetDate >= candidateStart && targetDate <= candidateEnd) {
|
||||
if (!isBefore(targetDate, candidateStart) && !isAfter(targetDate, candidateEnd)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (event.repeat === 'months') {
|
||||
const interval = event.repeatInterval || 1
|
||||
const baseDay = baseStart.getDate()
|
||||
const interval = event.repeatInterval || 1
|
||||
const baseDay = getDate(baseStart)
|
||||
|
||||
// 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()
|
||||
// targetYear & targetMonth not needed in refactored logic
|
||||
const baseYear = getYear(baseStart)
|
||||
const baseMonth = getMonth(baseStart)
|
||||
|
||||
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)
|
||||
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 daysInMonth = getDaysInMonth(new Date(candidateYear, candidateMonth, 1))
|
||||
const effectiveDay = Math.min(baseDay, daysInMonth)
|
||||
|
||||
const candidateStart = new Date(candidateYear, candidateMonth, effectiveDay)
|
||||
const candidateStart = makeTZDate(candidateYear, candidateMonth, effectiveDay)
|
||||
|
||||
// Skip if before base start
|
||||
if (candidateStart < baseStart) continue
|
||||
if (isBefore(candidateStart, baseStart)) continue
|
||||
|
||||
// Check repeat count limit
|
||||
const occIndex = getMonthlyOccurrenceIndex(event, toLocalString(candidateStart))
|
||||
const occIndex = getMonthlyOccurrenceIndex(event, toLocalString(candidateStart, timeZone), timeZone)
|
||||
if (occIndex === null) continue
|
||||
|
||||
// Calculate end date for this occurrence
|
||||
const candidateEnd = new Date(candidateStart)
|
||||
candidateEnd.setDate(candidateEnd.getDate() + spanDays)
|
||||
const candidateEnd = addDays(candidateStart, spanDays)
|
||||
|
||||
// Check if target date falls within this occurrence's span
|
||||
if (targetDate >= candidateStart && targetDate <= candidateEnd) {
|
||||
if (!isBefore(targetDate, candidateStart) && !isAfter(targetDate, candidateEnd)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -368,12 +377,10 @@ const pad = (n) => String(n).padStart(2, '0')
|
||||
* @param {string} bStr - Second date string (YYYY-MM-DD)
|
||||
* @returns {number} Number of days inclusive
|
||||
*/
|
||||
function daysInclusive(aStr, bStr) {
|
||||
const a = fromLocalString(aStr)
|
||||
const b = fromLocalString(bStr)
|
||||
const A = new Date(a.getFullYear(), a.getMonth(), a.getDate()).getTime()
|
||||
const B = new Date(b.getFullYear(), b.getMonth(), b.getDate()).getTime()
|
||||
return Math.floor(Math.abs(B - A) / DAY_MS) + 1
|
||||
function daysInclusive(aStr, bStr, timeZone = DEFAULT_TZ) {
|
||||
const a = fromLocalString(aStr, timeZone)
|
||||
const b = fromLocalString(bStr, timeZone)
|
||||
return Math.abs(differenceInCalendarDays(startOfDay(a), startOfDay(b))) + 1
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -382,23 +389,23 @@ function daysInclusive(aStr, bStr) {
|
||||
* @param {number} n - Number of days to add (can be negative)
|
||||
* @returns {string} New date string
|
||||
*/
|
||||
function addDaysStr(str, n) {
|
||||
const d = fromLocalString(str)
|
||||
d.setDate(d.getDate() + n)
|
||||
return toLocalString(d)
|
||||
function addDaysStr(str, n, timeZone = DEFAULT_TZ) {
|
||||
const d = fromLocalString(str, timeZone)
|
||||
return toLocalString(addDays(d, n), timeZone)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get localized weekday names starting from Monday
|
||||
* @returns {Array<string>} Array of localized weekday names
|
||||
*/
|
||||
function getLocalizedWeekdayNames() {
|
||||
function getLocalizedWeekdayNames(timeZone = DEFAULT_TZ) {
|
||||
const res = []
|
||||
const base = new Date(2025, 0, 6) // A Monday
|
||||
const base = makeTZDate(2025, 0, 6, timeZone) // Monday
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const d = new Date(base)
|
||||
d.setDate(base.getDate() + i)
|
||||
res.push(d.toLocaleDateString(undefined, { weekday: 'short' }))
|
||||
const d = addDays(base, i)
|
||||
res.push(
|
||||
new Intl.DateTimeFormat(undefined, { weekday: 'short', timeZone }).format(d)
|
||||
)
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -445,9 +452,9 @@ function reorderByFirstDay(days, firstDay) {
|
||||
* @param {boolean} short - Whether to return short name
|
||||
* @returns {string} Localized month name
|
||||
*/
|
||||
function getLocalizedMonthName(idx, short = false) {
|
||||
const d = new Date(2025, idx, 1)
|
||||
return d.toLocaleDateString(undefined, { month: short ? 'short' : 'long' })
|
||||
function getLocalizedMonthName(idx, short = false, timeZone = DEFAULT_TZ) {
|
||||
const d = makeTZDate(2025, idx, 1, timeZone)
|
||||
return new Intl.DateTimeFormat(undefined, { month: short ? 'short' : 'long', timeZone }).format(d)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -456,10 +463,11 @@ function getLocalizedMonthName(idx, short = false) {
|
||||
* @param {Date} endDate - End date
|
||||
* @returns {string} Formatted date range string
|
||||
*/
|
||||
function formatDateRange(startDate, endDate) {
|
||||
if (toLocalString(startDate) === toLocalString(endDate)) return toLocalString(startDate)
|
||||
const startISO = toLocalString(startDate)
|
||||
const endISO = toLocalString(endDate)
|
||||
function formatDateRange(startDate, endDate, timeZone = DEFAULT_TZ) {
|
||||
if (toLocalString(startDate, timeZone) === toLocalString(endDate, timeZone))
|
||||
return toLocalString(startDate, timeZone)
|
||||
const startISO = toLocalString(startDate, timeZone)
|
||||
const endISO = toLocalString(endDate, timeZone)
|
||||
const [sy, sm] = startISO.split('-')
|
||||
const [ey, em, ed] = endISO.split('-')
|
||||
if (sy === ey && sm === em) return `${startISO}/${ed}`
|
||||
@@ -521,4 +529,6 @@ export {
|
||||
getLocalizedMonthName,
|
||||
formatDateRange,
|
||||
lunarPhaseSymbol,
|
||||
makeTZDate,
|
||||
DEFAULT_TZ,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user