dgkf / dgkf/parttime

gmtoff() resolves every timezone against a fixed 1970 stub, so imputed offsets are wrong for DST and for zones that have changed since

Open
#63 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
R
Stars
18
Forks
1
PR merge metrics
No merged PRs in 30d

Description

`gmtoff()` computes an offset by forcing a fixed `1970-01-01` stub into the target zone:

```r
date_stub <- lubridate::as_datetime("1970-01-01 00:00:00 GMT")
as.numeric(difftime(date_stub, lubridate::force_tz(date_stub, tzone = tzone), units = "secs")) / 60
```

Because the instant is fixed at the epoch, the result is the offset that zone had in January 1970 — not the offset in force for the date being imputed. That is wrong in three separate ways:

| zone | `gmtoff()` | true 2001-01 | true 2001-06 | true 2026-06 |
|---|---|---|---|---|
| `America/New_York` | −300 | −300 | **−240** | **−240** |
| `Asia/Kathmandu` | 330 | **345** | **345** | **345** |
| `Europe/Moscow` | 180 | 180 | **240** | 180 |
| `Pacific/Apia` | −660 | −660 | −660 | **+780** |

1. **DST is ignored.** `America/New_York` always resolves to EST, so any summer date is off by an hour.
2. **Zones that changed their standard offset since 1970 are stale.** Nepal moved to +05:45 in 1986, so `Asia/Kathmandu` is off by 15 minutes for every modern date.
3. **`Pacific/Apia` is off by nearly a day**, having crossed the date line at the end of 2011.

Only `UTC`/`GMT` are correct in every case.

### Reproducible

This is reachable through exported API — `impute_time()` documents `tz` as "a character timezone name for imputation":

```r
library(parttime)
options(parttime.assume_tz_offset = NA) # so the offset is genuinely unknown

impute_time_mid(as.parttime("2001-06-15"), tz = "America/New_York")
# tzhour = -5, but the offset in force on that date was -04:00

impute_time_mid(as.parttime("2001-06-15"), tz = "Asia/Kathmandu")
# tzhour = 5, but Nepal has been +05:45 since 1986
```

### Cause

An offset is not a property of a zone alone — it is a property of a zone *at an instant*. `gmtoff()` never receives the instant, so it cannot give the right answer for any zone that has ever changed offset. Substituting a different fixed stub (e.g. "today") would trade one set of wrong answers for another.

The fix is to resolve against the value being imputed. `format(x, "%z")` on a `POSIXct` in the target zone gives the offset in force at that instant directly, so the underlying operation is available; what is missing is threading the instant through `interpret_tz()` to `gmtoff()`. That changes the signature and the two call sites in `impute_time.partial_time()`, so it is worth agreeing the approach before implementing.

`gmtoff()` is internal and has exactly one caller, `interpret_tz()`, so the blast radius is contained.

### Adjacent, but separate

While tracing this I found a second defect in the same path, which I'd be glad to file separately if useful. `impute_time.partial_time()` assigns the offset with integer division:

```r
vctrs::field(impute_pttm, "pttm_mat")[tzhour_na, "tzhour"] <- tz %/% 60
```

That truncates every sub-hour offset, and it affects explicit offset strings too, not just zone names:

```r
impute_time_mid(x, tz = "+0530") # tzhour 5, should be 5.5
impute_time_mid(x, tz = "-0430") # tzhour -5, should be -4.5 (also wrong sign direction)
impute_time_mid(x, tz = "+0545") # tzhour 5, should be 5.75
```

India, Nepal, Newfoundland, and the Australian half-hour zones are all affected. The package's own `impute_time_min`/`impute_time_max` defaults (`"-1200"`, `"+1400"`) are whole hours, so the defaults are unaffected.

Issue created with the help of Claude Opus 5.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start by tracing gmtoff() through interpret_tz() and the two call sites in impute_time.partial_time(), then run the supplied impute_time_mid() examples for the named timezones. Done means imputed offsets reflect the target date, including DST and historical zone changes; keep the separate integer-division defect out of this change.

Written by the indexing model from the issue text.

Assessment

Tech stack
r
Domain
backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.