diff --git a/NAMESPACE b/NAMESPACE index 3f1bffc3..da483518 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -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) diff --git a/NEWS.md b/NEWS.md index d5b30b2b..eb922049 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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). diff --git a/R/detect.R b/R/detect.R index e5a89159..449cc936 100644 --- a/R/detect.R +++ b/R/detect.R @@ -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)) } diff --git a/man/str_like.Rd b/man/str_like.Rd index b3732ef9..db54bf82 100644 --- a/man/str_like.Rd +++ b/man/str_like.Rd @@ -2,9 +2,12 @@ % Please edit documentation in R/detect.R \name{str_like} \alias{str_like} -\title{Detect a pattern in the same way as \code{SQL}'s \code{LIKE} operator} +\alias{str_ilike} +\title{Detect a pattern in the same way as \code{SQL}'s \code{LIKE} and \code{ILIKE} operators} \usage{ -str_like(string, pattern, ignore_case = TRUE) +str_like(string, pattern, ignore_case = deprecated()) + +str_ilike(string, pattern) } \arguments{ \item{string}{Input vector. Either a character vector, or something @@ -13,26 +16,38 @@ coercible to one.} \item{pattern}{A character vector containing a SQL "like" pattern. See above for details.} -\item{ignore_case}{Ignore case of matches? Defaults to \code{TRUE} to match -the SQL \code{LIKE} operator.} +\item{ignore_case}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}}} } \value{ A logical vector the same length as \code{string}. } \description{ -\code{str_like()} follows the conventions of the SQL \code{LIKE} operator: +\code{str_like()} and \code{str_like()} follow the conventions of the SQL \code{LIKE} +and \code{ILIKE} operators, namely: \itemize{ \item Must match the entire string. \item \verb{_} matches a single character (like \code{.}). \item \verb{\%} matches any number of characters (like \verb{.*}). \item \verb{\\\%} and \verb{\\_} match literal \verb{\%} and \verb{_}. -\item The match is case insensitive by default. } + +The difference between the two functions is their case-sensitivity: +\code{str_like()} is case sensitive and \code{str_ilike()} is not. +} +\note{ +Prior to stringr 1.6.0, \code{str_like()} was incorrectly case-insensitive. } \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(fruit, "\%apple") + +str_ilike(fruit, "app") +str_ilike(fruit, "app\%") +str_ilike(fruit, "APP\%") +str_ilike(fruit, "ba_ana") +str_ilike(fruit, "\%apple") } diff --git a/tests/testthat/_snaps/detect.md b/tests/testthat/_snaps/detect.md index fcd57834..571828c3 100644 --- a/tests/testthat/_snaps/detect.md +++ b/tests/testthat/_snaps/detect.md @@ -39,7 +39,7 @@ 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")) @@ -47,3 +47,21 @@ 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. + diff --git a/tests/testthat/test-detect.R b/tests/testthat/test-detect.R index ab3458db..65ba41e8 100644 --- a/tests/testthat/test-detect.R +++ b/tests/testthat/test-detect.R @@ -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.$")