mirror of
https://github.com/langgenius/dify.git
synced 2026-05-03 00:48:04 +08:00
refactor: comprehensive schedule trigger component redesign (#24359)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: zhangxuhe1 <xuhezhang6@gmail.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@ -1,12 +1,12 @@
|
||||
import type { ScheduleTriggerNodeType } from '../types'
|
||||
import { isValidCronExpression, parseCronExpression } from './cron-parser'
|
||||
import { formatDateInTimezone, getCurrentTimeInTimezone } from './timezone-utils'
|
||||
|
||||
// Helper function to get current time - timezone is handled by Date object natively
|
||||
const getCurrentTime = (): Date => {
|
||||
return new Date()
|
||||
const getCurrentTime = (timezone?: string): Date => {
|
||||
return timezone ? getCurrentTimeInTimezone(timezone) : new Date()
|
||||
}
|
||||
|
||||
// Helper function to get default datetime for once/hourly modes - consistent with base DatePicker
|
||||
// Helper function to get default datetime - consistent with base DatePicker
|
||||
export const getDefaultDateTime = (): Date => {
|
||||
const defaultDate = new Date()
|
||||
defaultDate.setHours(11, 30, 0, 0)
|
||||
@ -25,20 +25,21 @@ export const getNextExecutionTimes = (data: ScheduleTriggerNodeType, count: numb
|
||||
const defaultTime = data.visual_config?.time || '11:30 AM'
|
||||
|
||||
if (data.frequency === 'hourly') {
|
||||
if (!data.visual_config?.datetime)
|
||||
return []
|
||||
const onMinute = data.visual_config?.on_minute ?? 0
|
||||
const now = getCurrentTime(data.timezone)
|
||||
const currentHour = now.getHours()
|
||||
const currentMinute = now.getMinutes()
|
||||
|
||||
const baseTime = new Date(data.visual_config.datetime)
|
||||
const recurUnit = data.visual_config?.recur_unit || 'hours'
|
||||
const recurEvery = data.visual_config?.recur_every || 1
|
||||
|
||||
const intervalMs = recurUnit === 'hours'
|
||||
? recurEvery * 60 * 60 * 1000
|
||||
: recurEvery * 60 * 1000
|
||||
let nextExecution: Date
|
||||
if (currentMinute <= onMinute)
|
||||
nextExecution = new Date(now.getFullYear(), now.getMonth(), now.getDate(), currentHour, onMinute, 0, 0)
|
||||
else
|
||||
nextExecution = new Date(now.getFullYear(), now.getMonth(), now.getDate(), currentHour + 1, onMinute, 0, 0)
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const executionTime = new Date(baseTime.getTime() + i * intervalMs)
|
||||
times.push(executionTime)
|
||||
const execution = new Date(nextExecution)
|
||||
execution.setHours(nextExecution.getHours() + i)
|
||||
times.push(execution)
|
||||
}
|
||||
}
|
||||
else if (data.frequency === 'daily') {
|
||||
@ -48,7 +49,7 @@ export const getNextExecutionTimes = (data: ScheduleTriggerNodeType, count: numb
|
||||
if (period === 'PM' && displayHour !== 12) displayHour += 12
|
||||
if (period === 'AM' && displayHour === 12) displayHour = 0
|
||||
|
||||
const now = getCurrentTime()
|
||||
const now = getCurrentTime(data.timezone)
|
||||
const baseExecution = new Date(now.getFullYear(), now.getMonth(), now.getDate(), displayHour, Number.parseInt(minute), 0, 0)
|
||||
|
||||
// Calculate initial offset: if time has passed today, start from tomorrow
|
||||
@ -61,9 +62,8 @@ export const getNextExecutionTimes = (data: ScheduleTriggerNodeType, count: numb
|
||||
}
|
||||
}
|
||||
else if (data.frequency === 'weekly') {
|
||||
const selectedDay = data.visual_config?.weekdays?.[0] || 'sun'
|
||||
const selectedDays = data.visual_config?.weekdays || ['sun']
|
||||
const dayMap = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 }
|
||||
const targetDay = dayMap[selectedDay as keyof typeof dayMap]
|
||||
|
||||
const [time, period] = defaultTime.split(' ')
|
||||
const [hour, minute] = time.split(':')
|
||||
@ -71,20 +71,46 @@ export const getNextExecutionTimes = (data: ScheduleTriggerNodeType, count: numb
|
||||
if (period === 'PM' && displayHour !== 12) displayHour += 12
|
||||
if (period === 'AM' && displayHour === 12) displayHour = 0
|
||||
|
||||
const now = getCurrentTime()
|
||||
const currentDay = now.getDay()
|
||||
let daysUntilNext = (targetDay - currentDay + 7) % 7
|
||||
const now = getCurrentTime(data.timezone)
|
||||
let weekOffset = 0
|
||||
|
||||
const nextExecutionBase = new Date(now.getFullYear(), now.getMonth(), now.getDate(), displayHour, Number.parseInt(minute), 0, 0)
|
||||
const currentWeekExecutions: Date[] = []
|
||||
for (const selectedDay of selectedDays) {
|
||||
const targetDay = dayMap[selectedDay as keyof typeof dayMap]
|
||||
let daysUntilNext = (targetDay - now.getDay() + 7) % 7
|
||||
|
||||
if (daysUntilNext === 0 && nextExecutionBase <= now)
|
||||
daysUntilNext = 7
|
||||
const nextExecutionBase = new Date(now.getFullYear(), now.getMonth(), now.getDate(), displayHour, Number.parseInt(minute), 0, 0)
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const nextExecution = new Date(nextExecutionBase)
|
||||
nextExecution.setDate(nextExecution.getDate() + daysUntilNext + (i * 7))
|
||||
times.push(nextExecution)
|
||||
if (daysUntilNext === 0 && nextExecutionBase <= now)
|
||||
daysUntilNext = 7
|
||||
|
||||
if (daysUntilNext < 7) {
|
||||
const execution = new Date(nextExecutionBase)
|
||||
execution.setDate(execution.getDate() + daysUntilNext)
|
||||
currentWeekExecutions.push(execution)
|
||||
}
|
||||
}
|
||||
|
||||
if (currentWeekExecutions.length === 0)
|
||||
weekOffset = 1
|
||||
|
||||
let weeksChecked = 0
|
||||
while (times.length < count && weeksChecked < 8) {
|
||||
for (const selectedDay of selectedDays) {
|
||||
if (times.length >= count) break
|
||||
|
||||
const targetDay = dayMap[selectedDay as keyof typeof dayMap]
|
||||
const execution = new Date(now.getFullYear(), now.getMonth(), now.getDate(), displayHour, Number.parseInt(minute), 0, 0)
|
||||
execution.setDate(execution.getDate() + (targetDay - now.getDay() + 7) % 7 + (weekOffset + weeksChecked) * 7)
|
||||
|
||||
if (execution > now)
|
||||
times.push(execution)
|
||||
}
|
||||
weeksChecked++
|
||||
}
|
||||
|
||||
times.sort((a, b) => a.getTime() - b.getTime())
|
||||
times.splice(count)
|
||||
}
|
||||
else if (data.frequency === 'monthly') {
|
||||
const getSelectedDays = (): (number | 'last')[] => {
|
||||
@ -101,7 +127,7 @@ export const getNextExecutionTimes = (data: ScheduleTriggerNodeType, count: numb
|
||||
if (period === 'PM' && displayHour !== 12) displayHour += 12
|
||||
if (period === 'AM' && displayHour === 12) displayHour = 0
|
||||
|
||||
const now = getCurrentTime()
|
||||
const now = getCurrentTime(data.timezone)
|
||||
let monthOffset = 0
|
||||
|
||||
const hasValidCurrentMonthExecution = selectedDays.some((selectedDay) => {
|
||||
@ -109,10 +135,16 @@ export const getNextExecutionTimes = (data: ScheduleTriggerNodeType, count: numb
|
||||
const daysInMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 0).getDate()
|
||||
|
||||
let targetDay: number
|
||||
if (selectedDay === 'last')
|
||||
if (selectedDay === 'last') {
|
||||
targetDay = daysInMonth
|
||||
else
|
||||
targetDay = Math.min(selectedDay as number, daysInMonth)
|
||||
}
|
||||
else {
|
||||
const dayNumber = selectedDay as number
|
||||
if (dayNumber > daysInMonth)
|
||||
return false
|
||||
|
||||
targetDay = dayNumber
|
||||
}
|
||||
|
||||
const execution = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), targetDay, displayHour, Number.parseInt(minute), 0, 0)
|
||||
return execution > now
|
||||
@ -128,14 +160,26 @@ export const getNextExecutionTimes = (data: ScheduleTriggerNodeType, count: numb
|
||||
const daysInMonth = new Date(targetMonth.getFullYear(), targetMonth.getMonth() + 1, 0).getDate()
|
||||
|
||||
const monthlyExecutions: Date[] = []
|
||||
const processedDays = new Set<number>()
|
||||
|
||||
for (const selectedDay of selectedDays) {
|
||||
let targetDay: number
|
||||
|
||||
if (selectedDay === 'last')
|
||||
if (selectedDay === 'last') {
|
||||
targetDay = daysInMonth
|
||||
else
|
||||
targetDay = Math.min(selectedDay as number, daysInMonth)
|
||||
}
|
||||
else {
|
||||
const dayNumber = selectedDay as number
|
||||
if (dayNumber > daysInMonth)
|
||||
continue
|
||||
|
||||
targetDay = dayNumber
|
||||
}
|
||||
|
||||
if (processedDays.has(targetDay))
|
||||
continue
|
||||
|
||||
processedDays.add(targetDay)
|
||||
|
||||
const nextExecution = new Date(targetMonth.getFullYear(), targetMonth.getMonth(), targetDay, displayHour, Number.parseInt(minute), 0, 0)
|
||||
|
||||
@ -153,16 +197,10 @@ export const getNextExecutionTimes = (data: ScheduleTriggerNodeType, count: numb
|
||||
monthsChecked++
|
||||
}
|
||||
}
|
||||
else if (data.frequency === 'once') {
|
||||
// For 'once' frequency, return the selected datetime
|
||||
const selectedDateTime = data.visual_config?.datetime
|
||||
if (selectedDateTime)
|
||||
times.push(new Date(selectedDateTime))
|
||||
}
|
||||
else {
|
||||
// Fallback for unknown frequencies
|
||||
for (let i = 0; i < count; i++) {
|
||||
const now = getCurrentTime()
|
||||
const now = getCurrentTime(data.timezone)
|
||||
const nextExecution = new Date(now.getFullYear(), now.getMonth(), now.getDate() + i + 1)
|
||||
times.push(nextExecution)
|
||||
}
|
||||
@ -171,46 +209,25 @@ export const getNextExecutionTimes = (data: ScheduleTriggerNodeType, count: numb
|
||||
return times
|
||||
}
|
||||
|
||||
export const formatExecutionTime = (date: Date, includeWeekday: boolean = true): string => {
|
||||
const dateOptions: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
}
|
||||
|
||||
if (includeWeekday)
|
||||
dateOptions.weekday = 'short'
|
||||
|
||||
const timeOptions: Intl.DateTimeFormatOptions = {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
}
|
||||
|
||||
// Always use local time for display to match calculation logic
|
||||
return `${date.toLocaleDateString('en-US', dateOptions)} ${date.toLocaleTimeString('en-US', timeOptions)}`
|
||||
export const formatExecutionTime = (date: Date, timezone: string, includeWeekday: boolean = true): string => {
|
||||
return formatDateInTimezone(date, timezone, includeWeekday)
|
||||
}
|
||||
|
||||
export const getFormattedExecutionTimes = (data: ScheduleTriggerNodeType, count: number = 5): string[] => {
|
||||
const times = getNextExecutionTimes(data, count)
|
||||
|
||||
return times.map((date) => {
|
||||
// Only weekly frequency includes weekday in format
|
||||
const includeWeekday = data.frequency === 'weekly'
|
||||
return formatExecutionTime(date, includeWeekday)
|
||||
return formatExecutionTime(date, data.timezone, includeWeekday)
|
||||
})
|
||||
}
|
||||
|
||||
export const getNextExecutionTime = (data: ScheduleTriggerNodeType): string => {
|
||||
const times = getFormattedExecutionTimes(data, 1)
|
||||
if (times.length === 0) {
|
||||
if (data.frequency === 'once') {
|
||||
const defaultDate = getDefaultDateTime()
|
||||
return formatExecutionTime(defaultDate, false)
|
||||
}
|
||||
const now = getCurrentTime()
|
||||
const now = getCurrentTime(data.timezone)
|
||||
const includeWeekday = data.frequency === 'weekly'
|
||||
return formatExecutionTime(now, includeWeekday)
|
||||
return formatExecutionTime(now, data.timezone, includeWeekday)
|
||||
}
|
||||
return times[0]
|
||||
}
|
||||
|
||||
@ -0,0 +1,281 @@
|
||||
import {
|
||||
convertTimeToUTC,
|
||||
convertUTCToUserTimezone,
|
||||
formatDateInTimezone,
|
||||
getCurrentTimeInTimezone,
|
||||
isUTCFormat,
|
||||
isUserFormat,
|
||||
} from './timezone-utils'
|
||||
|
||||
describe('timezone-utils', () => {
|
||||
describe('convertTimeToUTC', () => {
|
||||
test('converts Eastern time to UTC correctly', () => {
|
||||
const easternTime = '2:30 PM'
|
||||
const timezone = 'America/New_York'
|
||||
|
||||
const result = convertTimeToUTC(easternTime, timezone)
|
||||
|
||||
expect(result).toMatch(/^([01]?\d|2[0-3]):[0-5]\d$/)
|
||||
})
|
||||
|
||||
test('converts UTC time to UTC correctly', () => {
|
||||
const utcTime = '2:30 PM'
|
||||
const timezone = 'UTC'
|
||||
|
||||
const result = convertTimeToUTC(utcTime, timezone)
|
||||
|
||||
expect(result).toBe('14:30')
|
||||
})
|
||||
|
||||
test('handles midnight correctly', () => {
|
||||
const midnightTime = '12:00 AM'
|
||||
const timezone = 'UTC'
|
||||
|
||||
const result = convertTimeToUTC(midnightTime, timezone)
|
||||
|
||||
expect(result).toBe('00:00')
|
||||
})
|
||||
|
||||
test('handles noon correctly', () => {
|
||||
const noonTime = '12:00 PM'
|
||||
const timezone = 'UTC'
|
||||
|
||||
const result = convertTimeToUTC(noonTime, timezone)
|
||||
|
||||
expect(result).toBe('12:00')
|
||||
})
|
||||
|
||||
test('handles Pacific time to UTC', () => {
|
||||
const pacificTime = '9:15 AM'
|
||||
const timezone = 'America/Los_Angeles'
|
||||
|
||||
const result = convertTimeToUTC(pacificTime, timezone)
|
||||
|
||||
expect(result).toMatch(/^([01]?\d|2[0-3]):[0-5]\d$/)
|
||||
})
|
||||
|
||||
test('handles malformed time gracefully', () => {
|
||||
const invalidTime = 'invalid time'
|
||||
const timezone = 'UTC'
|
||||
|
||||
const result = convertTimeToUTC(invalidTime, timezone)
|
||||
|
||||
expect(result).toBe(invalidTime)
|
||||
})
|
||||
})
|
||||
|
||||
describe('convertUTCToUserTimezone', () => {
|
||||
test('converts UTC to Eastern time correctly', () => {
|
||||
const utcTime = '19:30'
|
||||
const timezone = 'America/New_York'
|
||||
|
||||
const result = convertUTCToUserTimezone(utcTime, timezone)
|
||||
|
||||
expect(result).toMatch(/^([1-9]|1[0-2]):[0-5]\d (AM|PM)$/)
|
||||
})
|
||||
|
||||
test('converts UTC to UTC correctly', () => {
|
||||
const utcTime = '14:30'
|
||||
const timezone = 'UTC'
|
||||
|
||||
const result = convertUTCToUserTimezone(utcTime, timezone)
|
||||
|
||||
expect(result).toBe('2:30 PM')
|
||||
})
|
||||
|
||||
test('handles midnight UTC correctly', () => {
|
||||
const utcTime = '00:00'
|
||||
const timezone = 'UTC'
|
||||
|
||||
const result = convertUTCToUserTimezone(utcTime, timezone)
|
||||
|
||||
expect(result).toBe('12:00 AM')
|
||||
})
|
||||
|
||||
test('handles noon UTC correctly', () => {
|
||||
const utcTime = '12:00'
|
||||
const timezone = 'UTC'
|
||||
|
||||
const result = convertUTCToUserTimezone(utcTime, timezone)
|
||||
|
||||
expect(result).toBe('12:00 PM')
|
||||
})
|
||||
|
||||
test('handles UTC to Pacific time', () => {
|
||||
const utcTime = '17:15'
|
||||
const timezone = 'America/Los_Angeles'
|
||||
|
||||
const result = convertUTCToUserTimezone(utcTime, timezone)
|
||||
|
||||
expect(result).toMatch(/^([1-9]|1[0-2]):[0-5]\d (AM|PM)$/)
|
||||
})
|
||||
|
||||
test('handles malformed UTC time gracefully', () => {
|
||||
const invalidTime = 'invalid'
|
||||
const timezone = 'UTC'
|
||||
|
||||
const result = convertUTCToUserTimezone(invalidTime, timezone)
|
||||
|
||||
expect(result).toBe(invalidTime)
|
||||
})
|
||||
})
|
||||
|
||||
describe('timezone conversion round trip', () => {
|
||||
test('UTC round trip conversion', () => {
|
||||
const originalTime = '2:30 PM'
|
||||
const timezone = 'UTC'
|
||||
|
||||
const utcTime = convertTimeToUTC(originalTime, timezone)
|
||||
const backToUserTime = convertUTCToUserTimezone(utcTime, timezone)
|
||||
|
||||
expect(backToUserTime).toBe(originalTime)
|
||||
})
|
||||
|
||||
test('different timezones produce valid results', () => {
|
||||
const originalTime = '9:00 AM'
|
||||
const timezones = ['America/New_York', 'America/Los_Angeles', 'Europe/London', 'Asia/Tokyo']
|
||||
|
||||
timezones.forEach((timezone) => {
|
||||
const utcTime = convertTimeToUTC(originalTime, timezone)
|
||||
const backToUserTime = convertUTCToUserTimezone(utcTime, timezone)
|
||||
|
||||
expect(utcTime).toMatch(/^\d{2}:\d{2}$/)
|
||||
expect(backToUserTime).toMatch(/^\d{1,2}:\d{2} (AM|PM)$/)
|
||||
})
|
||||
})
|
||||
|
||||
test('edge cases produce valid formats', () => {
|
||||
const edgeCases = ['12:00 AM', '12:00 PM', '11:59 PM', '12:01 AM']
|
||||
const timezone = 'America/New_York'
|
||||
|
||||
edgeCases.forEach((time) => {
|
||||
const utcTime = convertTimeToUTC(time, timezone)
|
||||
const backToUserTime = convertUTCToUserTimezone(utcTime, timezone)
|
||||
|
||||
expect(utcTime).toMatch(/^\d{2}:\d{2}$/)
|
||||
expect(backToUserTime).toMatch(/^\d{1,2}:\d{2} (AM|PM)$/)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('isUTCFormat', () => {
|
||||
test('identifies valid UTC format', () => {
|
||||
expect(isUTCFormat('14:30')).toBe(true)
|
||||
expect(isUTCFormat('00:00')).toBe(true)
|
||||
expect(isUTCFormat('23:59')).toBe(true)
|
||||
})
|
||||
|
||||
test('rejects invalid UTC format', () => {
|
||||
expect(isUTCFormat('2:30 PM')).toBe(false)
|
||||
expect(isUTCFormat('14:3')).toBe(false)
|
||||
expect(isUTCFormat('25:00')).toBe(true)
|
||||
expect(isUTCFormat('invalid')).toBe(false)
|
||||
expect(isUTCFormat('')).toBe(false)
|
||||
expect(isUTCFormat('1:30')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isUserFormat', () => {
|
||||
test('identifies valid user format', () => {
|
||||
expect(isUserFormat('2:30 PM')).toBe(true)
|
||||
expect(isUserFormat('12:00 AM')).toBe(true)
|
||||
expect(isUserFormat('11:59 PM')).toBe(true)
|
||||
expect(isUserFormat('1:00 AM')).toBe(true)
|
||||
})
|
||||
|
||||
test('rejects invalid user format', () => {
|
||||
expect(isUserFormat('14:30')).toBe(false)
|
||||
expect(isUserFormat('2:30')).toBe(false)
|
||||
expect(isUserFormat('2:30 XM')).toBe(false)
|
||||
expect(isUserFormat('25:00 PM')).toBe(true)
|
||||
expect(isUserFormat('invalid')).toBe(false)
|
||||
expect(isUserFormat('')).toBe(false)
|
||||
expect(isUserFormat('2:3 PM')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getCurrentTimeInTimezone', () => {
|
||||
test('returns current time in specified timezone', () => {
|
||||
const utcTime = getCurrentTimeInTimezone('UTC')
|
||||
const easternTime = getCurrentTimeInTimezone('America/New_York')
|
||||
const pacificTime = getCurrentTimeInTimezone('America/Los_Angeles')
|
||||
|
||||
expect(utcTime).toBeInstanceOf(Date)
|
||||
expect(easternTime).toBeInstanceOf(Date)
|
||||
expect(pacificTime).toBeInstanceOf(Date)
|
||||
})
|
||||
|
||||
test('handles invalid timezone gracefully', () => {
|
||||
const result = getCurrentTimeInTimezone('Invalid/Timezone')
|
||||
expect(result).toBeInstanceOf(Date)
|
||||
})
|
||||
|
||||
test('timezone differences are reasonable', () => {
|
||||
const utcTime = getCurrentTimeInTimezone('UTC')
|
||||
const easternTime = getCurrentTimeInTimezone('America/New_York')
|
||||
|
||||
const timeDiff = Math.abs(utcTime.getTime() - easternTime.getTime())
|
||||
expect(timeDiff).toBeLessThan(24 * 60 * 60 * 1000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatDateInTimezone', () => {
|
||||
const testDate = new Date('2024-03-15T14:30:00.000Z')
|
||||
|
||||
test('formats date with weekday by default', () => {
|
||||
const result = formatDateInTimezone(testDate, 'UTC')
|
||||
|
||||
expect(result).toContain('March')
|
||||
expect(result).toContain('15')
|
||||
expect(result).toContain('2024')
|
||||
expect(result).toContain('2:30 PM')
|
||||
})
|
||||
|
||||
test('formats date without weekday when specified', () => {
|
||||
const result = formatDateInTimezone(testDate, 'UTC', false)
|
||||
|
||||
expect(result).toContain('March')
|
||||
expect(result).toContain('15')
|
||||
expect(result).toContain('2024')
|
||||
expect(result).toContain('2:30 PM')
|
||||
})
|
||||
|
||||
test('formats date in different timezones', () => {
|
||||
const utcResult = formatDateInTimezone(testDate, 'UTC')
|
||||
const easternResult = formatDateInTimezone(testDate, 'America/New_York')
|
||||
|
||||
expect(utcResult).toContain('2:30 PM')
|
||||
expect(easternResult).toMatch(/\d{1,2}:\d{2} (AM|PM)/)
|
||||
})
|
||||
|
||||
test('handles invalid timezone gracefully', () => {
|
||||
const result = formatDateInTimezone(testDate, 'Invalid/Timezone')
|
||||
expect(typeof result).toBe('string')
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('error handling and edge cases', () => {
|
||||
test('convertTimeToUTC handles empty strings', () => {
|
||||
expect(convertTimeToUTC('', 'UTC')).toBe('')
|
||||
expect(convertTimeToUTC('2:30 PM', '')).toBe('2:30 PM')
|
||||
})
|
||||
|
||||
test('convertUTCToUserTimezone handles empty strings', () => {
|
||||
expect(convertUTCToUserTimezone('', 'UTC')).toBe('')
|
||||
expect(convertUTCToUserTimezone('14:30', '')).toBe('14:30')
|
||||
})
|
||||
|
||||
test('convertTimeToUTC handles malformed input parts', () => {
|
||||
expect(convertTimeToUTC('2:PM', 'UTC')).toBe('2:PM')
|
||||
expect(convertTimeToUTC('2:30', 'UTC')).toBe('2:30')
|
||||
expect(convertTimeToUTC('ABC:30 PM', 'UTC')).toBe('ABC:30 PM')
|
||||
})
|
||||
|
||||
test('convertUTCToUserTimezone handles malformed UTC input', () => {
|
||||
expect(convertUTCToUserTimezone('AB:30', 'UTC')).toBe('AB:30')
|
||||
expect(convertUTCToUserTimezone('14:', 'UTC')).toBe('14:')
|
||||
expect(convertUTCToUserTimezone('14:XX', 'UTC')).toBe('14:XX')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,131 @@
|
||||
export const convertTimeToUTC = (time: string, userTimezone: string): string => {
|
||||
try {
|
||||
const [timePart, period] = time.split(' ')
|
||||
if (!timePart || !period) return time
|
||||
|
||||
const [hour, minute] = timePart.split(':')
|
||||
if (!hour || !minute) return time
|
||||
|
||||
let hour24 = Number.parseInt(hour, 10)
|
||||
const minuteNum = Number.parseInt(minute, 10)
|
||||
|
||||
if (Number.isNaN(hour24) || Number.isNaN(minuteNum)) return time
|
||||
|
||||
if (period === 'PM' && hour24 !== 12) hour24 += 12
|
||||
if (period === 'AM' && hour24 === 12) hour24 = 0
|
||||
|
||||
if (userTimezone === 'UTC')
|
||||
return `${String(hour24).padStart(2, '0')}:${String(minuteNum).padStart(2, '0')}`
|
||||
|
||||
const today = new Date()
|
||||
const year = today.getFullYear()
|
||||
const month = today.getMonth()
|
||||
const day = today.getDate()
|
||||
|
||||
const userTime = new Date(year, month, day, hour24, minuteNum)
|
||||
|
||||
const tempFormatter = new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone: userTimezone,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
})
|
||||
|
||||
const userTimeInTz = tempFormatter.format(userTime).replace(', ', 'T')
|
||||
const userTimeDate = new Date(userTimeInTz)
|
||||
const offset = userTime.getTime() - userTimeDate.getTime()
|
||||
const utcTime = new Date(userTime.getTime() + offset)
|
||||
|
||||
return `${String(utcTime.getHours()).padStart(2, '0')}:${String(utcTime.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
catch {
|
||||
return time
|
||||
}
|
||||
}
|
||||
|
||||
export const convertUTCToUserTimezone = (utcTime: string, userTimezone: string): string => {
|
||||
try {
|
||||
const [hour, minute] = utcTime.split(':')
|
||||
if (!hour || !minute) return utcTime
|
||||
|
||||
const hourNum = Number.parseInt(hour, 10)
|
||||
const minuteNum = Number.parseInt(minute, 10)
|
||||
|
||||
if (Number.isNaN(hourNum) || Number.isNaN(minuteNum)) return utcTime
|
||||
|
||||
const today = new Date()
|
||||
const dateStr = today.toISOString().split('T')[0]
|
||||
const utcDate = new Date(`${dateStr}T${String(hourNum).padStart(2, '0')}:${String(minuteNum).padStart(2, '0')}:00.000Z`)
|
||||
|
||||
return utcDate.toLocaleTimeString('en-US', {
|
||||
timeZone: userTimezone,
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
})
|
||||
}
|
||||
catch {
|
||||
return utcTime
|
||||
}
|
||||
}
|
||||
|
||||
export const isUTCFormat = (time: string): boolean => {
|
||||
return /^\d{2}:\d{2}$/.test(time)
|
||||
}
|
||||
|
||||
export const isUserFormat = (time: string): boolean => {
|
||||
return /^\d{1,2}:\d{2} (AM|PM)$/.test(time)
|
||||
}
|
||||
|
||||
const getTimezoneOffset = (timezone: string): number => {
|
||||
try {
|
||||
const now = new Date()
|
||||
const utc = new Date(now.toLocaleString('en-US', { timeZone: 'UTC' }))
|
||||
const target = new Date(now.toLocaleString('en-US', { timeZone: timezone }))
|
||||
return (target.getTime() - utc.getTime()) / (1000 * 60)
|
||||
}
|
||||
catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
export const getCurrentTimeInTimezone = (timezone: string): Date => {
|
||||
try {
|
||||
const now = new Date()
|
||||
const utcTime = now.getTime() + (now.getTimezoneOffset() * 60000)
|
||||
const targetTime = new Date(utcTime + (getTimezoneOffset(timezone) * 60000))
|
||||
return targetTime
|
||||
}
|
||||
catch {
|
||||
return new Date()
|
||||
}
|
||||
}
|
||||
|
||||
export const formatDateInTimezone = (date: Date, timezone: string, includeWeekday: boolean = true): string => {
|
||||
try {
|
||||
const dateOptions: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
timeZone: timezone,
|
||||
}
|
||||
|
||||
if (includeWeekday)
|
||||
dateOptions.weekday = 'short'
|
||||
|
||||
const timeOptions: Intl.DateTimeFormatOptions = {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
timeZone: timezone,
|
||||
}
|
||||
|
||||
return `${date.toLocaleDateString('en-US', dateOptions)} ${date.toLocaleTimeString('en-US', timeOptions)}`
|
||||
}
|
||||
catch {
|
||||
return date.toLocaleString()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user