Skip to content

Commit ca4b646

Browse files
authored
fix: parsing time stamps with time zone offsets containing colons (#291)
1 parent 71778bb commit ca4b646

File tree

14 files changed

+205
-20
lines changed

14 files changed

+205
-20
lines changed

DESCRIPTION

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ Suggests:
5656
rsconnect,
5757
spelling,
5858
testthat,
59-
webshot2
59+
webshot2,
60+
withr
6061
VignetteBuilder:
6162
knitr
6263
RdMacros:

NEWS.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
# connectapi 0.2.0.9000
1+
# Unreleased
2+
3+
- Fixed a bug where timestamps from Connect not in UTC were parsed as `NA` (#290)
4+
- Fixed a bug where timestamps sent to Connect may have added the difference between the local time zone and UTC (#291)
25

36
# connectapi 0.2.0
47

R/parse.R

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ make_timestamp <- function(input) {
1818
# TODO: make sure this is the right timestamp format
1919
return(input)
2020
}
21-
safe_format(input, "%Y-%m-%dT%H:%M:%SZ")
21+
22+
# In the call to `safe_format`:
23+
# - The format specifier adds a literal "Z" to the end of the timestamp, which
24+
# tells Connect "This is UTC".
25+
# - The `tz` argument tells R to produce times in the UTC time zone.
26+
# - The `usetz` argument says "Don't concatenate ' UTC' to the end of the string".
27+
safe_format(input, "%Y-%m-%dT%H:%M:%SZ", tz = "UTC", usetz = FALSE)
2228
}
2329

2430
ensure_columns <- function(.data, ptype) {
@@ -107,8 +113,7 @@ coerce_datetime <- function(x, to, ...) {
107113
} else if (is.numeric(x)) {
108114
vctrs::new_datetime(as.double(x), tzone = tzone(to))
109115
} else if (is.character(x)) {
110-
# Parse as ISO8601
111-
as.POSIXct(strptime(x, format = "%Y-%m-%dT%H:%M:%SZ"), tz = tzone(to))
116+
parse_connect_rfc3339(x)
112117
} else if (inherits(x, "POSIXct")) {
113118
x
114119
} else if (all(is.logical(x) & is.na(x)) && length(is.logical(x) & is.na(x)) > 0) {
@@ -118,6 +123,51 @@ coerce_datetime <- function(x, to, ...) {
118123
}
119124
}
120125

126+
# Parses a character vector of dates received from Connect, using use RFC 3339,
127+
# returning a vector of POSIXct datetimes.
128+
#
129+
# R parses character timestamps as ISO 8601. When specifying %z, it expects time
130+
# zones to be specified as `-1400` to `+1400`.
131+
#
132+
# Connect's API sends times in a specific RFC 3339 format: indicating time zone
133+
# offsets with `-14:00` to `+14:00`, and zero offset with `Z`.
134+
# https://github.com/golang/go/blob/54fe0fd43fcf8609666c16ae6d15ed92873b1564/src/time/format.go#L86
135+
# For example:
136+
# - "2023-08-22T14:13:14Z"
137+
# - "2023-08-22T15:13:14+01:00"
138+
# - "2020-01-01T00:02:03-01:00"
139+
parse_connect_rfc3339 <- function(x) {
140+
# Convert any timestamps with offsets to a format recognized by `strptime`.
141+
x <- gsub("([+-]\\d\\d):(\\d\\d)$", "\\1\\2", x)
142+
143+
# `purrr::map2_vec()` converts to POSIXct automatically, but we need
144+
# `as.POSIXct()` in there to account vectors of length 1, which it seems are
145+
# not converted.
146+
#
147+
# Parse with an inner call to `strptime()`; convert the resulting `POSIXlt`
148+
# object to `POSIXct`.
149+
#
150+
# We must specify `tz` in the inner call to correctly compute date math.
151+
# Specifying `tz` when parsing just changes the time zone without doing any
152+
# date math!
153+
#
154+
# > xlt
155+
# [1] "2024-08-29 16:36:33 EDT"
156+
# > tzone(xlt)
157+
# [1] "America/New_York"
158+
# > as.POSIXct(xlt, tz = "UTC")
159+
# [1] "2024-08-29 16:36:33 UTC"
160+
purrr::map_vec(x, function(.x) {
161+
# Times with and without offsets require different formats.
162+
format_string = ifelse(
163+
grepl("Z$", .x),
164+
"%Y-%m-%dT%H:%M:%SZ",
165+
"%Y-%m-%dT%H:%M:%S%z"
166+
)
167+
as.POSIXct(strptime(.x, format = format_string, tz = "UTC"))
168+
})
169+
}
170+
121171
vec_cast.POSIXct.double <- function(x, to, ...) {
122172
warn_experimental("vec_cast.POSIXct.double")
123173
vctrs::new_datetime(x, tzone = tzone(to))

man/ContentTask.Rd

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/EnvironmentR6.Rd

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/Vanity.Rd

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/VariantR6.Rd

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/VariantSchedule.Rd

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/VariantTask.Rd

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/connectapi-package.Rd

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)