Skip to content

Commit 67e1f87

Browse files
committed
Merge remote-tracking branch 'origin/master' into issue777666777
2 parents 8232e74 + 895067d commit 67e1f87

9 files changed

Lines changed: 148 additions & 36 deletions

File tree

.ci/atime/tests.R

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,14 @@ for(retGrp_chr in c("T","F"))extra.test.list[[sprintf(
6464
# - seconds.limit: The maximum median timing (in seconds) of an expression. No timings for larger N are computed past that threshold. Default of 0.01 seconds should be sufficient for most tests.
6565
# - Historical references (short version name = commit SHA1).
6666
# For historical regressions, use version names 'Before', 'Regression', and 'Fixed'
67-
# When there was no regression, use 'Slow' and 'Fast'
67+
# When there was no regression, use 'Slow' and 'Fast'
68+
# - Caveat: atime installs each version as a separate package but runs them all in ONE R session.
69+
# State registered outside a package's own namespace therefore persists across versions and can
70+
# mask a regression. The clearest case is S3 methods for base group generics (Ops, Math, Summary)
71+
# and helpers like chooseOpsMethod: these register into the base namespace's S3 table
72+
# (get(".__S3MethodsTable__.", envir = baseenv())), which is shared. If one version registers such
73+
# a method, it stays active while a version that lacks it is measured. When a test depends on
74+
# such state, reset it inside `expr` to match the version under test (see the #7213 test below).
6875
# @note Please check https://github.com/tdhock/atime/blob/main/vignettes/data.table.Rmd for more information.
6976
# nolint start: undesirable_operator_linter. ':::' needed+appropriate here.
7077
test.list <- atime::atime_test_list(
@@ -335,8 +342,8 @@ test.list <- atime::atime_test_list(
335342
L = as.data.table(as.character(rnorm(N, 1, 0.5)))
336343
setkey(L, V1)
337344
},
338-
Fast = "680b5e8e6d3f16a09dfb2f86ac7b2ce5ce70c3f1", # Merge commit in the PR (https://github.com/Rdatatable/data.table/pull/4501/commits) that fixes the issue
339-
Slow = "3ca83738d70d5597d9e168077f3768e32569c790", # Circa 2024 master parent of close-to-last merge commit (https://github.com/Rdatatable/data.table/commit/353dc7a6b66563b61e44b2fa0d7b73a0f97ca461) in the PR (https://github.com/Rdatatable/data.table/pull/4501/commits) that fixes the issue
345+
Fast = "680b5e8e6d3f16a09dfb2f86ac7b2ce5ce70c3f1", # Merge commit in the PR (https://github.com/Rdatatable/data.table/pull/4501/commits) that fixes the issue
346+
Slow = "3ca83738d70d5597d9e168077f3768e32569c790", # Circa 2024 master parent of close-to-last merge commit (https://github.com/Rdatatable/data.table/commit/353dc7a6b66563b61e44b2fa0d7b73a0f97ca461) in the PR (https://github.com/Rdatatable/data.table/pull/4501/commits) that fixes the issue
340347
Slower = "cacdc92df71b777369a217b6c902c687cf35a70d", # Circa 2020 parent of the first commit (https://github.com/Rdatatable/data.table/commit/74636333d7da965a11dad04c322c752a409db098) in the PR (https://github.com/Rdatatable/data.table/pull/4501/commits) that fixes the issue
341348
## New DT can safely retain key.
342349
expr = data.table:::`[.data.table`(L, , .SD)),
@@ -371,7 +378,7 @@ test.list <- atime::atime_test_list(
371378
x = rep(NA, N)
372379
x[i] = i
373380
x
374-
})
381+
})
375382
},
376383
Slow = "fd24a3105953f7785ea7414678ed8e04524e6955", # Parent of the merge commit (https://github.com/Rdatatable/data.table/commit/ed72e398df76a0fcfd134a4ad92356690e4210ea) of the PR (https://github.com/Rdatatable/data.table/pull/5054) that fixes the issue
377384
Fast = "ed72e398df76a0fcfd134a4ad92356690e4210ea", # Merge commit of the PR (https://github.com/Rdatatable/data.table/pull/5054) that fixes the issue
@@ -400,6 +407,35 @@ test.list <- atime::atime_test_list(
400407
Fast = "2715663fcf0344c3f7c73241d391d8de347bdb9d", # Merge commit of the PR that improves efficiency
401408
expr = data.table:::as.data.table.array(arr, na.rm=FALSE)),
402409

410+
# Date-IDate subtraction became much slower when chooseOpsMethod.IDate was added in #7213.
411+
"Date-IDate subtraction regression in #7213" = atime::atime_test(
412+
N = as.integer(10^seq(1, 5, by=0.5)),
413+
setup = {
414+
short_date = as.Date("2000-01-01") + seq_len(40L)
415+
long_date = as.Date("2000-01-01") + seq_len(N)
416+
},
417+
Before = "84b0e32f7a1bfdd8ec3a2c4012010b3ec072b31f", # Parent of the regression commit.
418+
Regression = "cfa9f49bd27195962573ad493a31600d173abc5c", # Merge commit of #7213 which added chooseOpsMethod.IDate.
419+
seconds.limit = 1,
420+
# Reset the shared base S3 table to match the version under test (see "Caveat" in the intro above).
421+
expr = {
422+
ns = environment(data.table::as.IDate)
423+
s3_table = get(".__S3MethodsTable__.", envir = baseenv())
424+
s3_generics = c("chooseOpsMethod.IDate" = "chooseOpsMethod", "-.IDate" = "-")
425+
for (s3_method in names(s3_generics)) {
426+
if (exists(s3_method, envir = s3_table, inherits = FALSE)) {
427+
rm(list = s3_method, envir = s3_table)
428+
}
429+
if (exists(s3_method, envir = ns, inherits = FALSE)) {
430+
base::registerS3method(
431+
s3_generics[[s3_method]], "IDate",
432+
get(s3_method, envir = ns, inherits = FALSE),
433+
envir = ns)
434+
}
435+
}
436+
outer(short_date, data.table::as.IDate(long_date), `-`)
437+
}),
438+
403439
# https://github.com/Rdatatable/data.table/pull/7144 added the speedup code and this performance test.
404440
"isoweek improved in #7144" = atime::atime_test(
405441
setup = {

.dev/cc.R

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ sourceImports = function(path=getwd(), quiet=FALSE) {
5151
# https://stat.ethz.ch/pipermail/r-devel/2024-April/083319.html
5252
for (import in imported) assign(import, getExportedValue(entry[[1L]], import), .GlobalEnv)
5353
}
54-
return(invisible())
54+
invisible()
5555
}
5656

5757
cc = function(test=FALSE, clean=FALSE, debug=FALSE, omp=!debug, path=Sys.getenv("PROJ_PATH", unset=normalizePath(".")), CC="gcc", quiet=FALSE) {
@@ -70,7 +70,7 @@ cc = function(test=FALSE, clean=FALSE, debug=FALSE, omp=!debug, path=Sys.getenv(
7070
dll = grep("data_table.so", dll, fixed=TRUE, value=TRUE)
7171
sapply(dll, dyn.unload)
7272
gc()
73-
73+
7474
# hack for windows under mingw-like environment
7575
# PROJ_PATH must be windows like i.e.: export PROJ_PATH=$(Rscript -e 'getwd()')
7676
if (.Platform$OS.type == "windows") {
@@ -90,10 +90,11 @@ cc = function(test=FALSE, clean=FALSE, debug=FALSE, omp=!debug, path=Sys.getenv(
9090
if (!quiet) cat(getwd(),"\n")
9191
if (clean) system("rm *.o *.so")
9292
OMP = if (omp) "openmp" else "no-openmp"
93+
R = file.path(R.home("bin"), "R")
9394
if (debug) {
94-
cmd = sprintf(R"(MAKEFLAGS='-j CC=%s PKG_CFLAGS=-f% CFLAGS=-std=c11\ -O0\ -ggdb\ %s\ -pedantic' R CMD SHLIB -d -o data_table.so *.c)", CC, OMP, W32)
95+
cmd = sprintf(R"(MAKEFLAGS='-j CC=%s PKG_CFLAGS=-f%s CFLAGS=-std=c11\ -O0\ -ggdb\ %s\ -pedantic' %s CMD SHLIB -d -o data_table.so *.c)", CC, OMP, W32, R)
9596
} else {
96-
cmd = sprintf(R"(MAKEFLAGS='-j CC=%s CFLAGS=-f%s\ -std=c11\ -O3\ -pipe\ -Wall\ -pedantic\ -Wstrict-prototypes\ -isystem\ /usr/share/R/include\ %s\ -fno-common' R CMD SHLIB -o data_table.so *.c)", CC, OMP, W32)
97+
cmd = sprintf(R"(MAKEFLAGS='-j CC=%s CFLAGS=-f%s\ -std=c11\ -O3\ -pipe\ -Wall\ -pedantic\ -Wstrict-prototypes\ -isystem\ %s\ %s\ -fno-common' %s CMD SHLIB -o data_table.so *.c)", CC, OMP, R.home("include"), W32, R)
9798
# the -isystem suppresses strict-prototypes warnings from R's headers, #5477. Look at the output to see what -I is and pass the same path to -isystem.
9899
# TODO add -Wextra too?
99100
}
@@ -126,4 +127,3 @@ cc = function(test=FALSE, clean=FALSE, debug=FALSE, omp=!debug, path=Sys.getenv(
126127
}
127128

128129
dd = function(omp=FALSE)cc(test=FALSE,debug=TRUE,omp=omp,clean=TRUE)
129-

NAMESPACE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ S3method(rep, IDate)
206206
S3method(rep, ITime)
207207
S3method(round, IDate)
208208
S3method(round, ITime)
209+
S3method(is.numeric, IDate)
210+
S3method(is.numeric, ITime)
209211
S3method(trunc, ITime)
210212
S3method(seq, IDate)
211213
S3method(seq, ITime)

NEWS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
3. options `"datatable.old.matrix.autoname"` is now `FALSE` by default, meaning `names(data.table(x=1, cbind(1)))` is now `c("x", "V2")`. Toggle the option to retain the old behavior for now; future releases will work to remove this possibility. See the release notes for 1.18.0, item 1 under `NOTE OF INTENDED FUTURE POTENTIAL BREAKING CHANGES`.
1414

15+
4. `is.numeric()` now returns `FALSE` for `IDate` and `ITime`, since these classes don't support arbitrary numeric operations, [#7746](https://github.com/Rdatatable/data.table/issues/7746). Thanks @aitap for the report and fix.
16+
1517
### NEW FEATURES
1618

1719
1. `nafill()`, `setnafill()` extended to work on logical, factor and character vectors (part of [#3992](https://github.com/Rdatatable/data.table/issues/3992)). Includes support for `Date`, `IDate`, `POSIXct` and character vectors. Thanks @jangorecki for the request and @jangorecki, @MichaelChirico and @ben-schwen for the PRs.
@@ -64,6 +66,10 @@
6466

6567
13. `rbindlist()` (and therefore the `rbind()` method for `data.table`s) no longer raises an error upon encountering more than approximately 50000 columns in a list entry, [#7793](https://github.com/Rdatatable/data.table/issues/7793). The bug was introduced in `data.table` version 1.18.2.1. Thanks to @rickhelmus for the report and @aitap for the fix.
6668

69+
14. Subtracting an `IDate` from a `Date` is fast again by avoiding unnecessary conversion to `POSIXlt`/`POSIXct`, [#7825](https://github.com/Rdatatable/data.table/issues/7825). Thanks @gilesheywood for the report and @ben-schwen for the fix.
70+
71+
15. `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.
72+
6773
### Notes
6874

6975
1. {data.table} now depends on R 3.5.0 (2018).

R/IDateTime.R

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55

66
as.IDate = function(x, ...) UseMethod("as.IDate")
77

8+
copy_names = function(ans, nm) {
9+
if (!is.null(nm)) setattr(ans, "names", nm)
10+
ans
11+
}
12+
813
as.IDate.default = function(x, ..., tz = attr(x, "tzone", exact=TRUE)) {
914
if (is.null(tz)) tz = "UTC"
1015
if (is.character(x)) {
@@ -17,30 +22,35 @@ as.IDate.default = function(x, ..., tz = attr(x, "tzone", exact=TRUE)) {
1722
as.IDate.numeric = function(x, origin = "1970-01-01", ...) {
1823
if (origin=="1970-01-01") {
1924
# standard epoch
25+
nm = names(x)
2026
x = as.integer(x)
2127
class(x) = c("IDate", "Date")
2228
# We used to use structure() here because class(x)<- copied several times in R before v3.1.0
2329
# 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
2430
# structure() contains a match() and replace for specials, which we don't need.
2531
# class()<- ensures at least 1 shallow copy as appropriate is returned.
26-
x
32+
copy_names(x, nm)
2733
} else {
2834
# only call expensive as.IDate.character if we have to
29-
as.IDate(origin, ...) + as.integer(x)
35+
as.IDate(origin, ...) + copy_names(as.integer(x), names(x))
3036
}
3137
}
3238

3339
as.IDate.Date = function(x, ...) {
40+
nm = names(x)
3441
x = as.integer(x) # if already integer, x will be left unchanged as the original input
3542
class(x) = c("IDate", "Date") # class()<- will copy if as.integer() did not create, and may not if it did we hope
36-
x # always return a new object
43+
copy_names(x, nm)
3744
}
3845

3946
as.IDate.POSIXct = function(x, tz = attr(x, "tzone", exact=TRUE), ...) {
40-
if (is_utc(tz))
41-
(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
42-
else
47+
if (is_utc(tz)) {
48+
ans = as.integer(as.numeric(x) %/% 86400L) # %/% returns a new object, so setattr() is safe
49+
setattr(ans, "class", c("IDate", "Date"))
50+
copy_names(ans, names(x))
51+
} else {
4352
as.IDate(as.Date(x, tz = tz %||% '', ...))
53+
}
4454
}
4555

4656
as.IDate.IDate = function(x, ...) x
@@ -51,6 +61,10 @@ as.Date.IDate = function(x, ...) {
5161
}
5262

5363
mean.IDate =
64+
function(x, ...) {
65+
x = unclass(x)
66+
as.IDate(NextMethod())
67+
}
5468
seq.IDate =
5569
c.IDate =
5670
cut.IDate =
@@ -90,6 +104,8 @@ round.IDate = function(x, digits=c("weeks", "months", "quarters", "years"), ...)
90104
quarters = ISOdate(year(x), 3L * (quarter(x)-1L) + 1L, 1L),
91105
years = ISOdate(year(x), 1L, 1L)))
92106
}
107+
# Dates aren't simple numbers, and round.IDate doesn't accept numeric 'digits'.
108+
is.numeric.IDate = function(x) FALSE
93109

94110
chooseOpsMethod.IDate = function(x, y, mx, my, cl, reverse) inherits(y, "Date")
95111

@@ -106,12 +122,23 @@ chooseOpsMethod.IDate = function(x, y, mx, my, cl, reverse) inherits(y, "Date")
106122
}
107123
if (inherits(e1, "Date") && inherits(e2, "Date"))
108124
stopf("binary + is not defined for \"IDate\" objects")
109-
(setattr(as.integer(unclass(e1) + unclass(e2)), "class", c("IDate", "Date"))) # () wrap to return visibly
125+
res = unclass(e1) + unclass(e2)
126+
nm = names(res)
127+
ans = as.integer(res)
128+
setattr(ans, "class", c("IDate", "Date"))
129+
copy_names(ans, nm)
110130
}
111131

112132
`-.IDate` = function(e1, e2) {
113133
if (!inherits(e1, "IDate")) {
114-
if (inherits(e1, 'Date')) return(base::`-.Date`(e1, e2))
134+
if (inherits(e1, "Date")) {
135+
if (!inherits(e2, "Date")) return(base::`-.Date`(e1, e2))
136+
#7825 avoid base::`-.Date` to avoid conversion from IDate to POSIXlt/POSIXct
137+
ans = unclass(e1) - unclass(e2)
138+
setattr(ans, "class", "difftime")
139+
setattr(ans, "units", "days")
140+
return(ans)
141+
}
115142
stopf("can only subtract from \"IDate\" objects")
116143
}
117144
if (storage.mode(e1) != "integer")
@@ -126,14 +153,16 @@ chooseOpsMethod.IDate = function(x, y, mx, my, cl, reverse) inherits(y, "Date")
126153
return(base::`-.Date`(as.Date(e1), e2))
127154
# can't call base::.Date directly (last line of base::`-.Date`) as tried in PR#3168 because ?.Date states "Internal objects in the base package most of which are only user-visible because of the special nature of the base namespace."
128155
}
129-
ans = as.integer(unclass(e1) - unclass(e2))
156+
res = unclass(e1) - unclass(e2)
157+
nm = names(res)
158+
ans = as.integer(res)
130159
if (inherits(e2, "Date")) {
131160
setattr(ans, "class", "difftime")
132161
setattr(ans, "units", "days")
133162
} else {
134163
setattr(ans, "class", c("IDate", "Date"))
135164
}
136-
ans
165+
copy_names(ans, nm)
137166
}
138167

139168

@@ -155,13 +184,18 @@ as.ITime.POSIXct = function(x, tz = attr(x, "tzone", exact=TRUE), ...) {
155184
}
156185

157186
as.ITime.numeric = function(x, ms = 'truncate', ...) {
187+
nm = names(x)
158188
secs = clip_msec(x, ms) %% 86400L # the %% here ensures a local copy is obtained; the truncate as.integer() may not copy
159-
(setattr(secs, "class", "ITime"))
189+
setattr(secs, "class", "ITime")
190+
copy_names(secs, nm)
160191
}
161192

162193
as.ITime.character = function(x, format, ...) {
194+
nm = names(x)
163195
x = unclass(x)
164-
if (!missing(format)) return(as.ITime(strptime(x, format = format, ...), ...))
196+
if (!missing(format)) {
197+
return(copy_names(as.ITime(strptime(x, format = format, ...), ...), nm))
198+
}
165199
# else allow for mixed formats, such as test 1189 where seconds are caught despite varying format
166200
y = strptime(x, format = "%H:%M:%OS", ...)
167201
w = which(is.na(y))
@@ -181,18 +215,23 @@ as.ITime.character = function(x, format, ...) {
181215
w = w[!nna]
182216
}
183217
}
184-
as.ITime(y, ...)
218+
copy_names(as.ITime(y, ...), nm)
185219
}
186220

187221
as.ITime.POSIXlt = function(x, ms = 'truncate', ...) {
222+
nm = names(x)
188223
secs = clip_msec(x$sec, ms)
189-
(setattr(with(x, secs + min * 60L + hour * 3600L), "class", "ITime")) # () wrap to return visibly
224+
ans = with(x, secs + min * 60L + hour * 3600L)
225+
setattr(ans, "class", "ITime")
226+
copy_names(ans, nm)
190227
}
191228

192229
as.ITime.times = function(x, ms = 'truncate', ...) {
230+
nm = names(x)
193231
secs = 86400L * (unclass(x) %% 1L)
194232
secs = clip_msec(secs, ms)
195-
(setattr(secs, "class", "ITime")) # the first line that creates sec will create a local copy so we can use setattr() to avoid potential copy of class()<-
233+
setattr(secs, "class", "ITime") # the first line that creates sec will create a local copy so we can use setattr() to avoid potential copy of class()<-
234+
copy_names(secs, nm)
196235
}
197236

198237
as.character.ITime = format.ITime = function(x, ...) {
@@ -248,6 +287,9 @@ round.ITime = function(x, digits = c("hours", "minutes"), ...)
248287
"class", "ITime"))
249288
}
250289

290+
# Day times aren't simple numbers, and round.ITime doesn't accept numeric 'digits'.
291+
is.numeric.ITime = function(x) FALSE
292+
251293
trunc.ITime = function(x, units = c("hours", "minutes"), ...)
252294
{
253295
(setattr(switch(match.arg(units),
@@ -272,7 +314,11 @@ unique.ITime = function(x, ...) {
272314
}
273315

274316
# various methods to ensure ITime class is retained, #3628
275-
mean.ITime = seq.ITime = c.ITime = function(x, ...) as.ITime(NextMethod())
317+
mean.ITime = function(x, ...) {
318+
x = unclass(x)
319+
as.ITime(NextMethod())
320+
}
321+
c.ITime = seq.ITime = function(...) as.ITime(NextMethod())
276322

277323

278324
# create a data.table with IDate and ITime columns

R/data.table.R

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,7 @@ replace_dot_alias = function(e) {
567567
stopf("When by and keyby are both provided, keyby must be TRUE or FALSE")
568568
}
569569
if (missing(by)) { missingby=TRUE; by=bysub=NULL } # possible when env is used, PR#4304
570-
else if (verbose && !is.null(env)) catf("Argument '%s' after substitute: %s\n", "by", paste(deparse(bysub, width.cutoff=500L), collapse="\n"))
570+
else if (verbose && !is.null(env)) catf("Argument '%s' after substitute: %s\n", "by", deparse1(bysub, collapse="\n"))
571571
}
572572
bynull = !missingby && is.null(by) #3530
573573
byjoin = !is.null(by) && is.symbol(bysub) && bysub==".EACHI"
@@ -632,7 +632,7 @@ replace_dot_alias = function(e) {
632632
substitute2(.j, env),
633633
list(.j = substitute(j))
634634
))
635-
if (missing(jsub)) {j = substitute(); jsub=NULL} else if (verbose && !is.null(env)) catf("Argument '%s' after substitute: %s\n", "j", paste(deparse(jsub, width.cutoff=500L), collapse="\n"))
635+
if (missing(jsub)) {j = substitute(); jsub=NULL} else if (verbose && !is.null(env)) catf("Argument '%s' after substitute: %s\n", "j", deparse1(jsub, collapse="\n"))
636636
}
637637
}
638638
if (!missing(j)) {
@@ -722,7 +722,7 @@ replace_dot_alias = function(e) {
722722
substitute2(.i, env),
723723
list(.i = substitute(i))
724724
))
725-
if (missing(isub)) {i = substitute(); isub=NULL} else if (verbose && !is.null(env)) catf("Argument '%s' after substitute: %s\n", "i", paste(deparse(isub, width.cutoff=500L), collapse="\n"))
725+
if (missing(isub)) {i = substitute(); isub=NULL} else if (verbose && !is.null(env)) catf("Argument '%s' after substitute: %s\n", "i", deparse1(isub, collapse="\n"))
726726
}
727727
}
728728
if (!missing(i)) {
@@ -2252,7 +2252,7 @@ replace_dot_alias = function(e) {
22522252

22532253
# What's the name of the top-level call in 'j'?
22542254
# NB: earlier, we used 'as.character()' but that fails for closures/builtins (#6026).
2255-
root_name = function(jsub) if (is.call(jsub)) paste(deparse(jsub[[1L]]), collapse = " ") else ""
2255+
root_name = function(jsub) if (is.call(jsub)) deparse1(jsub[[1L]], width.cutoff=60L) else ""
22562256

22572257
DT = function(x, ...) { #4872
22582258
old = getOption("datatable.optimize")

R/transpose.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ tstrsplit = function(x, ..., fill=NA, type.convert=FALSE, keep, names=FALSE) {
5151
if(!n) stopf("The argument 'type.convert' does not support empty list.")
5252
is_named = nzchar(names(type.convert))
5353
all_is_named = length(is_named) && all(is_named) # because all(is_named)=TRUE if is_named=NULL <-- names(type.convert)=NULL
54-
last_item = paste(deparse(substitute(type.convert)[[n + 1L]], width.cutoff=500L), collapse=" ")
54+
last_item = deparse1(substitute(type.convert)[[n + 1L]])
5555
if (!all_is_named) {
5656
if (!(sum(!is_named) == 1L && !is_named[n] && is.function(type.convert[[n]])))
5757
stopf("When the argument 'type.convert' contains an unnamed element, it is expected to be the last element and should be a function. More than one unnamed element is not allowed unless all elements are functions with length equal to %d (the length of the transpose list or 'keep' argument if it is specified).", length(keep))

0 commit comments

Comments
 (0)