diff --git a/NAMESPACE b/NAMESPACE index ca4c74d9d..5c23255c3 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -9,7 +9,7 @@ export(data.table, tables, setkey, setkeyv, key, haskey, CJ, SJ, copy) export(rowwiseDT) export(setindex, setindexv, indices) export(as.data.table,is.data.table,test.data.table) -export(last,first,like,"%like%","%ilike%","%flike%","%plike%",between,"%between%",inrange,"%inrange%", "%notin%") +export(last,first,like,"%like%","%ilike%","%flike%","%plike%",between,"%between%",inrange,"%inrange%", "%notin%", "%fin%") export(timetaken) export(truelength, setalloccol, setallocrow, alloc.col, ":=", let) export(setattr, setnames, setcolorder, set, setDT, setDF) diff --git a/NEWS.md b/NEWS.md index 5d2c35aa9..31ef6b3b5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -90,6 +90,8 @@ 20. `print.data.table()` now correctly displays data when `col.names="none"` and `row.names=FALSE`, [#7735](https://github.com/Rdatatable/data.table/issues/7735). Thanks to @jan-swissre for the report and @venom1204 for the fix. +21. New `%fin%` operator for fast row-wise existence checks, e.g., `x %fin% y` or `x[, found := .(col1, col2) %fin% y]`. It supports multi-column matching using `data.table` join semantics, offering a concise and high-performance alternative to standard R `%in%` for membership testing, [#2279](https://github.com/Rdatatable/data.table/issues/2279). Thanks to @franknarf1 for the request and @venom1204 for teh implementation. + ### Notes 1. {data.table} now depends on R 3.5.0 (2018). diff --git a/R/setops.R b/R/setops.R index 4ae78b193..65ad25285 100644 --- a/R/setops.R +++ b/R/setops.R @@ -296,3 +296,29 @@ all.equal.data.table = function(target, current, trim.levels=TRUE, check.attribu } TRUE } + +`%fin%` = function(x, y) { + mc = match.call() + if (is.list(x) && !is.data.table(x)) { + if (is.call(mc$x) && (identical(mc$x[[1L]], quote(`list`)) || identical(mc$x[[1L]], quote(`.`)))) { + nms = names(mc$x) + arg_names = as.character(mc$x[-1L]) + if (is.null(nms)) { + names(x) = arg_names + } else { + names(x) = ifelse(!nzchar(nms[-1L]), arg_names, nms[-1L]) + } + } + x = as.data.table(x) + } else if (!is.data.table(x)) { + x = as.data.table(list(x)) + setnames(x, names(y)[1]) + } + join_cols = names(x) + if (!all(join_cols %in% names(y))) { + stopf("All columns in the left-hand side (%s) must exist in the right-hand table (%s).", + paste(join_cols, collapse=","), paste(names(y), collapse=",")) + } + ans = y[x, .N, on=join_cols, by=.EACHI] + ans$N > 0L +} diff --git a/inst/tests/tests.Rraw b/inst/tests/tests.Rraw index 7cb84b7ec..b3c352a71 100644 --- a/inst/tests/tests.Rraw +++ b/inst/tests/tests.Rraw @@ -21985,3 +21985,14 @@ DT3 = data.table(a=c(1,NA), b=c("x",NA), c=c(3,NA)) test(2387.04, setnafill(copy(DT3), type="locf", cols=sapply(DT3, is.numeric)), data.table(a=c(1,1), b=c("x",NA), c=c(3,3))) test(2387.05, setnafill(copy(DT3), type="locf", cols=c(TRUE,NA,FALSE)), error="'cols' contains NA at position 2") test(2387.06, setnafill(copy(DT3), type="locf", cols=c(TRUE,FALSE)), error="'cols' is a logical vector of length 2 but there are 3 columns") + +# %fin% set operator, #2279 +x = data.table(name=c("A","B","C"), year=c(2020,2020,2021), grade=c(8,9,10)); y = data.table(name=c("A","B","D"), year=c(2020,2019,2021), grade=c(8,9,10)) +test(2388.01, x %fin% y, c(TRUE,FALSE,FALSE)) +test(2388.02, x[, .(name,year,grade) %fin% y], c(TRUE,FALSE,FALSE)) +test(2388.03, data.table(name=c("A","A","B"),year=c(2020,2020,2021),grade=c(8,8,9)) %fin% y, c(TRUE,TRUE,FALSE)) +test(2388.04, data.table(name=c("A","B"),year=c(2020,2021),grade=c(8,9)) %fin% y, c(TRUE,FALSE)) +test(2388.05, data.table(name=c("A","B"),year=c(2020,2021),grade=c(8,9)) %fin% data.table(name=c("A","B"),year=c(2020,2021)), error="All columns in the left-hand side") +test(2388.06, data.table(name=c("A","B"),year=c(2020,2021),grade=c(8,9)) %fin% data.table(name=c("A","B"),year=c(2020,2021),grade=c(8,9),extra=c(1,2)), c(TRUE,TRUE)) +test(2388.07, list(name=c("A","B"), year=c(2020,2022)) %fin% y, c(TRUE,FALSE)) +test(2388.08, c("A","B") %fin% y[, .(name)], c(TRUE,TRUE)) diff --git a/man/setops.Rd b/man/setops.Rd index dfa2572c7..78b6f1e87 100644 --- a/man/setops.Rd +++ b/man/setops.Rd @@ -10,6 +10,7 @@ \alias{funion} \alias{setequal} \alias{fsetequal} +\alias{\%fin\%} \title{ Set operations for data tables } \description{ Similar to base R set functions, \code{union}, \code{intersect}, \code{setdiff} and \code{setequal} but for \code{data.table}s. Additional \code{all} argument controls how duplicated rows are handled. Functions \code{fintersect}, \code{setdiff} (\code{MINUS} or \code{EXCEPT} in SQL) and \code{funion} are meant to provide functionality of corresponding SQL operators. Unlike SQL, data.table functions will retain row order. @@ -19,6 +20,7 @@ fintersect(x, y, all = FALSE) fsetdiff(x, y, all = FALSE) funion(x, y, all = FALSE) fsetequal(x, y, all = TRUE) +x \%fin\% y } \arguments{ \item{x, y}{\code{data.table}s.} @@ -33,6 +35,7 @@ fsetequal(x, y, all = TRUE) } \details{ \code{bit64::integer64} columns are supported but not \code{complex} and \code{list}, except for \code{funion}. + \code{\%fin\%} provides a fast way to check if rows of x exist in y. } \value{ A data.table in case of \code{fintersect}, \code{funion} and \code{fsetdiff}. Logical \code{TRUE} or \code{FALSE} for \code{fsetequal}. @@ -54,5 +57,9 @@ funion(x, y) # union funion(x, y, all=TRUE) # union all fsetequal(x, x2, all=FALSE) # setequal fsetequal(x, x2) # setequal all +# %fin% operator +x = data.table(name=c("A","B","C"), year=c(2020,2020,2021)) +y = data.table(name=c("A","B"), year=c(2020,2020)) +x[, found := .(name, year) \%fin\% y] } \keyword{ data }