Skip to content

Commit bd984f8

Browse files
authored
Add server-side processing support for SearchBuilder (#1113)
1 parent 8bf8fa4 commit bd984f8

File tree

4 files changed

+148
-0
lines changed

4 files changed

+148
-0
lines changed

NEWS.md

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
- `updateSearch()` now sets the slider values based on the new search string for numeric columns (thanks, @mikmart, #1110).
66

7+
- Added server-side processing support for the [SearchBuilder](https://datatables.net/extensions/searchbuilder/) extension (thanks, @AhmedKhaled945, @shrektan, @mikmart, #963).
8+
79
# CHANGES IN DT VERSION 0.31
810

911
- Upgraded DataTables version to 1.13.6 (thanks, @stla, #1091).

R/searchbuilder.R

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# server-side processing for the SearchBuilder extension
2+
# https://datatables.net/extensions/searchbuilder/
3+
4+
sbEvaluateSearch = function(search, data) {
5+
# https://datatables.net/reference/option/searchBuilder.preDefined
6+
stopifnot(search$logic %in% c('AND', 'OR'))
7+
Reduce(
8+
switch(search$logic, AND = `&`, OR = `|`),
9+
lapply(search$criteria, sbEvaluateCriteria, data)
10+
)
11+
}
12+
13+
sbEvaluateCriteria = function(criteria, data) {
14+
# https://datatables.net/reference/option/searchBuilder.preDefined.criteria
15+
if ('logic' %in% names(criteria)) {
16+
# this is a sub-group
17+
sbEvaluateSearch(criteria, data)
18+
} else {
19+
# this is a criteria
20+
cond = criteria$condition
21+
type = criteria$type
22+
x = data[[criteria$origData %||% criteria$data]]
23+
v = sbParseValue(sbExtractValue(criteria), type)
24+
sbEvaluateCondition(cond, type, x, v)
25+
}
26+
}
27+
28+
sbExtractValue = function(criteria) {
29+
if (criteria$condition %in% c('between', '!between')) {
30+
# array values are passed in a funny way to R
31+
c(criteria$value1, criteria$value2)
32+
} else {
33+
criteria$value
34+
}
35+
}
36+
37+
sbParseValue = function(value, type) {
38+
# TODO: handle 'moment' and 'luxon' types mentioned in condition reference
39+
if (type %in% c('string', 'html')) {
40+
as.character(value)
41+
} else if (type %in% c('num', 'num-fmt', 'html-num', 'html-num-fmt')) {
42+
as.numeric(value)
43+
} else if (type %in% c('date')) {
44+
as.Date(value)
45+
} else {
46+
stop(sprintf('unsupported criteria type "%s"', type))
47+
}
48+
}
49+
50+
sbEvaluateCondition = function(condition, type, x, value) {
51+
# https://datatables.net/reference/option/searchBuilder.preDefined.criteria.condition
52+
if (type %in% c('string', 'html')) {
53+
switch(
54+
condition,
55+
'!=' = x != value,
56+
'!null' = !is.na(x) & x != '',
57+
'=' = x == value,
58+
'contains' = grepl(value, x, fixed = TRUE),
59+
'!contains' = !grepl(value, x, fixed = TRUE),
60+
'ends' = endsWith(as.character(x), value),
61+
'!ends' = !endsWith(as.character(x), value),
62+
'null' = is.na(x) | x == '',
63+
'starts' = startsWith(as.character(x), value),
64+
'!starts' = !startsWith(as.character(x), value),
65+
stop(sprintf('unsupported condition "%s" for criteria type "%s"', condition, type))
66+
)
67+
} else if (type %in% c('num', 'num-fmt', 'html-num', 'html-num-fmt')) {
68+
switch(
69+
condition,
70+
'!=' = x != value,
71+
'!null' = !is.na(x),
72+
'<' = x < value,
73+
'<=' = x <= value,
74+
'=' = x == value,
75+
'>' = x > value,
76+
'>=' = x >= value,
77+
'between' = x >= value[1] & x <= value[2],
78+
'!between' = x < value[1] | x > value[2],
79+
'null' = is.na(x),
80+
stop(sprintf('unsupported condition "%s" for criteria type "%s"', condition, type))
81+
)
82+
} else if (type %in% c('date', 'moment', 'luxon')) {
83+
switch(
84+
condition,
85+
'!=' = x != value,
86+
'!null' = !is.na(x),
87+
'<' = x < value,
88+
'=' = x == value,
89+
'>' = x > value,
90+
'between' = x >= value[1] & x <= value[2],
91+
'!between' = x < value[1] | x > value[2],
92+
'null' = is.na(x),
93+
stop(sprintf('unsupported condition "%s" for criteria type "%s"', condition, type))
94+
)
95+
} else {
96+
stop(sprintf('unsupported criteria type "%s"', type))
97+
}
98+
}

R/shiny.R

+5
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,11 @@ dataTablesFilter = function(data, params) {
649649
# start searching with all rows
650650
i = seq_len(n)
651651

652+
# apply SearchBuilder query if present
653+
if (!is.null(s <- q$searchBuilder)) {
654+
i = which(sbEvaluateSearch(s, data))
655+
}
656+
652657
# search by columns
653658
if (length(i)) for (j in names(q$columns)) {
654659
col = q$columns[[j]]

tests/testit/test-searchbuilder.R

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
library(testit)
2+
3+
assert('SearchBuilder condition evaluation works', {
4+
(sbEvaluateCondition('>', 'num', 1:2, 1) == c(FALSE, TRUE))
5+
(sbEvaluateCondition('between', 'num', 7, c(2, 4)) == FALSE)
6+
(sbEvaluateCondition('starts', 'string', 'foo', 'f') == TRUE)
7+
(sbEvaluateCondition('starts', 'string', factor('foo'), 'f') == TRUE)
8+
(sbEvaluateCondition('null', 'string', c('', NA)) == c(TRUE, TRUE))
9+
})
10+
11+
assert('SearchBuilder logic evaluation works', {
12+
res = sbEvaluateSearch(
13+
list(
14+
logic = 'AND',
15+
criteria = list(
16+
list(condition = '<=', data = 'a', value = '4', type = 'num'),
17+
list(condition = '>=', data = 'a', value = '2', type = 'num')
18+
)
19+
),
20+
data.frame(a = 1:9)
21+
)
22+
(setequal(which(res), 2:4))
23+
})
24+
25+
assert('SearchBuilder complex queries work', {
26+
res = sbEvaluateSearch(
27+
list(
28+
logic = 'OR',
29+
criteria = list(
30+
list(condition = '=', data = 'a', value = '7', type = 'num'),
31+
list(
32+
logic = 'AND',
33+
criteria = list(
34+
list(condition = '<=', data = 'a', value = '4', type = 'num'),
35+
list(condition = '>=', data = 'a', value = '2', type = 'num')
36+
)
37+
)
38+
)
39+
),
40+
data.frame(a = 1:9)
41+
)
42+
(setequal(which(res), c(2:4, 7)))
43+
})

0 commit comments

Comments
 (0)