Skip to content

Commit

Permalink
str_ilike() (#544)
Browse files Browse the repository at this point in the history
Add `str_ilike()` and deprecate `ignore_case` argument to `str_like()`. Fixes #543.

---------

Co-authored-by: Hadley Wickham <hadley@posit.co>
  • Loading branch information
edward-burn and hadley authored Aug 20, 2024
1 parent 9304301 commit 90f8ba7
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 18 deletions.
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export(str_flatten)
export(str_flatten_comma)
export(str_glue)
export(str_glue_data)
export(str_ilike)
export(str_interp)
export(str_length)
export(str_like)
Expand Down
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# stringr (development version)

* New `str_ilike()` that follows the conventions of the SQL ILIKE operator (@edward-burn, #543).
* `str_like(ignore_case)` is deprecated, with `str_like()` now always case sensitive to better follow the conventions of the SQL LIKE operator (@edward-burn, #543).
* `str_sub<-` now gives a more informative error if `value` is not the correct length.
* Add `sep` argument to `str_dup()` so that it is possible to repeat a string and
add a separator between every repeated value (@edward-burn, #564).
Expand Down
56 changes: 47 additions & 9 deletions R/detect.R
Original file line number Diff line number Diff line change
Expand Up @@ -108,39 +108,77 @@ str_ends <- function(string, pattern, negate = FALSE) {
)
}

#' Detect a pattern in the same way as `SQL`'s `LIKE` operator
#' Detect a pattern in the same way as `SQL`'s `LIKE` and `ILIKE` operators
#'
#' @description
#' `str_like()` follows the conventions of the SQL `LIKE` operator:
#' `str_like()` and `str_like()` follow the conventions of the SQL `LIKE`
#' and `ILIKE` operators, namely:
#'
#' * Must match the entire string.
#' * `_` matches a single character (like `.`).
#' * `%` matches any number of characters (like `.*`).
#' * `\%` and `\_` match literal `%` and `_`.
#' * The match is case insensitive by default.
#'
#' The difference between the two functions is their case-sensitivity:
#' `str_like()` is case sensitive and `str_ilike()` is not.
#'
#' @note
#' Prior to stringr 1.6.0, `str_like()` was incorrectly case-insensitive.
#'
#' @inheritParams str_detect
#' @param pattern A character vector containing a SQL "like" pattern.
#' See above for details.
#' @param ignore_case Ignore case of matches? Defaults to `TRUE` to match
#' the SQL `LIKE` operator.
#' @param ignore_case `r lifecycle::badge("deprecated")`
#' @return A logical vector the same length as `string`.
#' @export
#' @examples
#' fruit <- c("apple", "banana", "pear", "pineapple")
#' str_like(fruit, "app")
#' str_like(fruit, "app%")
#' str_like(fruit, "APP%")
#' str_like(fruit, "ba_ana")
#' str_like(fruit, "%APPLE")
str_like <- function(string, pattern, ignore_case = TRUE) {
#' str_like(fruit, "%apple")
#'
#' str_ilike(fruit, "app")
#' str_ilike(fruit, "app%")
#' str_ilike(fruit, "APP%")
#' str_ilike(fruit, "ba_ana")
#' str_ilike(fruit, "%apple")
str_like <- function(string, pattern, ignore_case = deprecated()) {
check_lengths(string, pattern)
check_character(pattern)
if (inherits(pattern, "stringr_pattern")) {
cli::cli_abort("{.arg pattern} must be a plain string, not a stringr modifier.")
}
if (lifecycle::is_present(ignore_case)) {
lifecycle::deprecate_warn(
when = "1.6.0",
what = "str_like(ignore_case)",
details = c(
"`str_like()` is always case sensitive.",
"Use `str_ilike()` for case insensitive string matching."
)
)
check_bool(ignore_case)
if (ignore_case) {
return(str_ilike(string, pattern))
}
}

pattern <- regex(like_to_regex(pattern), ignore_case = FALSE)
stri_detect_regex(string, pattern, opts_regex = opts(pattern))
}

#' @export
#' @rdname str_like
str_ilike <- function(string, pattern) {
check_lengths(string, pattern)
check_character(pattern)
if (inherits(pattern, "stringr_pattern")) {
cli::cli_abort(tr_("{.arg pattern} must be a plain string, not a stringr modifier."))
}
check_bool(ignore_case)

pattern <- regex(like_to_regex(pattern), ignore_case = ignore_case)
pattern <- regex(like_to_regex(pattern), ignore_case = TRUE)
stri_detect_regex(string, pattern, opts_regex = opts(pattern))
}

Expand Down
29 changes: 22 additions & 7 deletions man/str_like.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 19 additions & 1 deletion tests/testthat/_snaps/detect.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,29 @@
Error in `str_like()`:
! Can't recycle `string` (size 2) to match `pattern` (size 3).

# str_like works
# str_like is case sensitive

Code
str_like("abc", regex("x"))
Condition
Error in `str_like()`:
! `pattern` must be a plain string, not a stringr modifier.

# ignore_case is deprecated but still respected

Code
out <- str_like("abc", "AB%", ignore_case = TRUE)
Condition
Warning:
The `ignore_case` argument of `str_like()` is deprecated as of stringr 1.6.0.
i `str_like()` is always case sensitive.
i Use `str_ilike()` for case insensitive string matching.

# str_ilike works

Code
str_ilike("abc", regex("x"))
Condition
Error in `str_ilike()`:
! `pattern` must be a plain string, not a stringr modifier.

17 changes: 16 additions & 1 deletion tests/testthat/test-detect.R
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,26 @@ test_that("functions use tidyverse recycling rules", {
# str_like ----------------------------------------------------------------


test_that("str_like works", {
test_that("str_like is case sensitive", {
expect_true(str_like("abc", "ab%"))
expect_false(str_like("abc", "AB%"))
expect_snapshot(str_like("abc", regex("x")), error = TRUE)
})

test_that("ignore_case is deprecated but still respected", {
expect_snapshot(out <- str_like("abc", "AB%", ignore_case = TRUE))
expect_equal(out, TRUE)

expect_warning(out <- str_like("abc", "AB%", ignore_case = FALSE))
expect_equal(out, FALSE)
})

test_that("str_ilike works", {
expect_true(str_ilike("abc", "ab%"))
expect_true(str_ilike("abc", "AB%"))
expect_snapshot(str_ilike("abc", regex("x")), error = TRUE)
})

test_that("like_to_regex generates expected regexps",{
expect_equal(like_to_regex("ab%"), "^ab.*$")
expect_equal(like_to_regex("ab_"), "^ab.$")
Expand Down

0 comments on commit 90f8ba7

Please sign in to comment.