Skip to content

Commit 14e5b1a

Browse files
c cbind rbind more consistent to base R (#253)
* extend c, cbind, and rbind * consistent use of Rboolean (#251) * R consistent integer division and modulo operation (#250) * r consistent integer division * tweak NEWS --------- Co-authored-by: Michael Chirico <chiricom@google.com> * fix test ancient * add origin for as.{Date,POSIXct,POSIXlt} for ancient * `stringsAsFactors=FALSE` for ancient * fix ancient * skip a test for ancient * fix cbind tests for ancient * skip cbind tests for ancient * skip rbind tests for ancient * fix target_class_and_sample_value for missing .bit64.suppressPromoteInteger64ToCharacterMessage * merge target_class_and_sample_value in target_class * touch-ups to c() * touch-ups to rbind,cbind * missing ')' * inherits() instead of class() Co-authored-by: Michael Chirico <chiricom@google.com> * remove eval(str2lang()) * another round of touch-up * vestigial ')' * missing '{' * simplify (?) replacement helper for cbind * more expect_same_error * switch branch order * fix updated logical condition for unnested branches * revert unrelated switch to expect_same_error * replace rbind helper too * fix helper argument name Co-authored-by: Michael Chirico <michaelchirico4@gmail.com> * refactoring, add comments, add news * extract replace_dimnames * tweak NEWS * fix deparse.level problems * skip ancient test * more canonical patrick (avoid expand.grid), don't test 4 * raw cases * missing symmetric cases for difftime, factor, ordered * explicitly ignore coercion warning before error --------- Co-authored-by: Michael Chirico <chiricom@google.com> Co-authored-by: Michael Chirico <michaelchirico4@gmail.com>
1 parent e3b11ff commit 14e5b1a

5 files changed

Lines changed: 584 additions & 56 deletions

File tree

NEWS.md

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

99
All of these have observed downstream calls directly. If the call is in a CRAN/Bioconductor package, I'll be reaching out to help migrate onto the generic. This will probably take at least a year, so don't expect this to induce a CRAN release _per se_ until 2027 (although you should certainly aim to slip this in to your next release in the meantime).
1010

11+
1. When integer64 and character are combined, the result will be character. This prevents loss of information, e.g. in `c(as.integer64(1L), "a")` and `c(as.integer64(1L), "999999999999999999999999")`. To try this in advance for `c.integer64`, `cbind.integer64`, `rbind.integer64`, `[.integer64<-`, `[[.integer64<-`, `union`, `intersect`, `setdiff`, `setdiff` and `is.element` one can set the option `bit64.promoteInteger64ToCharacter=TRUE`.
12+
1113
## BREAKING CHANGES
1214

1315
1. {bit64} no longer `Depends` on {bit}; instead it `Imports` it. Please file an issue if this
@@ -58,6 +60,7 @@
5860
1. A replacement in an integer64 vector or array using `[<-` or `[[<-` with a complex or POSIXct leads to an R consistent coercion of the integer64 object to a complex or POSIXct object and not just an error. Thanks @hcirellu.
5961
1. `union`, `setdiff`, `intersect`, `setequal` and `is.element` get an overload to work correctly with `integer64` (#182).
6062
1. The methods of the 'Ops' group (e.g. `+`, `&`, `==`) now support dispatch for both arguments so that e.g. `difftime * integer64` works consistent to R (#179). Thanks @hcirellu. Note that this relies on `chooseOpsMethod()` and thus R 4.3.0.
63+
1. `c.integer64`, `cbind.integer64` and `rbind.integer64` now support combining with lists and recursion as `base::c`, `base::cbind` and `base::rbind` do (#252). In addition, by setting the option `bit64.promoteInteger64ToCharacter=TRUE` the methods return character if integer64 and character are combined. Thanks @hcirellu.
6164

6265
## BUG FIXES
6366

R/integer64.R

Lines changed: 185 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -224,11 +224,12 @@ NULL
224224
#' @param recursive logical. If `recursive = TRUE`, the function
225225
#' recursively descends through lists (and pairlists) combining all
226226
#' their elements into a vector.
227+
#' @param deparse.level integer controlling the construction of labels in the case of non-matrix-like arguments
227228
#'
228229
#' @returns
229-
#' [c()] returns a integer64 vector of the total length of the input
230+
#' [c()] returns a vector of the appropriate mode. This could be a integer64 vector or a list of objects
230231
#'
231-
#' [cbind()] and [rbind()] return a integer64 matrix
232+
#' [cbind()] and [rbind()] return a matrix, data.frame or list with dimensions
232233
#'
233234
#' @note
234235
#' R currently only dispatches generic 'c' to method 'c.integer64' if the
@@ -1031,58 +1032,205 @@ position_args_with_int64_to_int_coercion = function(sys_call, eval_frame, skipLa
10311032

10321033
#' @rdname c.integer64
10331034
#' @export
1034-
c.integer64 = function(..., recursive = FALSE) {
1035-
l = list(...)
1036-
for (k in seq_along(l)) {
1037-
if (recursive && is.list(l[[k]])) {
1038-
l[[k]] <- do.call(c.integer64, c(l[[k]], list(recursive = TRUE)))
1039-
} else {
1040-
if (!is.integer64(l[[k]])) {
1041-
nam <- names(l[[k]])
1042-
l[[k]] <- as.integer64(l[[k]])
1043-
names(l[[k]]) <- nam
1035+
c.integer64 = function(..., recursive=FALSE) {
1036+
# This check can be dropped in the future when c.integer64 is not exported anymore
1037+
if (...length() == 0L) return(NULL)
1038+
dots = list(...)
1039+
1040+
if (!isTRUE(recursive) && any(vapply(dots, is.list, FALSE))) {
1041+
return(unlist(lapply(dots, function(el) if (inherits(el, "POSIXlt")) el else as.list(el)), recursive=FALSE))
1042+
}
1043+
1044+
value_class = target_class(dots, recursive=recursive, POSIXltAsCharacter=TRUE)
1045+
# find positions of elements to be converted
1046+
if(value_class == "integer64") {
1047+
# integer64 doesn't have to be converted, but `oldClass(val) = NULL` has to be applied
1048+
checkFunc = Negate(is.null)
1049+
} else {
1050+
checkFunc = is.integer64
1051+
}
1052+
findPositionsOfItemsToConvert = function(x) {
1053+
res = list()
1054+
for (ii in seq_along(x)) {
1055+
if (inherits(x[[ii]], c("list", "data.frame"))) {
1056+
res = c(res, lapply(findPositionsOfItemsToConvert(x[[ii]]), function(el) c(ii, el)))
1057+
} else if (checkFunc(x[[ii]])) {
1058+
res = c(res, list(ii))
10441059
}
1045-
oldClass(l[[k]]) <- NULL
10461060
}
1061+
res
10471062
}
1048-
ret = do.call(c, l)
1049-
oldClass(ret) = "integer64"
1063+
for (idx in findPositionsOfItemsToConvert(dots)) {
1064+
val = dots[[idx]]
1065+
if (inherits(val, "POSIXlt")) {
1066+
val = lapply(unclass(val), as, value_class)
1067+
} else {
1068+
val = as(val, value_class)
1069+
if (value_class == "integer64")
1070+
oldClass(val) = NULL
1071+
}
1072+
dots[[idx]] = val
1073+
}
1074+
1075+
ret = do.call(c, c(dots, list(recursive=recursive)))
1076+
if (value_class == "integer64")
1077+
oldClass(ret) = value_class
10501078
ret
10511079
}
10521080

1081+
# helper function to generate names for cbind/rbind if missing; it is not intended to be exported
1082+
make_names_for_cbind = function(sys_call, dots, deparse.level=1) {
1083+
nd = names(dots)
1084+
if (!deparse.level %in% c(1L, 2L)) return(nd)
1085+
if (is.null(nd))
1086+
nd = character(length(dots))
1087+
1088+
sys_call_dots = sys_call[-1L][seq_along(dots)]
1089+
sel = !logical(length(sys_call_dots))
1090+
if (deparse.level == 1L)
1091+
sel = vapply(sys_call_dots, is.symbol, FALSE)
1092+
sel = sel & nd == "" & !vapply(dots, function(el) length(el) == 1L && is.na(el), FALSE)
1093+
nd[sel] = as.character(sys_call_dots[sel])
1094+
nd
1095+
}
1096+
10531097
#' @rdname c.integer64
10541098
#' @export
1055-
cbind.integer64 = function(...) {
1056-
l = list(...)
1057-
K <- length(l)
1058-
for (k in 1:K) {
1059-
if (!is.integer64(l[[k]])) {
1060-
nam <- names(l[[k]])
1061-
l[[k]] <- as.integer64(l[[k]])
1062-
names(l[[k]]) <- nam
1099+
cbind.integer64 = function(..., deparse.level=1) {
1100+
dots = list(...)
1101+
value_class = target_class(dots, recursive=FALSE)
1102+
1103+
# find positions of elements to be converted
1104+
if(value_class == "integer64") {
1105+
# integer64 doesn't have to be converted, but `oldClass(val) = NULL` has to be applied
1106+
checkFunc = Negate(is.null)
1107+
} else {
1108+
checkFunc = is.integer64
1109+
}
1110+
positionsOfItemsToConvert = which(vapply(dots, function(el) !is.list(el) && checkFunc(el), FALSE, USE.NAMES=FALSE))
1111+
1112+
# set names if missing
1113+
names(dots) = make_names_for_cbind(sys.call(sys.nframe() - 1L), dots, deparse.level)
1114+
# convert relevant items
1115+
for (idx in positionsOfItemsToConvert) {
1116+
val = dots[[idx]]
1117+
val = structure(as(val, value_class), dim=dim(val), dimnames=dimnames(val), names=names(val))
1118+
if (value_class == "integer64")
1119+
oldClass(val) = NULL
1120+
dots[[idx]] = val
1121+
}
1122+
ret = withCallingHandlers_and_choose_call(
1123+
do.call(cbind, c(dots, list(deparse.level=deparse.level))),
1124+
c("cbind", "cbind"),
1125+
callStack = sys.calls()
1126+
)
1127+
1128+
# restore integer64 class
1129+
if (value_class == "integer64") {
1130+
if (is.list(ret)) {
1131+
estimatedColumnIndices = lapply(dots, function(el) {
1132+
res = ncol(el)
1133+
if (is.null(res))
1134+
res = as.integer(length(el) > 0L)
1135+
res
1136+
})
1137+
lastValue = 0L
1138+
for (ii in seq_along(estimatedColumnIndices)) {
1139+
estimatedColumnIndices[[ii]] = lastValue + seq_len(estimatedColumnIndices[[ii]])
1140+
lastValue = lastValue + length(estimatedColumnIndices[[ii]])
1141+
}
1142+
nrow_ret = nrow(ret)
1143+
for (idx in positionsOfItemsToConvert) {
1144+
for (ii in estimatedColumnIndices[[idx]]) {
1145+
if (is.data.frame(ret)) {
1146+
oldClass(ret[[ii]]) = value_class
1147+
} else {
1148+
for (jj in seq_len(nrow_ret))
1149+
oldClass(ret[[(ii - 1L)*nrow_ret + jj]]) = value_class
1150+
}
10631151
}
1064-
oldClass(l[[k]]) <- NULL
1152+
}
1153+
} else {
1154+
oldClass(ret) = value_class
1155+
}
10651156
}
1066-
ret = do.call(cbind, l)
1067-
oldClass(ret) = "integer64"
10681157
ret
10691158
}
10701159

10711160
#' @rdname c.integer64
10721161
#' @export
1073-
rbind.integer64 = function(...) {
1074-
l = list(...)
1075-
K <- length(l)
1076-
for (k in 1:K) {
1077-
if (!is.integer64(l[[k]])) {
1078-
nam <- names(l[[k]])
1079-
l[[k]] <- as.integer64(l[[k]])
1080-
names(l[[k]]) <- nam
1162+
rbind.integer64 = function(..., deparse.level=1) {
1163+
dots = list(...)
1164+
value_class = target_class(dots, recursive=TRUE)
1165+
1166+
# find positions of elements to be converted
1167+
if(value_class == "integer64") {
1168+
# integer64 doesn't have to be converted, but `oldClass(val) = NULL` has to be applied
1169+
checkFunc = Negate(is.null)
1170+
} else {
1171+
checkFunc = is.integer64
1172+
}
1173+
findPositionsOfItemsToConvert = function(x) {
1174+
res = list()
1175+
for (ii in seq_along(x)) {
1176+
if (is.data.frame(x[[ii]])) {
1177+
res = c(res, lapply(findPositionsOfItemsToConvert(x[[ii]]), function(el) c(ii, el)))
1178+
} else if (checkFunc(x[[ii]]) && !is.list(x[[ii]])) {
1179+
res = c(res, list(ii))
1180+
}
1181+
}
1182+
res
1183+
}
1184+
positionsOfItemsToConvert = findPositionsOfItemsToConvert(dots)
1185+
1186+
# set names if missing
1187+
names(dots) = make_names_for_cbind(sys.call(sys.nframe() - 1L), dots, deparse.level)
1188+
# convert relevant items
1189+
for (idx in positionsOfItemsToConvert) {
1190+
val = dots[[idx]]
1191+
val = structure(as(val, value_class), dim=dim(val), dimnames=dimnames(val), names=names(val))
1192+
if (value_class == "integer64")
1193+
oldClass(val) = NULL
1194+
dots[[idx]] = val
1195+
}
1196+
ret = withCallingHandlers_and_choose_call(
1197+
do.call(rbind, c(dots, list(deparse.level=deparse.level))),
1198+
c("rbind", "rbind"),
1199+
callStack = sys.calls()
1200+
)
1201+
1202+
# restore integer64 class
1203+
if (value_class == "integer64") {
1204+
if (is.list(ret)) {
1205+
if (is.data.frame(ret)) {
1206+
for (ii in seq_along(ret))
1207+
oldClass(ret[[ii]]) = value_class
1208+
} else {
1209+
# for rbind, we have to estimate row indices for each item to convert because of possible recycling
1210+
# this is mainly for POSIXlt
1211+
estimatedRowIndices = lapply(dots, function(el) {
1212+
res = nrow(el)
1213+
if (is.null(res))
1214+
res = as.integer(length(el) > 0L)
1215+
res
1216+
})
1217+
lastValue = 0L
1218+
for (ii in seq_along(estimatedRowIndices)) {
1219+
estimatedRowIndices[[ii]] = lastValue + seq_len(estimatedRowIndices[[ii]])
1220+
lastValue = lastValue + length(estimatedRowIndices[[ii]])
10811221
}
1082-
oldClass(l[[k]]) <- NULL
1222+
col_offset = (seq_len(ncol(ret)) - 1L)*nrow(ret)
1223+
for (idx in positionsOfItemsToConvert) {
1224+
for (ii in estimatedRowIndices[[idx]]) {
1225+
for (jj in col_offset)
1226+
oldClass(ret[[ii + jj]]) = value_class
1227+
}
1228+
}
1229+
}
1230+
} else {
1231+
oldClass(ret) = value_class
1232+
}
10831233
}
1084-
ret = do.call(rbind, l)
1085-
oldClass(ret) = "integer64"
10861234
ret
10871235
}
10881236

R/zzz.R

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ if (getRversion() < "3.6.0") {
4848
# * call stack: [A, B, C, D, E]; function_names = c("C", "D") returns C
4949
# * call stack: [A, B, C, D, E]; function_names = c("E", "D") returns D
5050
# * call stack: [A, B, C, D, E]; function_names = c("E", "X") returns A
51-
choose_sys_call = function(function_names, name_to_display=NULL) {
52-
calls = sys.calls()
51+
choose_sys_call = function(function_names, name_to_display=NULL, callStack=NULL) {
52+
calls = if (is.null(callStack)) sys.calls() else callStack
5353
if (length(calls) == 1L || length(function_names) == 0L) return(calls[[1L]])
5454
# find last occurrence of last name in function_names
5555
function_names_rev = rev(as.character(function_names))
@@ -70,14 +70,14 @@ choose_sys_call = function(function_names, name_to_display=NULL) {
7070
ret
7171
}
7272

73-
withCallingHandlers_and_choose_call = function(expr, function_names, name_to_display=NULL) {
73+
withCallingHandlers_and_choose_call = function(expr, function_names, name_to_display=NULL, callStack=NULL) {
7474
wch = substitute(
7575
withCallingHandlers(expr, error=error, warning=warning),
7676
list(
7777
expr = sys.call()[[2L]],
78-
error = function(e) stop(errorCondition(e$message, call=choose_sys_call(function_names, name_to_display))),
78+
error = function(e) stop(errorCondition(e$message, call=choose_sys_call(function_names, name_to_display, callStack=callStack))),
7979
warning = function(w) {
80-
warning(warningCondition(w$message, call=choose_sys_call(function_names, name_to_display)))
80+
warning(warningCondition(w$message, call=choose_sys_call(function_names, name_to_display, callStack=callStack)))
8181
invokeRestart("muffleWarning")
8282
}
8383
)
@@ -86,10 +86,20 @@ withCallingHandlers_and_choose_call = function(expr, function_names, name_to_dis
8686
}
8787

8888
# function to determine target class and sample value for union, intersect, setdiff, setequal, min, max, range, sum, prod, c, cbind and rbind functions
89-
target_class = function(x) {
89+
getClassesOfElements = function(x, recursive) {
90+
classes = vapply(x, function(el) if (inherits(el, c("list", "data.frame"))) "list" else class(el)[1L], character(1L))
91+
if (recursive) {
92+
union(classes[classes != "list"], unlist(lapply(x[classes == "list"], function(el) getClassesOfElements(el, recursive=TRUE))))
93+
} else {
94+
unique(classes)
95+
}
96+
}
97+
98+
target_class = function(x, recursive=FALSE, POSIXltAsCharacter=FALSE) {
9099

91-
classes = unique(vapply(x, function(el) if (inherits(el, c("list", "data.frame"))) "list" else class(el)[1L], character(1L)))
100+
classes = getClassesOfElements(x, recursive=isTRUE(recursive))
92101

102+
if ("POSIXlt" %in% classes && isTRUE(POSIXltAsCharacter)) return("character")
93103
if ("complex" %in% classes) return("complex")
94104
if (any(c("character", "factor", "ordered") %in% classes)) {
95105
# TODO(#44): next Release: change default behavior; subsequent Release: change from message to warning; subsequent Release: change from warning to error; subsequent Release: remove option

man/c.integer64.Rd

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

0 commit comments

Comments
 (0)