Skip to content

Commit 700ba68

Browse files
authored
Document and test rounding that can occur when parsing (#212)
* Add parsing tests related to parsing into less precise containers * Document rounding process when parsing
1 parent 03680f3 commit 700ba68

File tree

8 files changed

+188
-3
lines changed

8 files changed

+188
-3
lines changed

R/date.R

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,10 @@ date_set_zone.Date <- function(x, zone) {
908908
#' _`date_parse()` ignores both the `%z` and `%Z` commands,_ as clock treats
909909
#' Date as a _naive_ type, with a yet-to-be-specified time zone.
910910
#'
911+
#' If parsing a string with sub-daily components, such as hours, minutes or
912+
#' seconds, note that the conversion to Date will round those components to
913+
#' the nearest day. See the examples for a way to control this.
914+
#'
911915
#' @inheritParams zoned-parsing
912916
#'
913917
#' @return A Date.
@@ -931,6 +935,29 @@ date_set_zone.Date <- function(x, zone) {
931935
#' # A neat feature of `date_parse()` is the ability to parse
932936
#' # the ISO year-week-day format
933937
#' date_parse("2020-W01-2", format = "%G-W%V-%u")
938+
#'
939+
#' # ---------------------------------------------------------------------------
940+
#' # Rounding of sub-daily components
941+
#'
942+
#' # Note that rounding a string with time components will round them to the
943+
#' # nearest day if you try and parse them
944+
#' x <- c("2019-01-01 11", "2019-01-01 12")
945+
#'
946+
#' # Hour 12 rounds up to the next day
947+
#' date_parse(x, format = "%Y-%m-%d %H")
948+
#'
949+
#' # If you don't like this, one option is to just not parse the time component
950+
#' date_parse(x, format = "%Y-%m-%d")
951+
#'
952+
#' # A more general option is to parse the full string as a naive-time,
953+
#' # then round manually
954+
#' nt <- naive_time_parse(x, format = "%Y-%m-%d %H", precision = "hour")
955+
#' nt
956+
#'
957+
#' nt <- time_point_floor(nt, "day")
958+
#' nt
959+
#'
960+
#' as.Date(nt)
934961
date_parse <- function(x, ..., format = NULL, locale = clock_locale()) {
935962
x <- naive_time_parse(x, ..., format = format, precision = "day", locale = locale)
936963
as.Date(x)

R/posixt.R

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,6 +1052,10 @@ date_set_zone.POSIXt <- function(x, zone) {
10521052
#' `NA`s, or completely fails to parse, then no time zone will be able to be
10531053
#' determined. In that case, the result will use `"UTC"`.
10541054
#'
1055+
#' If manually parsing sub-second components, be aware that they will be
1056+
#' automatically rounded to the nearest second when converting them to POSIXct.
1057+
#' See the examples for a way to control this.
1058+
#'
10551059
#' @inheritParams zoned-parsing
10561060
#' @inheritParams as-zoned-time-naive-time
10571061
#'
@@ -1105,6 +1109,30 @@ date_set_zone.POSIXt <- function(x, zone) {
11051109
#' "1970-10-25 01:00:00 EST"
11061110
#' )
11071111
#' date_time_parse_abbrev(abbrev_times, "America/New_York")
1112+
#'
1113+
#' # ---------------------------------------------------------------------------
1114+
#' # Rounding of sub-second components
1115+
#'
1116+
#' # Generally, if you have a string with sub-second components, they will
1117+
#' # be ignored when parsing into a date-time
1118+
#' x <- c("2019-01-01 00:00:01.1", "2019-01-01 00:00:01.7")
1119+
#'
1120+
#' date_time_parse(x, "America/New_York")
1121+
#'
1122+
#' # If you manually try and parse those sub-second components with `%4S` to
1123+
#' # read the 2 seconds, 1 decimal point, and 1 fractional component, the
1124+
#' # fractional component will be rounded to the nearest second
1125+
#' date_time_parse(x, "America/New_York", format = "%Y-%m-%d %H:%M:%4S")
1126+
#'
1127+
#' # If you don't like this, parse the full string as a naive-time,
1128+
#' # then round manually and convert to a POSIXct
1129+
#' nt <- naive_time_parse(x, format = "%Y-%m-%d %H:%M:%S", precision = "millisecond")
1130+
#' nt
1131+
#'
1132+
#' nt <- time_point_floor(nt, "second")
1133+
#' nt
1134+
#'
1135+
#' as.POSIXct(nt, "America/New_York")
11081136
NULL
11091137

11101138
#' @rdname date-time-parse

man/date-time-parse.Rd

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/date_parse.Rd

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-date.R

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,9 +255,7 @@ test_that("`%z` and `%Z` commands are ignored", {
255255
)
256256
})
257257

258-
# TODO: We probably don't want this:
259-
# https://github.com/HowardHinnant/date/issues/657
260-
test_that("parsing into a less precise time point rounds rather than floors", {
258+
test_that("parsing into a date if you requested to parse time components rounds the time (#207)", {
261259
expect_identical(
262260
date_parse("2019-12-31 11:59:59", format = "%Y-%m-%d %H:%M:%S"),
263261
as.Date("2019-12-31")

tests/testthat/test-gregorian-year-month-day.R

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,24 @@ test_that("parsing NA returns NA", {
312312
)
313313
})
314314

315+
test_that("parsing doesn't round parsed components more precise than the resulting container (#207)", {
316+
# With year-month-day, only the year/month/day components are extracted at the end,
317+
# the hour component isn't touched
318+
expect_identical(
319+
year_month_day_parse("2019-12-31 12", format = "%Y-%m-%d %H", precision = "day"),
320+
year_month_day(2019, 12, 31)
321+
)
322+
})
323+
324+
test_that("parsing rounds parsed subsecond components more precise than the resulting container (#207)", {
325+
# Requesting `%7S` parses the full `01.1238`, and the `1238` portion is rounded up immediately
326+
# after parsing the `%S` command, not at the very end
327+
expect_identical(
328+
year_month_day_parse("2019-01-01 01:01:01.1238", format = "%Y-%m-%d %H:%M:%7S", precision = "millisecond"),
329+
year_month_day(2019, 1, 1, 1, 1, 1, 124, subsecond_precision = "millisecond")
330+
)
331+
})
332+
315333
# ------------------------------------------------------------------------------
316334
# calendar_group()
317335

tests/testthat/test-naive-time.R

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,37 @@ test_that("%Z is completely ignored", {
272272
)
273273
})
274274

275+
test_that("parsing rounds parsed components more precise than the resulting container (#207)", {
276+
expect_identical(
277+
naive_time_parse("2019-12-31 11", format = "%Y-%m-%d %H", precision = "day"),
278+
as_naive_time(year_month_day(2019, 12, 31))
279+
)
280+
expect_identical(
281+
naive_time_parse("2019-12-31 12", format = "%Y-%m-%d %H", precision = "day"),
282+
as_naive_time(year_month_day(2020, 1, 1))
283+
)
284+
285+
# If you don't try and parse them, it won't round
286+
expect_identical(
287+
naive_time_parse("2019-12-31 12", format = "%Y-%m-%d", precision = "day"),
288+
as_naive_time(year_month_day(2019, 12, 31))
289+
)
290+
})
291+
292+
test_that("parsing rounds parsed subsecond components more precise than the resulting container (#207)", {
293+
# Default N for milliseconds is 6, so `%6S` (2 hour seconds, 1 for decimal, 3 for subseconds)
294+
expect_identical(
295+
naive_time_parse("2019-01-01 01:01:01.1238", format = "%Y-%m-%d %H:%M:%S", precision = "millisecond"),
296+
as_naive_time(year_month_day(2019, 1, 1, 1, 1, 1, 123, subsecond_precision = "millisecond"))
297+
)
298+
299+
# Requesting `%7S` parses the full `01.1238`, and the `1238` portion is rounded up
300+
expect_identical(
301+
naive_time_parse("2019-01-01 01:01:01.1238", format = "%Y-%m-%d %H:%M:%7S", precision = "millisecond"),
302+
as_naive_time(year_month_day(2019, 1, 1, 1, 1, 1, 124, subsecond_precision = "millisecond"))
303+
)
304+
})
305+
275306
# ------------------------------------------------------------------------------
276307
# format()
277308

tests/testthat/test-zoned-time.R

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,34 @@ test_that("`x` is translated to UTF-8", {
230230
)
231231
})
232232

233+
test_that("leftover subseconds result in a parse failure", {
234+
x <- "2019-01-01 01:01:01.1238-05:00[America/New_York]"
235+
236+
# This is fine
237+
expect_identical(
238+
zoned_time_parse_complete(x, precision = "microsecond"),
239+
as_zoned_time(as_naive_time(year_month_day(2019, 1, 1, 1, 1, 1, 123800, subsecond_precision = "microsecond")), "America/New_York")
240+
)
241+
242+
# This defaults to `%6S`, which parses `01.123` then stops,
243+
# leaving a `8` for `%z` to parse, resulting in a failure. Because everything
244+
# fails, we get a UTC time zone.
245+
expect_identical(
246+
expect_warning(zoned_time_parse_complete(x, precision = "millisecond"), class = "clock_warning_parse_failures"),
247+
as_zoned_time(naive_seconds(NA) + duration_milliseconds(NA), zone = "UTC")
248+
)
249+
})
250+
251+
test_that("parsing rounds parsed subsecond components more precise than the resulting container (#207)", {
252+
x <- "2019-01-01 01:01:01.1238-05:00[America/New_York]"
253+
254+
# Requesting `%7S` parses the full `01.1238`, and the `1238` portion is rounded up
255+
expect_identical(
256+
zoned_time_parse_complete(x, precision = "millisecond", format = "%Y-%m-%d %H:%M:%7S%Ez[%Z]"),
257+
as_zoned_time(as_naive_time(year_month_day(2019, 1, 1, 1, 1, 1, 124, subsecond_precision = "millisecond")), "America/New_York")
258+
)
259+
})
260+
233261
# ------------------------------------------------------------------------------
234262
# zoned_time_parse_abbrev()
235263

0 commit comments

Comments
 (0)