Skip to content

Commit 7f3fba9

Browse files
authored
added ghead/gtail support for n>1 (#5089)
1 parent b33dee6 commit 7f3fba9

File tree

4 files changed

+128
-97
lines changed

4 files changed

+128
-97
lines changed

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@
133133
# 2: 3
134134
```
135135

136+
24. `DT[, head(.SD,n), by=grp]` and `tail` are now optimized when `n>1`, [#5060](https://github.com/Rdatatable/data.table/issues/5060) [#523](https://github.com/Rdatatable/data.table/issues/523#issuecomment-162934391). `n==1` was already optimized. Thanks to Jan Gorecki and Michael Young for requesting, and Benjamin Schwendinger for the PR.
137+
136138
## BUG FIXES
137139

138140
1. `by=.EACHI` when `i` is keyed but `on=` different columns than `i`'s key could create an invalidly keyed result, [#4603](https://github.com/Rdatatable/data.table/issues/4603) [#4911](https://github.com/Rdatatable/data.table/issues/4911). Thanks to @myoung3 and @adamaltmejd for reporting, and @ColeMiller1 for the PR. An invalid key is where a `data.table` is marked as sorted by the key columns but the data is not sorted by those columns, leading to incorrect results from subsequent queries.

R/data.table.R

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -809,8 +809,8 @@ replace_dot_alias = function(e) {
809809
# when the 'by' expression includes get/mget/eval, all.vars cannot be trusted to infer all used columns, #4981
810810
allbyvars = NULL
811811
else
812-
allbyvars = intersect(all.vars(bysub), names_x)
813-
812+
allbyvars = intersect(all.vars(bysub), names_x)
813+
814814
orderedirows = .Call(CisOrderedSubset, irows, nrow(x)) # TRUE when irows is NULL (i.e. no i clause). Similar but better than is.sorted(f__)
815815
bysameorder = byindex = FALSE
816816
if (!bysub %iscall% ":" && ##Fix #4285
@@ -1740,13 +1740,13 @@ replace_dot_alias = function(e) {
17401740
# is.symbol() is for #1369, #1974 and #2949
17411741
if (!(is.call(q) && is.symbol(q[[1L]]) && is.symbol(q[[2L]]) && (q1 <- q[[1L]]) %chin% gfuns)) return(FALSE)
17421742
if (!(q2 <- q[[2L]]) %chin% names(SDenv$.SDall) && q2 != ".I") return(FALSE) # 875
1743-
if ((length(q)==2L || (!is.null(names(q)) && startsWith(names(q)[3L], "na"))) && (!q1 %chin% c("head","tail"))) return(TRUE)
1743+
if ((length(q)==2L || (!is.null(names(q)) && startsWith(names(q)[3L], "na")))) return(TRUE)
17441744
# ^^ base::startWith errors on NULL unfortunately
17451745
# head-tail uses default value n=6 which as of now should not go gforce ... ^^
17461746
# otherwise there must be three arguments, and only in two cases:
17471747
# 1) head/tail(x, 1) or 2) x[n], n>0
17481748
length(q)==3L && length(q3 <- q[[3L]])==1L && is.numeric(q3) &&
1749-
( (q1 %chin% c("head", "tail") && q3==1L) || ((q1 == "[" || (q1 == "[[" && eval(call('is.atomic', q[[2L]]), envir=x))) && q3>0L) )
1749+
( (q1 %chin% c("head", "tail")) || ((q1 == "[" || (q1 == "[[" && eval(call('is.atomic', q[[2L]]), envir=x))) && q3>0L) )
17501750
}
17511751
if (jsub[[1L]]=="list") {
17521752
GForce = TRUE
@@ -1762,6 +1762,8 @@ replace_dot_alias = function(e) {
17621762
if (length(jsub[[ii]])==3L) jsub[[ii]][[3L]] = eval(jsub[[ii]][[3L]], parent.frame()) # tests 1187.2 & 1187.4
17631763
}
17641764
else {
1765+
# adding argument to ghead/gtail if none is supplied to g-optimized head/tail
1766+
if (length(jsub) == 2L && jsub[[1L]] %chin% c("head", "tail")) jsub[["n"]] = 6L
17651767
jsub[[1L]] = as.name(paste0("g", jsub[[1L]]))
17661768
if (length(jsub)==3L) jsub[[3L]] = eval(jsub[[3L]], parent.frame()) # tests 1187.3 & 1187.5
17671769
}
@@ -1841,6 +1843,25 @@ replace_dot_alias = function(e) {
18411843
ans = gforce(thisEnv, jsub, o__, f__, len__, irows) # irows needed for #971.
18421844
gi = if (length(o__)) o__[f__] else f__
18431845
g = lapply(grpcols, function(i) groups[[i]][gi])
1846+
1847+
# adding ghead/gtail(n) support for n > 1 #5060 #523
1848+
q3 = 0
1849+
if (!is.symbol(jsub)) {
1850+
headTail_arg = function(q) {
1851+
if (length(q)==3L && length(q3 <- q[[3L]])==1L && is.numeric(q3) &&
1852+
(q1 <- q[[1L]]) %chin% c("ghead", "gtail") && q3!=1) q3
1853+
else 0
1854+
}
1855+
if (jsub[[1L]] == "list"){
1856+
q3 = max(sapply(jsub, headTail_arg))
1857+
} else if (length(jsub)==3L) {
1858+
q3 = headTail_arg(jsub)
1859+
}
1860+
}
1861+
if (q3 > 0) {
1862+
grplens = pmin.int(q3, len__)
1863+
g = lapply(g, rep.int, times=grplens)
1864+
}
18441865
ans = c(g, ans)
18451866
} else {
18461867
ans = .Call(Cdogroups, x, xcols, groups, grpcols, jiscols, xjiscols, grporder, o__, f__, len__, jsub, SDenv, cols, newnames, !missing(on), verbose)

inst/tests/tests.Rraw

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8116,21 +8116,36 @@ test(1579.18, dt[, tail(.SD,1L), by=x], dt[, utils::tail(.SD,1L), by=x])
81168116
test(1579.19, dt[, tail(.SD,1L), by=x], dt[, utils::tail(.SD,1L), by=x])
81178117
test(1579.20, dt[, tail(.SD,1L), keyby=x], dt[, utils::tail(.SD,1L), keyby=x])
81188118
test(1579.21, dt[, tail(.SD,1L), keyby=x], dt[, utils::tail(.SD,1L), keyby=x])
8119-
# GForce _doesn't_ work when n > 1
8120-
test(1579.22, dt[ , tail(.SD, 2), by = x, verbose = TRUE], output = 'GForce FALSE')
8119+
# 1579.22 tested gtail with n>1; now 1579.4+ below
81218120

81228121
mysub <- function(x, n) x[n]
8123-
test(1579.23, dt[, .SD[2], by=x], dt[, mysub(.SD,2), by=x])
8124-
test(1579.24, dt[, .SD[2], by=x], dt[, mysub(.SD,2), by=x])
8125-
test(1579.25, dt[, .SD[2], keyby=x], dt[, mysub(.SD,2), keyby=x])
8126-
test(1579.26, dt[, .SD[2], keyby=x], dt[, mysub(.SD,2), keyby=x])
8127-
test(1579.27, dt[, .SD[2L], by=x], dt[, mysub(.SD,2L), by=x])
8128-
test(1579.28, dt[, .SD[2L], by=x], dt[, mysub(.SD,2L), by=x])
8129-
test(1579.29, dt[, .SD[2L], keyby=x], dt[, mysub(.SD,2L), keyby=x])
8130-
test(1579.30, dt[, .SD[2L], keyby=x], dt[, mysub(.SD,2L), keyby=x])
8131-
8132-
ans = capture.output(dt[, .SD[2], by=x, verbose=TRUE])
8133-
test(1579.31, any(grepl("GForce optimized", ans)), TRUE)
8122+
test(1579.23, dt[, .SD[2], by=x, verbose=TRUE], dt[, mysub(.SD,2), by=x], output="GForce optimized.*g[[]")
8123+
test(1579.24, dt[, .SD[2], keyby=x], dt[, mysub(.SD,2), keyby=x])
8124+
test(1579.25, dt[, .SD[2L], by=x], dt[, mysub(.SD,2L), by=x])
8125+
test(1579.26, dt[, .SD[2L], keyby=x], dt[, mysub(.SD,2L), keyby=x])
8126+
test(1579.27, dt[, .SD[15], by=x], dt[, mysub(.SD,15), by=x]) # tests 15 > grpsize and that NA is correct including for integer64
8127+
test(1579.28, dt[, .SD[15], keyby=x], dt[, mysub(.SD,15), keyby=x])
8128+
8129+
# gforce head/tail for n>1, #5060
8130+
set.seed(99)
8131+
DT = data.table(x = sample(letters[1:5], 20, TRUE),
8132+
y = rep.int(1:2, 10), # to test 2 grouping columns get rep'd properly
8133+
i = sample(c(-2L,0L,3L,NA), 20, TRUE),
8134+
d = sample(c(1.2,-3.4,5.6,NA), 20, TRUE),
8135+
s = sample(c("foo","bar",NA), 20, TRUE),
8136+
l = sample(list(1:3, mean, letters[4:5], NULL), 20, replace=TRUE))
8137+
if (test_bit64) DT[, i64:=sample(as.integer64(c(-2200000000,+2400000000,NA)), 20, TRUE)]
8138+
options(datatable.optimize=2L)
8139+
test(1579.401, DT[, .N, by=x]$N, INT(4,6,5,2,3)) # the smallest group is 2, so n=5 tests n constrained to grpsize
8140+
test(1579.402, DT[, head(.SD,2), by=x, verbose=TRUE], DT[, utils::head(.SD,2), by=x], output="optimized.*ghead")
8141+
test(1579.403, DT[, head(.SD,2), keyby=x, verbose=TRUE], DT[, utils::head(.SD,2), keyby=x], output="optimized.*ghead")
8142+
test(1579.404, DT[, head(.SD,5), by=x, verbose=TRUE], DT[, utils::head(.SD,5), by=x], output="optimized.*ghead")
8143+
test(1579.405, DT[, head(.SD,5), keyby=x, verbose=TRUE], DT[, utils::head(.SD,5), keyby=x], output="optimized.*ghead")
8144+
test(1579.406, DT[, tail(.SD,2), by=x, verbose=TRUE], DT[, utils::tail(.SD,2), by=x], output="optimized.*gtail")
8145+
test(1579.407, DT[, tail(.SD,2), keyby=x, verbose=TRUE], DT[, utils::tail(.SD,2), keyby=x], output="optimized.*gtail")
8146+
test(1579.408, DT[, tail(.SD,5), by=x, verbose=TRUE], DT[, utils::tail(.SD,5), by=x], output="optimized.*gtail")
8147+
test(1579.409, DT[, tail(.SD,5), keyby=x, verbose=TRUE], DT[, utils::tail(.SD,5), keyby=x], output="optimized.*gtail")
8148+
test(1579.410, DT[, tail(.SD,2), by=.(x,y), verbose=TRUE], DT[, utils::tail(.SD,2), by=.(x,y)], output="optimized.*gtail")
81348149

81358150
options(datatable.optimize = Inf)
81368151

@@ -14695,11 +14710,11 @@ DT = data.table(a=c(rep(1L, 7L), rep(2L, 5L)), b=1:12, d=12:1)
1469514710
test(2018.1, DT[, head(.SD), a, verbose=TRUE],
1469614711
data.table(a=c(rep(1L, 6L), rep(2L, 5L)), b=c(1:6, 8:12), d=c(12:7, 5:1)),
1469714712
output=c("lapply optimization changed j from 'head(.SD)' to 'list(head(b, n = 6L), head(d, n = 6L))'",
14698-
"GForce is on, left j unchanged"))
14713+
"GForce optimized j to 'list(ghead(b, n = 6L), ghead(d, n = 6L))'"))
1469914714
test(2018.2, DT[, head(b), a, verbose=TRUE],
1470014715
data.table(a=c(rep(1L, 6L), rep(2L, 5L)), V1=c(1:6, 8:12)),
1470114716
output=c("lapply optimization is on, j unchanged as 'head(b)'",
14702-
"GForce is on, left j unchanged"))
14717+
"GForce optimized j to 'ghead(b, n = 6L)'"))
1470314718
test(2018.3, DT[, tail(.SD), a], data.table(a=c(rep(1L, 6L), rep(2L, 5L)), b=c(2:7, 8:12), d=c(11:6, 5:1)))
1470414719
test(2018.4, DT[, tail(b), a], data.table(a=c(rep(1L, 6L), rep(2L, 5L)), V1=c(2:7, 8:12)))
1470514720
# gforce tests coverage

src/gsumm.c

Lines changed: 71 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -900,81 +900,72 @@ SEXP gmedian(SEXP x, SEXP narmArg) {
900900
return ans;
901901
}
902902

903-
static SEXP gfirstlast(SEXP x, const bool first, const int w) {
903+
static SEXP gfirstlast(SEXP x, const bool first, const int w, const bool headw) {
904904
// w: which item (1 other than for gnthvalue when could be >1)
905+
// headw: select 1:w of each group when first=true, and (n-w+1):n when first=false (i.e. tail)
905906
const bool nosubset = irowslen == -1;
907+
const bool issorted = !isunsorted; // make a const-bool for use inside loops
906908
const int n = nosubset ? length(x) : irowslen;
907-
SEXP ans;
908909
if (nrow != n) error(_("nrow [%d] != length(x) [%d] in %s"), nrow, n, first?"gfirst":"glast");
909-
const bool gnth = w>1; // const bool to avoid fetching grpsize[i] when not needed
910-
switch(TYPEOF(x)) {
911-
case LGLSXP: {
912-
const int *ix = LOGICAL(x);
913-
ans = PROTECT(allocVector(LGLSXP, ngrp));
914-
int *ians = LOGICAL(ans);
915-
for (int i=0; i<ngrp; ++i) {
916-
if (gnth && w>grpsize[i]) { ians[i]=NA_LOGICAL; continue; }
917-
int k = first ? ff[i]+w-2 : ff[i]+grpsize[i]-w-1;
918-
if (isunsorted) k = oo[k]-1;
919-
ians[i] = nosubset ? ix[k] : (irows[k]==NA_INTEGER ? NA_LOGICAL : ix[irows[k]-1]);
920-
}
921-
}
922-
break;
923-
case INTSXP: {
924-
const int *ix = INTEGER(x);
925-
ans = PROTECT(allocVector(INTSXP, ngrp));
926-
int *ians = INTEGER(ans);
910+
if (w==1 && headw) error(_("Internal error: gfirstlast headw should only be true when w>1"));
911+
int anslen = ngrp;
912+
if (headw) {
913+
anslen = 0;
927914
for (int i=0; i<ngrp; ++i) {
928-
if (gnth && w>grpsize[i]) { ians[i]=NA_INTEGER; continue; }
929-
int k = first ? ff[i]+w-2 : ff[i]+grpsize[i]-w-1;
930-
if (isunsorted) k = oo[k]-1;
931-
ians[i] = nosubset ? ix[k] : (irows[k]==NA_INTEGER ? NA_INTEGER : ix[irows[k]-1]);
915+
anslen += MIN(w, grpsize[i]);
932916
}
933917
}
934-
break;
935-
case REALSXP: {
936-
const double *dx = REAL(x);
937-
ans = PROTECT(allocVector(REALSXP, ngrp));
938-
double *dans = REAL(ans);
939-
for (int i=0; i<ngrp; ++i) {
940-
if (gnth && w>grpsize[i]) { dans[i]=NA_REAL; continue; }
941-
int k = first ? ff[i]+w-2 : ff[i]+grpsize[i]-w-1;
942-
if (isunsorted) k = oo[k]-1;
943-
dans[i] = nosubset ? dx[k] : (irows[k]==NA_INTEGER ? NA_REAL : dx[irows[k]-1]);
944-
}
918+
SEXP ans = PROTECT(allocVector(TYPEOF(x), anslen));
919+
int ansi = 0;
920+
#define DO(CTYPE, RTYPE, RNA, ASSIGN) { \
921+
const CTYPE *xd = (const CTYPE *)RTYPE(x); \
922+
if (headw) { \
923+
/* returning more than 1 per group; w>1 */ \
924+
for (int i=0; i<ngrp; ++i) { \
925+
const int grpn = grpsize[i]; \
926+
const int thisn = MIN(w, grpn); \
927+
const int jstart = ff[i]-1+ (!first)*(grpn-thisn); \
928+
const int jend = jstart+thisn; \
929+
for (int j=jstart; j<jend; ++j) { \
930+
const int k = issorted ? j : oo[j]-1; \
931+
/* ternary on const-bool assumed to be branch-predicted and ok inside loops */ \
932+
const CTYPE val = nosubset ? xd[k] : (irows[k]==NA_INTEGER ? RNA : xd[irows[k]-1]); \
933+
ASSIGN; \
934+
} \
935+
} \
936+
} else if (w==1) { \
937+
for (int i=0; i<ngrp; ++i) { \
938+
const int j = ff[i]-1 + (first ? 0 : grpsize[i]-1); \
939+
const int k = issorted ? j : oo[j]-1; \
940+
const CTYPE val = nosubset ? xd[k] : (irows[k]==NA_INTEGER ? RNA : xd[irows[k]-1]); \
941+
ASSIGN; \
942+
} \
943+
} else if (w>1 && first) { \
944+
/* gnthvalue */ \
945+
for (int i=0; i<ngrp; ++i) { \
946+
const int grpn = grpsize[i]; \
947+
if (w>grpn) { const CTYPE val=RNA; ASSIGN; continue; } \
948+
const int j = ff[i]-1+w-1; \
949+
const int k = issorted ? j : oo[j]-1; \
950+
const CTYPE val = nosubset ? xd[k] : (irows[k]==NA_INTEGER ? RNA : xd[irows[k]-1]); \
951+
ASSIGN; \
952+
} \
953+
} else { \
954+
/* w>1 && !first not supported because -i in R means everything-but-i and gnthvalue */ \
955+
/* currently takes n>0 only. However, we could still support n'th from the end, somehow */ \
956+
error(_("Internal error: unanticipated case in gfirstlast first=%d w=%d headw=%d"), \
957+
first, w, headw); \
958+
} \
945959
}
946-
break;
947-
case CPLXSXP: {
948-
const Rcomplex *dx = COMPLEX(x);
949-
ans = PROTECT(allocVector(CPLXSXP, ngrp));
950-
Rcomplex *dans = COMPLEX(ans);
951-
for (int i=0; i<ngrp; ++i) {
952-
if (gnth && w>grpsize[i]) { dans[i]=NA_CPLX; continue; }
953-
int k = first ? ff[i]+w-2 : ff[i]+grpsize[i]-w-1;
954-
if (isunsorted) k = oo[k]-1;
955-
dans[i] = nosubset ? dx[k] : (irows[k]==NA_INTEGER ? NA_CPLX : dx[irows[k]-1]);
956-
}
957-
} break;
958-
case STRSXP: {
959-
const SEXP *sx = STRING_PTR(x);
960-
ans = PROTECT(allocVector(STRSXP, ngrp));
961-
for (int i=0; i<ngrp; ++i) {
962-
if (gnth && w>grpsize[i]) { SET_STRING_ELT(ans, i, NA_STRING); continue; }
963-
int k = first ? ff[i]+w-2 : ff[i]+grpsize[i]-w-1;
964-
if (isunsorted) k = oo[k]-1;
965-
SET_STRING_ELT(ans, i, nosubset ? sx[k] : (irows[k]==NA_INTEGER ? NA_STRING : sx[irows[k]-1]));
966-
}
967-
} break;
968-
case VECSXP: {
969-
const SEXP *vx = SEXPPTR_RO(x);
970-
ans = PROTECT(allocVector(VECSXP, ngrp));
971-
for (int i=0; i<ngrp; ++i) {
972-
if (gnth && w>grpsize[i]) { SET_VECTOR_ELT(ans, i, ScalarLogical(NA_LOGICAL)); continue; }
973-
int k = first ? ff[i]+w-2 : ff[i]+grpsize[i]-w-1;
974-
if (isunsorted) k = oo[k]-1;
975-
SET_VECTOR_ELT(ans, i, nosubset ? vx[k] : (irows[k]==NA_INTEGER ? ScalarLogical(NA_LOGICAL) : vx[irows[k]-1]));
976-
}
977-
} break;
960+
switch(TYPEOF(x)) {
961+
case LGLSXP: { int *ansd=LOGICAL(ans); DO(int, LOGICAL, NA_LOGICAL, ansd[ansi++]=val) } break;
962+
case INTSXP: { int *ansd=INTEGER(ans); DO(int, INTEGER, NA_INTEGER, ansd[ansi++]=val) } break;
963+
case REALSXP: if (INHERITS(x, char_integer64)) {
964+
int64_t *ansd=(int64_t *)REAL(ans); DO(int64_t, REAL, NA_INTEGER64, ansd[ansi++]=val) }
965+
else { double *ansd=REAL(ans); DO(double, REAL, NA_REAL, ansd[ansi++]=val) } break;
966+
case CPLXSXP: { Rcomplex *ansd=COMPLEX(ans); DO(Rcomplex, COMPLEX, NA_CPLX, ansd[ansi++]=val) } break;
967+
case STRSXP: DO(SEXP, STRING_PTR, NA_STRING, SET_STRING_ELT(ans,ansi++,val)) break;
968+
case VECSXP: DO(SEXP, SEXPPTR_RO, ScalarLogical(NA_LOGICAL), SET_VECTOR_ELT(ans,ansi++,val)) break;
978969
default:
979970
error(_("Type '%s' not supported by GForce head/tail/first/last/`[`. Either add the prefix utils::head(.) or turn off GForce optimization using options(datatable.optimize=1)"), type2char(TYPEOF(x)));
980971
}
@@ -984,26 +975,28 @@ static SEXP gfirstlast(SEXP x, const bool first, const int w) {
984975
}
985976

986977
SEXP glast(SEXP x) {
987-
return gfirstlast(x, false, 1);
978+
return gfirstlast(x, false, 1, false);
988979
}
989980

990981
SEXP gfirst(SEXP x) {
991-
return gfirstlast(x, true, 1);
982+
return gfirstlast(x, true, 1, false);
992983
}
993984

994-
SEXP gtail(SEXP x, SEXP valArg) {
995-
if (!isInteger(valArg) || LENGTH(valArg)!=1 || INTEGER(valArg)[0]!=1) error(_("Internal error, gtail is only implemented for n=1. This should have been caught before. please report to data.table issue tracker.")); // # nocov
996-
return gfirstlast(x, false, 1);
985+
SEXP gtail(SEXP x, SEXP nArg) {
986+
if (!isInteger(nArg) || LENGTH(nArg)!=1 || INTEGER(nArg)[0]<1) error(_("Internal error, gtail is only implemented for n>0. This should have been caught before. please report to data.table issue tracker.")); // # nocov
987+
const int n=INTEGER(nArg)[0];
988+
return n==1 ? glast(x) : gfirstlast(x, false, n, true);
997989
}
998990

999-
SEXP ghead(SEXP x, SEXP valArg) {
1000-
if (!isInteger(valArg) || LENGTH(valArg)!=1 || INTEGER(valArg)[0]!=1) error(_("Internal error, ghead is only implemented for n=1. This should have been caught before. please report to data.table issue tracker.")); // # nocov
1001-
return gfirstlast(x, true, 1);
991+
SEXP ghead(SEXP x, SEXP nArg) {
992+
if (!isInteger(nArg) || LENGTH(nArg)!=1 || INTEGER(nArg)[0]<1) error(_("Internal error, gtail is only implemented for n>0. This should have been caught before. please report to data.table issue tracker.")); // # nocov
993+
const int n=INTEGER(nArg)[0];
994+
return n==1 ? gfirst(x) : gfirstlast(x, true, n, true);
1002995
}
1003996

1004-
SEXP gnthvalue(SEXP x, SEXP valArg) {
1005-
if (!isInteger(valArg) || LENGTH(valArg)!=1 || INTEGER(valArg)[0]<=0) error(_("Internal error, `g[` (gnthvalue) is only implemented single value subsets with positive index, e.g., .SD[2]. This should have been caught before. please report to data.table issue tracker.")); // # nocov
1006-
return gfirstlast(x, true, INTEGER(valArg)[0]);
997+
SEXP gnthvalue(SEXP x, SEXP nArg) {
998+
if (!isInteger(nArg) || LENGTH(nArg)!=1 || INTEGER(nArg)[0]<1) error(_("Internal error, `g[` (gnthvalue) is only implemented single value subsets with positive index, e.g., .SD[2]. This should have been caught before. please report to data.table issue tracker.")); // # nocov
999+
return gfirstlast(x, true, INTEGER(nArg)[0], false);
10071000
}
10081001

10091002
// TODO: gwhich.min, gwhich.max

0 commit comments

Comments
 (0)