fix: issue w/ timepicker (#26696)
Co-authored-by: lyzno1 <yuanyouhuilyz@gmail.com> Co-authored-by: lyzno1 <92089059+lyzno1@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import dayjs from './dayjs'
|
||||
import {
|
||||
getDateWithTimezone,
|
||||
isDayjsObject,
|
||||
toDayjs,
|
||||
} from './dayjs'
|
||||
|
||||
describe('dayjs utilities', () => {
|
||||
const timezone = 'UTC'
|
||||
|
||||
test('toDayjs parses time-only strings with timezone support', () => {
|
||||
const result = toDayjs('18:45', { timezone })
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.format('HH:mm')).toBe('18:45')
|
||||
expect(result?.utcOffset()).toBe(getDateWithTimezone({ timezone }).utcOffset())
|
||||
})
|
||||
|
||||
test('toDayjs parses 12-hour time strings', () => {
|
||||
const tz = 'America/New_York'
|
||||
const result = toDayjs('07:15 PM', { timezone: tz })
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.format('HH:mm')).toBe('19:15')
|
||||
expect(result?.utcOffset()).toBe(getDateWithTimezone({ timezone: tz }).utcOffset())
|
||||
})
|
||||
|
||||
test('isDayjsObject detects dayjs instances', () => {
|
||||
const date = dayjs()
|
||||
expect(isDayjsObject(date)).toBe(true)
|
||||
expect(isDayjsObject(getDateWithTimezone({ timezone }))).toBe(true)
|
||||
expect(isDayjsObject('2024-01-01')).toBe(false)
|
||||
expect(isDayjsObject({})).toBe(false)
|
||||
})
|
||||
|
||||
test('toDayjs parses datetime strings in target timezone', () => {
|
||||
const value = '2024-05-01 12:00:00'
|
||||
const tz = 'America/New_York'
|
||||
|
||||
const result = toDayjs(value, { timezone: tz })
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.hour()).toBe(12)
|
||||
expect(result?.format('YYYY-MM-DD HH:mm')).toBe('2024-05-01 12:00')
|
||||
})
|
||||
|
||||
test('toDayjs parses ISO datetime strings in target timezone', () => {
|
||||
const value = '2024-05-01T14:30:00'
|
||||
const tz = 'Europe/London'
|
||||
|
||||
const result = toDayjs(value, { timezone: tz })
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.hour()).toBe(14)
|
||||
expect(result?.minute()).toBe(30)
|
||||
})
|
||||
|
||||
test('toDayjs handles dates without time component', () => {
|
||||
const value = '2024-05-01'
|
||||
const tz = 'America/Los_Angeles'
|
||||
|
||||
const result = toDayjs(value, { timezone: tz })
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.format('YYYY-MM-DD')).toBe('2024-05-01')
|
||||
expect(result?.hour()).toBe(0)
|
||||
expect(result?.minute()).toBe(0)
|
||||
})
|
||||
})
|
||||
@@ -10,6 +10,25 @@ dayjs.extend(timezone)
|
||||
export default dayjs
|
||||
|
||||
const monthMaps: Record<string, Day[]> = {}
|
||||
const DEFAULT_OFFSET_STR = 'UTC+0'
|
||||
const TIME_ONLY_REGEX = /^(\d{1,2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?$/
|
||||
const TIME_ONLY_12H_REGEX = /^(\d{1,2}):(\d{2})(?::(\d{2}))?\s?(AM|PM)$/i
|
||||
|
||||
const COMMON_PARSE_FORMATS = [
|
||||
'YYYY-MM-DD',
|
||||
'YYYY/MM/DD',
|
||||
'DD-MM-YYYY',
|
||||
'DD/MM/YYYY',
|
||||
'MM-DD-YYYY',
|
||||
'MM/DD/YYYY',
|
||||
'YYYY-MM-DDTHH:mm:ss.SSSZ',
|
||||
'YYYY-MM-DDTHH:mm:ssZ',
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
'YYYY-MM-DDTHH:mm',
|
||||
'YYYY-MM-DDTHH:mmZ',
|
||||
'YYYY-MM-DDTHH:mm:ss',
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS',
|
||||
]
|
||||
|
||||
export const cloneTime = (targetDate: Dayjs, sourceDate: Dayjs) => {
|
||||
return targetDate.clone()
|
||||
@@ -76,21 +95,116 @@ export const getHourIn12Hour = (date: Dayjs) => {
|
||||
return hour === 0 ? 12 : hour >= 12 ? hour - 12 : hour
|
||||
}
|
||||
|
||||
export const getDateWithTimezone = (props: { date?: Dayjs, timezone?: string }) => {
|
||||
return props.date ? dayjs.tz(props.date, props.timezone) : dayjs().tz(props.timezone)
|
||||
export const getDateWithTimezone = ({ date, timezone }: { date?: Dayjs, timezone?: string }) => {
|
||||
if (!timezone)
|
||||
return (date ?? dayjs()).clone()
|
||||
return date ? dayjs.tz(date, timezone) : dayjs().tz(timezone)
|
||||
}
|
||||
|
||||
// Asia/Shanghai -> UTC+8
|
||||
const DEFAULT_OFFSET_STR = 'UTC+0'
|
||||
export const convertTimezoneToOffsetStr = (timezone?: string) => {
|
||||
if (!timezone)
|
||||
return DEFAULT_OFFSET_STR
|
||||
const tzItem = tz.find(item => item.value === timezone)
|
||||
if(!tzItem)
|
||||
if (!tzItem)
|
||||
return DEFAULT_OFFSET_STR
|
||||
return `UTC${tzItem.name.charAt(0)}${tzItem.name.charAt(2)}`
|
||||
}
|
||||
|
||||
export const isDayjsObject = (value: unknown): value is Dayjs => dayjs.isDayjs(value)
|
||||
|
||||
export type ToDayjsOptions = {
|
||||
timezone?: string
|
||||
format?: string
|
||||
formats?: string[]
|
||||
}
|
||||
|
||||
const warnParseFailure = (value: string) => {
|
||||
if (process.env.NODE_ENV !== 'production')
|
||||
console.warn('[TimePicker] Failed to parse time value', value)
|
||||
}
|
||||
|
||||
const normalizeMillisecond = (value: string | undefined) => {
|
||||
if (!value) return 0
|
||||
if (value.length === 3) return Number(value)
|
||||
if (value.length > 3) return Number(value.slice(0, 3))
|
||||
return Number(value.padEnd(3, '0'))
|
||||
}
|
||||
|
||||
const applyTimezone = (date: Dayjs, timezone?: string) => {
|
||||
return timezone ? getDateWithTimezone({ date, timezone }) : date
|
||||
}
|
||||
|
||||
export const toDayjs = (value: string | Dayjs | undefined, options: ToDayjsOptions = {}): Dayjs | undefined => {
|
||||
if (!value)
|
||||
return undefined
|
||||
|
||||
const { timezone: tzName, format, formats } = options
|
||||
|
||||
if (isDayjsObject(value))
|
||||
return applyTimezone(value, tzName)
|
||||
|
||||
if (typeof value !== 'string')
|
||||
return undefined
|
||||
|
||||
const trimmed = value.trim()
|
||||
|
||||
if (format) {
|
||||
const parsedWithFormat = tzName
|
||||
? dayjs.tz(trimmed, format, tzName, true)
|
||||
: dayjs(trimmed, format, true)
|
||||
if (parsedWithFormat.isValid())
|
||||
return parsedWithFormat
|
||||
}
|
||||
|
||||
const timeMatch = TIME_ONLY_REGEX.exec(trimmed)
|
||||
if (timeMatch) {
|
||||
const base = applyTimezone(dayjs(), tzName).startOf('day')
|
||||
const rawHour = Number(timeMatch[1])
|
||||
const minute = Number(timeMatch[2])
|
||||
const second = timeMatch[3] ? Number(timeMatch[3]) : 0
|
||||
const millisecond = normalizeMillisecond(timeMatch[4])
|
||||
|
||||
return base
|
||||
.set('hour', rawHour)
|
||||
.set('minute', minute)
|
||||
.set('second', second)
|
||||
.set('millisecond', millisecond)
|
||||
}
|
||||
|
||||
const timeMatch12h = TIME_ONLY_12H_REGEX.exec(trimmed)
|
||||
if (timeMatch12h) {
|
||||
const base = applyTimezone(dayjs(), tzName).startOf('day')
|
||||
let hour = Number(timeMatch12h[1]) % 12
|
||||
const isPM = timeMatch12h[4]?.toUpperCase() === 'PM'
|
||||
if (isPM)
|
||||
hour += 12
|
||||
const minute = Number(timeMatch12h[2])
|
||||
const second = timeMatch12h[3] ? Number(timeMatch12h[3]) : 0
|
||||
|
||||
return base
|
||||
.set('hour', hour)
|
||||
.set('minute', minute)
|
||||
.set('second', second)
|
||||
.set('millisecond', 0)
|
||||
}
|
||||
|
||||
const candidateFormats = formats ?? COMMON_PARSE_FORMATS
|
||||
for (const fmt of candidateFormats) {
|
||||
const parsed = tzName
|
||||
? dayjs.tz(trimmed, fmt, tzName, true)
|
||||
: dayjs(trimmed, fmt, true)
|
||||
if (parsed.isValid())
|
||||
return parsed
|
||||
}
|
||||
|
||||
const fallbackParsed = tzName ? dayjs.tz(trimmed, tzName) : dayjs(trimmed)
|
||||
if (fallbackParsed.isValid())
|
||||
return fallbackParsed
|
||||
|
||||
warnParseFailure(value)
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Parse date with multiple format support
|
||||
export const parseDateWithFormat = (dateString: string, format?: string): Dayjs | null => {
|
||||
if (!dateString) return null
|
||||
@@ -103,15 +217,7 @@ export const parseDateWithFormat = (dateString: string, format?: string): Dayjs
|
||||
|
||||
// Try common date formats
|
||||
const formats = [
|
||||
'YYYY-MM-DD', // Standard format
|
||||
'YYYY/MM/DD', // Slash format
|
||||
'DD-MM-YYYY', // European format
|
||||
'DD/MM/YYYY', // European slash format
|
||||
'MM-DD-YYYY', // US format
|
||||
'MM/DD/YYYY', // US slash format
|
||||
'YYYY-MM-DDTHH:mm:ss.SSSZ', // ISO format
|
||||
'YYYY-MM-DDTHH:mm:ssZ', // ISO format (no milliseconds)
|
||||
'YYYY-MM-DD HH:mm:ss', // Standard datetime format
|
||||
...COMMON_PARSE_FORMATS,
|
||||
]
|
||||
|
||||
for (const fmt of formats) {
|
||||
|
||||
Reference in New Issue
Block a user