Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 64 additions & 1 deletion src/plugin/timezone/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,51 @@ const getDateTimeFormat = (timezone, options = {}) => {
return dtf
}

/**
* Parse US locale date string to date components
* @param {string} str - US locale date string
* (e.g., '3/9/2025, 2:00:00 AM' '3/9/2025, 2:00:00.123 AM')
*/
function parseUSLocaleDate(str) {
const [datePart, timePartRaw] = str.split(',').map(s => s.trim())

const [month, day, year] = datePart.split('/').map(Number)

// Split "2:00:00.123 AM"
const parts = timePartRaw.split(' ')
const timePart = parts[0]
const period = parts[1] // AM / PM

const [h, m, sRaw] = timePart.split(':')

let second = 0
let millisecond = 0

if (sRaw.includes('.')) {
const [sec, ms] = sRaw.split('.')
second = Number(sec)
millisecond = Number(ms)
} else {
second = Number(sRaw)
}

let hour = Number(h)
const minute = Number(m)

if (period === 'PM' && hour !== 12) hour += 12
if (period === 'AM' && hour === 12) hour = 0

return {
year,
month,
day,
hour,
minute,
second,
millisecond
}
}

export default (o, c, d) => {
let defaultTimezone

Expand Down Expand Up @@ -96,7 +141,25 @@ export default (o, c, d) => {
const oldOffset = this.utcOffset()
const date = this.toDate()
const target = date.toLocaleString('en-US', { timeZone: timezone })
const diff = Math.round((date - new Date(target)) / 1000 / 60)


// Use string parsing instead of new Date(target) to avoid DST transition errors
const {
day, hour, minute
} = parseUSLocaleDate(target)
let deltaDay = date.getDate() - day
// Cross month adjustment
if (Math.abs(deltaDay) > 1) deltaDay = -Math.sign(deltaDay)

const diff = [
deltaDay,
(date.getHours() - hour),
(date.getMinutes() - minute)
].reduce((sum, v, i) => {
const factors = [24 * 60, 60, 1]
return sum + (v * factors[i])
}, 0)

const offset = (-Math.round(date.getTimezoneOffset() / 15) * 15) - diff
const isUTC = !Number(offset)
let ins
Expand Down
13 changes: 12 additions & 1 deletion src/plugin/utc/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,19 @@ export default (option, Dayjs, dayjs) => {
const localTimezoneOffset = this.$u
? this.toDate().getTimezoneOffset() : -1 * this.utcOffset()
ins = this.local().add(offset + localTimezoneOffset, MIN)

// Fix DST transition offset mismatch
// Problem: After adding offset, DST transition can cause timezone offset to change
// Example: dayjs.tz('2012-03-11 02:59:59', 'America/New_York').format()
// Expected: '2012-03-11T03:59:59-04:00'
// Actual: '2012-03-11T04:59:59-04:00'
// Solution: Recalculate offset after add() and adjust for any DST-induced changes
const newLocalTimezoneOffset = ins.$u
? ins.toDate().getTimezoneOffset() : (-1 * ins.utcOffset())
ins = ins.local().add(newLocalTimezoneOffset - localTimezoneOffset, MIN)

ins.$offset = offset
ins.$x.$localOffset = localTimezoneOffset
ins.$x.$localOffset = newLocalTimezoneOffset

return ins
}
Expand Down
6 changes: 6 additions & 0 deletions test/timezone.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,9 @@ it('UTC diff in DST', () => {
expect(day1.diff(day2, 'd'))
.toBe(-3)
})

it('DST transition case test', () => {
// Test DST transition boundary case
const result = dayjs('2025-11-02T09:00:00Z').tz('utc').startOf('day').toISOString()
expect(result).toBe('2025-11-02T00:00:00.000Z')
})