Date: a DST transition at local midnight makes setHours read back the wrong day
- Dominant language
- JavaScript
- Stars
- 11.3k
- Forks
- 859
- Avg merge
- 1h 30m
- Merged PRs (30d)
- 3
Description
## Summary
When a timezone's DST transition happens at local midnight, `Date` converts a wall-clock time to an instant using the UTC offset in force **before** the jump, then reads that instant back using the offset **after** it. A time in the last hour of the preceding day therefore reads back as the next day.
## Repro
```js
process.env.TZ = 'Europe/Kiev' // or run the device in that timezone
const d = new Date(1984, 2, 31) // 31 March 1984, local
d.setHours(23, 59, 59, 999)
console.log(d.getDate())
```
| | result |
|---|---|
| Hermes | `1` |
| V8 / JSC / Node | `31` |
Kyiv moved its clocks at `00:00` from 1981 through 1984, so `1984-04-01T00:00` local does not exist. Hermes resolves `1984-03-31T23:59:59.999` to an instant that it then formats as 1 April.
Instrumented on device:
```
start 1984-03-30T22:00:00.000Z getDate=31
setHours 1984-03-31T21:59:59.999Z getDate=1
```
The forward conversion applied no DST term; reading the same instant back applied one.
## Why this matters beyond a corner case
`dayjs` implements `daysInMonth()` as `endOf('month').date()` - take the last day of the month, `setHours(23, 59, 59, 999)`, read the day. Under this bug that returns `1` instead of the real length, and every consumer that builds a month grid from it renders a single cell.
We hit it through [`react-native-ui-datepicker`](https://github.com/farhoudshapouran/react-native-ui-datepicker), whose `getDaysInMonth` feeds `dayjs().daysInMonth()` straight into `Array.from({ length })`. A user picking a birth date in March 1984 saw a calendar with one day in it.
Scanning every month from 1930 to 2030 on the device under `Europe/Kiev` gives exactly four wrong answers: **March 1981, 1982, 1983, 1984** - the years the transition was at midnight. From 1985 Kyiv moved it to 02:00 and the results are correct again.
This is not limited to historical dates. Any zone that still transitions at local midnight is exposed for its **current** month: Chile, Cuba, Iran, Paraguay and parts of the Levant among them.
## Possibly related
On the same device, Hermes reports Kyiv as UTC+2/+3 for 1984, while Android's own tz database (`TZ=Europe/Kiev date` in the shell) reports MSK +3 / MSD +4, matching macOS and Node. Hermes appears to carry the zone's current standard offset backward through history. That does not cause the bug above, but it may share a root.
## Environment
- Hermes `v0.17.0`, as shipped with React Native `0.86.2`
- Android 16, Samsung SM-S916B (arm64-v8a)
- Device timezone `Europe/Kiev`
## Workaround
Compute month length with `Date.UTC`, which has no offset to resolve:
```js
const daysInMonth = (year, month) => new Date(Date.UTC(year, month + 1, 0)).getUTCDate()
```
Contributor guide
Research direction
Reproduce the Europe/Kiev case using JavaScript Date, especially setHours() followed by getDate(). Trace Hermes’s Date timezone conversion around the local-midnight DST transition and compare the result with the documented V8, JSC, and Node behavior. Done means the repro returns 31 and month-length calculations no longer produce incorrect results for the affected dates.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100