diff --git a/NEWS.md b/NEWS.md index 9de21ce6f..f216e2a26 100644 --- a/NEWS.md +++ b/NEWS.md @@ -58,6 +58,8 @@ 12. `print.data.table()` now truncates long character columns and list-column summaries by default to avoid horizontal console overflow, [#7718](https://github.com/Rdatatable/data.table/issues/7718). When `datatable.prettyprint.char` is `NULL` (the default), the truncation limit is now dynamically calculated based on the available console width. Use `options(datatable.prettyprint.char=Inf)` for the old default behavior (never truncate). Thanks @tdhock for the report and @venom1204 for the fix. +13. `as.IDate()` and `as.ITime()` now preserve names, matching base `as.Date()` behavior, [#7252](https://github.com/Rdatatable/data.table/issues/7252). Thanks @DavisVaughan for the report and @venom1204 for the PR. + ### Notes 1. {data.table} now depends on R 3.5.0 (2018). diff --git a/R/IDateTime.R b/R/IDateTime.R index 63588e775..0e20aec86 100644 --- a/R/IDateTime.R +++ b/R/IDateTime.R @@ -5,6 +5,11 @@ as.IDate = function(x, ...) UseMethod("as.IDate") +copy_names = function(ans, nm) { + if (!is.null(nm)) setattr(ans, "names", nm) + ans +} + as.IDate.default = function(x, ..., tz = attr(x, "tzone", exact=TRUE)) { if (is.null(tz)) tz = "UTC" if (is.character(x)) { @@ -15,32 +20,36 @@ as.IDate.default = function(x, ..., tz = attr(x, "tzone", exact=TRUE)) { } as.IDate.numeric = function(x, origin = "1970-01-01", ...) { + nm = names(x) if (origin=="1970-01-01") { - # standard epoch x = as.integer(x) class(x) = c("IDate", "Date") # We used to use structure() here because class(x)<- copied several times in R before v3.1.0 # Since R 3.1.0 improved class()<- and data.table's oldest oldest supported R is now 3.1.0, we can use class<- again # structure() contains a match() and replace for specials, which we don't need. # class()<- ensures at least 1 shallow copy as appropriate is returned. - x + copy_names(x, nm) } else { - # only call expensive as.IDate.character if we have to - as.IDate(origin, ...) + as.integer(x) + ans = as.IDate(origin, ...) + as.integer(x) + copy_names(ans, nm) } } as.IDate.Date = function(x, ...) { - x = as.integer(x) # if already integer, x will be left unchanged as the original input - class(x) = c("IDate", "Date") # class()<- will copy if as.integer() did not create, and may not if it did we hope - x # always return a new object + nm = names(x) + x = as.integer(x) + class(x) = c("IDate", "Date") + copy_names(x, nm) } as.IDate.POSIXct = function(x, tz = attr(x, "tzone", exact=TRUE), ...) { - if (is_utc(tz)) - (setattr(as.integer(as.numeric(x) %/% 86400L), "class", c("IDate", "Date"))) # %/% returns new object so can use setattr() on it; wrap with () to return visibly - else + if (is_utc(tz)) { + ans = as.integer(as.numeric(x) %/% 86400L) + setattr(ans, "class", c("IDate", "Date")) + copy_names(ans, names(x)) + } else { as.IDate(as.Date(x, tz = tz %||% '', ...)) + } } as.IDate.IDate = function(x, ...) x @@ -155,13 +164,19 @@ as.ITime.POSIXct = function(x, tz = attr(x, "tzone", exact=TRUE), ...) { } as.ITime.numeric = function(x, ms = 'truncate', ...) { + nm = names(x) secs = clip_msec(x, ms) %% 86400L # the %% here ensures a local copy is obtained; the truncate as.integer() may not copy - (setattr(secs, "class", "ITime")) + setattr(secs, "class", "ITime") + copy_names(secs, nm) } as.ITime.character = function(x, format, ...) { + nm = names(x) x = unclass(x) - if (!missing(format)) return(as.ITime(strptime(x, format = format, ...), ...)) + if (!missing(format)) { + ans = as.ITime(strptime(x, format = format, ...), ...) + return(copy_names(ans, nm)) + } # else allow for mixed formats, such as test 1189 where seconds are caught despite varying format y = strptime(x, format = "%H:%M:%OS", ...) w = which(is.na(y)) @@ -181,12 +196,16 @@ as.ITime.character = function(x, format, ...) { w = w[!nna] } } - as.ITime(y, ...) + ans = as.ITime(y, ...) + copy_names(ans, nm) } as.ITime.POSIXlt = function(x, ms = 'truncate', ...) { + nm = names(x) secs = clip_msec(x$sec, ms) - (setattr(with(x, secs + min * 60L + hour * 3600L), "class", "ITime")) # () wrap to return visibly + ans = with(x, secs + min * 60L + hour * 3600L) + setattr(ans, "class", "ITime") + copy_names(ans, nm) } as.ITime.times = function(x, ms = 'truncate', ...) { diff --git a/inst/tests/tests.Rraw b/inst/tests/tests.Rraw index 90e33145e..82eaf4b8e 100644 --- a/inst/tests/tests.Rraw +++ b/inst/tests/tests.Rraw @@ -21669,3 +21669,15 @@ test(2375.3, print(data.table(x=c("short", "abcdefghijklmnopqrstuvwxyz"))), outp test(2375.4, print(data.table(x="abcdefghijklmnopqrstuvwxyz")), output="abcdefghijklmnopqrstuvwxyz", options=list(width=200, datatable.prettyprint.char=NULL)) test(2375.5, print(data.table(id=1L, score=99.1, txt="abcdefghijklmnopqrstuvwxyz")), output="abcdefghijklmn...", options=list(width=20, datatable.prettyprint.char=NULL)) test(2375.6, print(data.table(x=rep("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1e6)), topn=1), output="1000000: ABCDEFGHIJKLM...", options=list(width=25, datatable.prettyprint.char=NULL)) + +# #7252 as.IDate()/as.ITime preserve names +test(2376.01, names(as.IDate(c(a = "2019-01-01"))), "a") +test(2376.02, names(c(a = as.IDate("2019-01-01"))), "a") +test(2376.03, names(as.ITime(c(a = "12:00:00"))), "a") +test(2376.04, names(as.IDate(structure(as.POSIXct("2019-01-01 12:00:00"), names = "a"))), "a") +test(2376.05, names(as.ITime(structure(3600, names = "a"))), "a") +test(2376.06, names(as.IDate(c(a = 18000))), "a") +test(2376.07, names(as.IDate(c(a = 1), origin = "2020-01-01")), "a") +test(2376.08, names(as.ITime(c(a = "12-00-00"), format = "%H-%M-%S")), "a") +test(2376.09, names(as.IDate(as.POSIXct(c(a = "2019-01-01"), tz="UTC"))), "a") +test(2376.10, names(as.IDate(as.POSIXct(c(a = "2019-01-01"), tz="America/New_York"))), "a")