From 249fd3093f6341c3ac4bf1745ed7867fd27579c2 Mon Sep 17 00:00:00 2001 From: PietrH Date: Tue, 30 Jun 2026 16:05:22 +0200 Subject: [PATCH 01/21] Refactor to offer a suggestion if there is a close match, don't print as many options, use cli styling. --- R/utils.R | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/R/utils.R b/R/utils.R index f42e572f..58ef87a5 100644 --- a/R/utils.R +++ b/R/utils.R @@ -20,18 +20,39 @@ check_value <- function(x, y, name = "value", lowercase = FALSE) { x <- tolower(x) y <- tolower(y) } - # Check value(s) against valid values - assertthat::assert_that( - all(x %in% y), # Returns TRUE for x = NULL - msg = glue::glue( - "Can't find {name} `{x}` in: {y}", - x = glue::glue_collapse(x, sep = "`, `", last = "` and/or `"), - y = glue::glue_collapse(y, sep = ", ", width = 300) + if (x %in% y) { + return(x) + } + # If the value is not found, check if it's a typo by calculating the + # Levenshtein distance. + relative_distances <- adist(x, y) / nchar(y) + # If any candidate string is less than 50% different from the reference, we + # can assume it's a typo and suggest it to the user. + candidates_col <- cli::cli_vec(y, list("vec-trunc" = 5)) + if (any(relative_distances <= 0.5)) { + closest_match <- y[which.min(relative_distances)] + # If there are many candidates, truncate the list to 5 items for the error + # message. + cli::cli_abort( + "Can't find {.var {name}}: {.val {x}} in: {.or {.str {candidates_col}}}. + Did you mean {.strong {.val {closest_match}}}?", + class = "etn_value_not_found_suggest", + call = rlang::caller_env() ) - ) + } else { + # Sort the references so the closest matches are mentioned. Only show 5 + # members. + candidates_col <- y[order(as.vector(relative_distances))] |> + cli::cli_vec(list("vec-trunc" = 5)) + cli::cli_abort( + "Can't find {.var {name}}: {.val {x}} in: {.or {.str {candidates_col}}}.", + class = "etn_value_not_found", + call = rlang::caller_env() + ) + } - return(x) + NULL } #' Get credentials from environment variables, or set them manually From 7ec0ef077f783c7b2e3a320e0f4e9146013dc64f Mon Sep 17 00:00:00 2001 From: PietrH Date: Tue, 30 Jun 2026 16:12:55 +0200 Subject: [PATCH 02/21] Add multi value support, only notify on actually missing values --- R/utils.R | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/R/utils.R b/R/utils.R index 58ef87a5..6c6a5635 100644 --- a/R/utils.R +++ b/R/utils.R @@ -21,12 +21,13 @@ check_value <- function(x, y, name = "value", lowercase = FALSE) { y <- tolower(y) } # Check value(s) against valid values - if (x %in% y) { + if (all(x %in% y)) { return(x) } + missing_value <- x[!x %in% y] # If the value is not found, check if it's a typo by calculating the # Levenshtein distance. - relative_distances <- adist(x, y) / nchar(y) + relative_distances <- adist(missing_value, y) / nchar(y) # If any candidate string is less than 50% different from the reference, we # can assume it's a typo and suggest it to the user. candidates_col <- cli::cli_vec(y, list("vec-trunc" = 5)) @@ -35,7 +36,7 @@ check_value <- function(x, y, name = "value", lowercase = FALSE) { # If there are many candidates, truncate the list to 5 items for the error # message. cli::cli_abort( - "Can't find {.var {name}}: {.val {x}} in: {.or {.str {candidates_col}}}. + "Can't find {.var {name}}: {.val {missing_value}} in: {.or {.str {candidates_col}}}. Did you mean {.strong {.val {closest_match}}}?", class = "etn_value_not_found_suggest", call = rlang::caller_env() @@ -46,7 +47,7 @@ check_value <- function(x, y, name = "value", lowercase = FALSE) { candidates_col <- y[order(as.vector(relative_distances))] |> cli::cli_vec(list("vec-trunc" = 5)) cli::cli_abort( - "Can't find {.var {name}}: {.val {x}} in: {.or {.str {candidates_col}}}.", + "Can't find {.var {name}}: {.val {missing_value}} in: {.or {.str {candidates_col}}}.", class = "etn_value_not_found", call = rlang::caller_env() ) From 99eb1a35f79c989b181aa5227ec03ca3a15789ca Mon Sep 17 00:00:00 2001 From: PietrH Date: Tue, 30 Jun 2026 16:13:04 +0200 Subject: [PATCH 03/21] Adapt tests to not check on message. --- tests/testthat/test-check_value.R | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/tests/testthat/test-check_value.R b/tests/testthat/test-check_value.R index 066b997f..d5b3582c 100644 --- a/tests/testthat/test-check_value.R +++ b/tests/testthat/test-check_value.R @@ -1,18 +1,15 @@ test_that("check_value() returns error for incorrect values", { expect_error( check_value("invalid", c("a", "b")), - "Can't find value `invalid` in: a, b", - fixed = TRUE + class = "etn_value_not_found" ) expect_error( check_value(c("a", "invalid"), c("a", "b")), - "Can't find value `a` and/or `invalid` in: a, b", - fixed = TRUE + class = "etn_value_not_found" ) expect_error( check_value("invalid", c("a", "b"), name = "param_name"), - "Can't find param_name `invalid` in: a, b", - fixed = TRUE + class = "etn_value_not_found" ) }) @@ -30,8 +27,7 @@ test_that("check_value() returns x for correct values", { test_that("check_value() can ignore case", { expect_error( check_value("A", c("a", "B")), - "Can't find value `A` in: a, B", - fixed = TRUE + class = "etn_value_not_found" ) expect_identical( check_value("A", c("a", "B"), lowercase = TRUE), @@ -51,7 +47,6 @@ test_that("check_value() can handle NA in reference", { expect_error( check_value("invalid", c("A", NA, "C")), - "Can't find value `invalid` in: A, C", - fixed = TRUE + class = "etn_value_not_found" ) }) From 237665a73883b656b908687a3f38cd91875255f9 Mon Sep 17 00:00:00 2001 From: PietrH Date: Tue, 30 Jun 2026 16:19:33 +0200 Subject: [PATCH 04/21] This line is never run, sylistic fluff --- R/utils.R | 2 -- 1 file changed, 2 deletions(-) diff --git a/R/utils.R b/R/utils.R index 6c6a5635..0161b8e9 100644 --- a/R/utils.R +++ b/R/utils.R @@ -52,8 +52,6 @@ check_value <- function(x, y, name = "value", lowercase = FALSE) { call = rlang::caller_env() ) } - - NULL } #' Get credentials from environment variables, or set them manually From 408cb1c829082307a74bd96dae226cc46c432f52 Mon Sep 17 00:00:00 2001 From: PietrH Date: Tue, 30 Jun 2026 16:19:41 +0200 Subject: [PATCH 05/21] Test for the suggestion --- tests/testthat/test-check_value.R | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/testthat/test-check_value.R b/tests/testthat/test-check_value.R index d5b3582c..2e015f37 100644 --- a/tests/testthat/test-check_value.R +++ b/tests/testthat/test-check_value.R @@ -50,3 +50,10 @@ test_that("check_value() can handle NA in reference", { class = "etn_value_not_found" ) }) + +test_that("check_value() can offer a suggestion for typos", { + expect_error( + check_value("seeschelde", c("demer", "dijle", "zeeschelde")), + class = "etn_value_not_found_suggest" + ) +}) From 1bc3a8f44935ff8883ab4a709173efa25ead02d1 Mon Sep 17 00:00:00 2001 From: PietrH Date: Tue, 30 Jun 2026 16:53:15 +0200 Subject: [PATCH 06/21] Add function family : otherwise pkgdown will complain! Co-Authored-By: Peter Desmet --- R/get_bibliography.R | 1 + 1 file changed, 1 insertion(+) diff --git a/R/get_bibliography.R b/R/get_bibliography.R index 8d64b7da..d0ecaab4 100644 --- a/R/get_bibliography.R +++ b/R/get_bibliography.R @@ -12,6 +12,7 @@ #' first row contains the citation for the ETN platform, the second row the #' citation for the etn R package, and subsequent rows contain citations for #' the animal and acoustic projects present in the input data.frame. +#' @family access functions #' @export #' #' @examplesIf interactive() && etn:::credentials_are_set() From 48a1cd78e05e12ed999e8e04db13e8150d364ba4 Mon Sep 17 00:00:00 2001 From: PietrH Date: Tue, 30 Jun 2026 16:53:22 +0200 Subject: [PATCH 07/21] devtools::document() --- man/get_acoustic_deployments.Rd | 1 + man/get_acoustic_detections.Rd | 1 + man/get_acoustic_projects.Rd | 1 + man/get_acoustic_receivers.Rd | 1 + man/get_animal_projects.Rd | 1 + man/get_animals.Rd | 1 + man/get_bibliography.Rd | 13 +++++++++++++ man/get_cpod_projects.Rd | 1 + man/get_package.Rd | 1 + man/get_tags.Rd | 1 + 10 files changed, 22 insertions(+) diff --git a/man/get_acoustic_deployments.Rd b/man/get_acoustic_deployments.Rd index 4087b38e..1841dc2a 100644 --- a/man/get_acoustic_deployments.Rd +++ b/man/get_acoustic_deployments.Rd @@ -67,6 +67,7 @@ Other access functions: \code{\link[=get_acoustic_receivers]{get_acoustic_receivers()}}, \code{\link[=get_animal_projects]{get_animal_projects()}}, \code{\link[=get_animals]{get_animals()}}, +\code{\link[=get_bibliography]{get_bibliography()}}, \code{\link[=get_cpod_projects]{get_cpod_projects()}}, \code{\link[=get_package]{get_package()}}, \code{\link[=get_tags]{get_tags()}} diff --git a/man/get_acoustic_detections.Rd b/man/get_acoustic_detections.Rd index 389cd724..ef58621d 100644 --- a/man/get_acoustic_detections.Rd +++ b/man/get_acoustic_detections.Rd @@ -112,6 +112,7 @@ Other access functions: \code{\link[=get_acoustic_receivers]{get_acoustic_receivers()}}, \code{\link[=get_animal_projects]{get_animal_projects()}}, \code{\link[=get_animals]{get_animals()}}, +\code{\link[=get_bibliography]{get_bibliography()}}, \code{\link[=get_cpod_projects]{get_cpod_projects()}}, \code{\link[=get_package]{get_package()}}, \code{\link[=get_tags]{get_tags()}} diff --git a/man/get_acoustic_projects.Rd b/man/get_acoustic_projects.Rd index 3c93e222..cbd560df 100644 --- a/man/get_acoustic_projects.Rd +++ b/man/get_acoustic_projects.Rd @@ -52,6 +52,7 @@ Other access functions: \code{\link[=get_acoustic_receivers]{get_acoustic_receivers()}}, \code{\link[=get_animal_projects]{get_animal_projects()}}, \code{\link[=get_animals]{get_animals()}}, +\code{\link[=get_bibliography]{get_bibliography()}}, \code{\link[=get_cpod_projects]{get_cpod_projects()}}, \code{\link[=get_package]{get_package()}}, \code{\link[=get_tags]{get_tags()}} diff --git a/man/get_acoustic_receivers.Rd b/man/get_acoustic_receivers.Rd index a77c10f6..a552e508 100644 --- a/man/get_acoustic_receivers.Rd +++ b/man/get_acoustic_receivers.Rd @@ -42,6 +42,7 @@ Other access functions: \code{\link[=get_acoustic_projects]{get_acoustic_projects()}}, \code{\link[=get_animal_projects]{get_animal_projects()}}, \code{\link[=get_animals]{get_animals()}}, +\code{\link[=get_bibliography]{get_bibliography()}}, \code{\link[=get_cpod_projects]{get_cpod_projects()}}, \code{\link[=get_package]{get_package()}}, \code{\link[=get_tags]{get_tags()}} diff --git a/man/get_animal_projects.Rd b/man/get_animal_projects.Rd index 93da18ba..3e45c8fa 100644 --- a/man/get_animal_projects.Rd +++ b/man/get_animal_projects.Rd @@ -48,6 +48,7 @@ Other access functions: \code{\link[=get_acoustic_projects]{get_acoustic_projects()}}, \code{\link[=get_acoustic_receivers]{get_acoustic_receivers()}}, \code{\link[=get_animals]{get_animals()}}, +\code{\link[=get_bibliography]{get_bibliography()}}, \code{\link[=get_cpod_projects]{get_cpod_projects()}}, \code{\link[=get_package]{get_package()}}, \code{\link[=get_tags]{get_tags()}} diff --git a/man/get_animals.Rd b/man/get_animals.Rd index ff10b09a..9863852c 100644 --- a/man/get_animals.Rd +++ b/man/get_animals.Rd @@ -66,6 +66,7 @@ Other access functions: \code{\link[=get_acoustic_projects]{get_acoustic_projects()}}, \code{\link[=get_acoustic_receivers]{get_acoustic_receivers()}}, \code{\link[=get_animal_projects]{get_animal_projects()}}, +\code{\link[=get_bibliography]{get_bibliography()}}, \code{\link[=get_cpod_projects]{get_cpod_projects()}}, \code{\link[=get_package]{get_package()}}, \code{\link[=get_tags]{get_tags()}} diff --git a/man/get_bibliography.Rd b/man/get_bibliography.Rd index 9dda095e..403ba7cc 100644 --- a/man/get_bibliography.Rd +++ b/man/get_bibliography.Rd @@ -39,3 +39,16 @@ data.frame( get_bibliography() \dontshow{\}) # examplesIf} } +\seealso{ +Other access functions: +\code{\link[=get_acoustic_deployments]{get_acoustic_deployments()}}, +\code{\link[=get_acoustic_detections]{get_acoustic_detections()}}, +\code{\link[=get_acoustic_projects]{get_acoustic_projects()}}, +\code{\link[=get_acoustic_receivers]{get_acoustic_receivers()}}, +\code{\link[=get_animal_projects]{get_animal_projects()}}, +\code{\link[=get_animals]{get_animals()}}, +\code{\link[=get_cpod_projects]{get_cpod_projects()}}, +\code{\link[=get_package]{get_package()}}, +\code{\link[=get_tags]{get_tags()}} +} +\concept{access functions} diff --git a/man/get_cpod_projects.Rd b/man/get_cpod_projects.Rd index 04382f0d..8106768f 100644 --- a/man/get_cpod_projects.Rd +++ b/man/get_cpod_projects.Rd @@ -49,6 +49,7 @@ Other access functions: \code{\link[=get_acoustic_receivers]{get_acoustic_receivers()}}, \code{\link[=get_animal_projects]{get_animal_projects()}}, \code{\link[=get_animals]{get_animals()}}, +\code{\link[=get_bibliography]{get_bibliography()}}, \code{\link[=get_package]{get_package()}}, \code{\link[=get_tags]{get_tags()}} } diff --git a/man/get_package.Rd b/man/get_package.Rd index cf9fb063..327f334f 100644 --- a/man/get_package.Rd +++ b/man/get_package.Rd @@ -69,6 +69,7 @@ Other access functions: \code{\link[=get_acoustic_receivers]{get_acoustic_receivers()}}, \code{\link[=get_animal_projects]{get_animal_projects()}}, \code{\link[=get_animals]{get_animals()}}, +\code{\link[=get_bibliography]{get_bibliography()}}, \code{\link[=get_cpod_projects]{get_cpod_projects()}}, \code{\link[=get_tags]{get_tags()}} } diff --git a/man/get_tags.Rd b/man/get_tags.Rd index 3d86dccd..cde03e8a 100644 --- a/man/get_tags.Rd +++ b/man/get_tags.Rd @@ -63,6 +63,7 @@ Other access functions: \code{\link[=get_acoustic_receivers]{get_acoustic_receivers()}}, \code{\link[=get_animal_projects]{get_animal_projects()}}, \code{\link[=get_animals]{get_animals()}}, +\code{\link[=get_bibliography]{get_bibliography()}}, \code{\link[=get_cpod_projects]{get_cpod_projects()}}, \code{\link[=get_package]{get_package()}} } From 55e9e0ddbc4798b1901c8434d6cf5c9491baa787 Mon Sep 17 00:00:00 2001 From: PietrH Date: Thu, 2 Jul 2026 10:18:03 +0200 Subject: [PATCH 08/21] Hardcode the citation to avoid OS differences --- R/utils.R | 25 ++++++++++--------- .../_snaps/get_package/bibliography.csv | 2 +- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/R/utils.R b/R/utils.R index 0161b8e9..8a256d51 100644 --- a/R/utils.R +++ b/R/utils.R @@ -274,18 +274,19 @@ credentials_are_set <- function(){ #' @family helper functions #' @noRd etn_citation <- function(){ - utils::citation("etn") |> - format(style = "text") |> - # Remove linebreaks that may have been introduced based on console width - stringr::str_squish() |> - # Remove markup: emphasis - stringr::str_remove_all(stringr::fixed("_")) |> - # Remove markup: links - stringr::str_remove_all("[<>]") |> - # Only mention doi once, so remove the doi: string - stringr::str_remove("doi:.*?(?= )") |> - # Finally, get rid of any superflous whitespace - stringr::str_squish() + + authors <- + "Huybrechts P, Desmet P, Govaert S, Oldoni D, Van Hoey S" + title <- + "etn: Access Data from the European Tracking Network" + + doi_url = "https://doi.org/10.5281/zenodo.15235747" + + version_str <- paste("R package version", utils::packageVersion("etn")) + + docs_url <- "https://inbo.github.io/etn/" + + sprintf("%s. %s. %s, %s, %s.", authors, title, doi_url, version_str, docs_url) } # WRAPPER FUNCTIONS ---- diff --git a/tests/testthat/_snaps/get_package/bibliography.csv b/tests/testthat/_snaps/get_package/bibliography.csv index 3a1aa447..fb3fd230 100644 --- a/tests/testthat/_snaps/get_package/bibliography.csv +++ b/tests/testthat/_snaps/get_package/bibliography.csv @@ -1,6 +1,6 @@ item,type,citation ETN,data platform,"Reubens, J., Aarestrup, K., Abecasis, D. et al. The European tracking network through time: united efforts to advance aquatic conservation in Europe. Anim Biotelemetry (2026). https://doi.org/10.1186/s40317-026-00475-z" -etn,R package,"Huybrechts P, Desmet P, Govaert S, Oldoni D, Van Hoey S (2026). etn: Access Data from the European Tracking Network. https://doi.org/10.5281/zenodo.15235747, R package version 3.0.0.9000, https://inbo.github.io/etn/." +etn,R package,"Huybrechts P, Desmet P, Govaert S, Oldoni D, Van Hoey S. etn: Access Data from the European Tracking Network. https://doi.org/10.5281/zenodo.15235747, R package version 3.0.0.9000, https://inbo.github.io/etn/." 2014_demer,animal project,"Pauwels, I.; Baeyens, R.; De Maerteleire, N.; Desmet, P.; Gelaude, E.; Milotic, T.; Pieters, S.; Reyserhove, L.; Robberechts, K.; Verhelst, P.; Coeck, J.; (2020): 2014_DEMER - Acoustic telemetry data for four fish species in the Demer river (Belgium). Marine Data Archive. https://doi.org/10.14284/432" albert,acoustic project, demer,acoustic project, From c64ad1100de6b120694a900f0db69d35a1e88564 Mon Sep 17 00:00:00 2001 From: PietrH Date: Thu, 2 Jul 2026 10:18:08 +0200 Subject: [PATCH 09/21] declare utils --- R/utils.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/utils.R b/R/utils.R index 8a256d51..4f83ff12 100644 --- a/R/utils.R +++ b/R/utils.R @@ -27,7 +27,7 @@ check_value <- function(x, y, name = "value", lowercase = FALSE) { missing_value <- x[!x %in% y] # If the value is not found, check if it's a typo by calculating the # Levenshtein distance. - relative_distances <- adist(missing_value, y) / nchar(y) + relative_distances <- utils::adist(missing_value, y) / nchar(y) # If any candidate string is less than 50% different from the reference, we # can assume it's a typo and suggest it to the user. candidates_col <- cli::cli_vec(y, list("vec-trunc" = 5)) From 86a2fcfff7e97d961dd2abfdb01606afa2fd65ab Mon Sep 17 00:00:00 2001 From: PietrH Date: Thu, 2 Jul 2026 10:35:25 +0200 Subject: [PATCH 10/21] Style --- R/utils.R | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/R/utils.R b/R/utils.R index 4f83ff12..8019e974 100644 --- a/R/utils.R +++ b/R/utils.R @@ -203,7 +203,8 @@ localdb_is_available <- function() { select_protocol <- function() { # ALlow overwriting of protocol logic by environmental variable user_selected_protocol <- Sys.getenv("ETN_PROTOCOL", - unset = "no_protocol_set") + unset = "no_protocol_set" + ) if (user_selected_protocol != "no_protocol_set") { return(user_selected_protocol) } @@ -259,8 +260,8 @@ remove_html_tags <- function(x) { #' environment variables. #' @family helper functions #' @noRd -credentials_are_set <- function(){ - nzchar(Sys.getenv("ETN_USER")) && nzchar(Sys.getenv("ETN_PWD")) +credentials_are_set <- function() { + nzchar(Sys.getenv("ETN_USER")) && nzchar(Sys.getenv("ETN_PWD")) } #' Get the citation for the etn package as plain text @@ -273,14 +274,13 @@ credentials_are_set <- function(){ #' #' @family helper functions #' @noRd -etn_citation <- function(){ - +etn_citation <- function() { authors <- "Huybrechts P, Desmet P, Govaert S, Oldoni D, Van Hoey S" title <- "etn: Access Data from the European Tracking Network" - doi_url = "https://doi.org/10.5281/zenodo.15235747" + doi_url <- "https://doi.org/10.5281/zenodo.15235747" version_str <- paste("R package version", utils::packageVersion("etn")) @@ -339,13 +339,13 @@ NULL # Checking this on every call would slow down the package. get_etnservice_version <<- memoise::memoise(get_etnservice_version, - cache = cachem::cache_mem(max_age = 60 * 15) + cache = cachem::cache_mem(max_age = 60 * 15) ) # Memoisation: only validate the login credentials every 15 minutes. validate_login <<- memoise::memoise(validate_login, - cache = cachem::cache_mem(max_age = 60 * 15) - ) + cache = cachem::cache_mem(max_age = 60 * 15) + ) } #' Expand columns From aad60e6350413096a76647db757555b43588b464 Mon Sep 17 00:00:00 2001 From: PietrH Date: Thu, 2 Jul 2026 11:26:00 +0200 Subject: [PATCH 11/21] Multiple missing values are possible --- R/utils.R | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/R/utils.R b/R/utils.R index 8019e974..3f0534fb 100644 --- a/R/utils.R +++ b/R/utils.R @@ -24,10 +24,10 @@ check_value <- function(x, y, name = "value", lowercase = FALSE) { if (all(x %in% y)) { return(x) } - missing_value <- x[!x %in% y] + missing_values <- x[!x %in% y] # If the value is not found, check if it's a typo by calculating the # Levenshtein distance. - relative_distances <- utils::adist(missing_value, y) / nchar(y) + relative_distances <- utils::adist(missing_values, y) / nchar(y) # If any candidate string is less than 50% different from the reference, we # can assume it's a typo and suggest it to the user. candidates_col <- cli::cli_vec(y, list("vec-trunc" = 5)) @@ -36,7 +36,7 @@ check_value <- function(x, y, name = "value", lowercase = FALSE) { # If there are many candidates, truncate the list to 5 items for the error # message. cli::cli_abort( - "Can't find {.var {name}}: {.val {missing_value}} in: {.or {.str {candidates_col}}}. + "Can't find {.var {name}}: {.val {missing_values}} in: {.or {.str {candidates_col}}}. Did you mean {.strong {.val {closest_match}}}?", class = "etn_value_not_found_suggest", call = rlang::caller_env() @@ -47,7 +47,7 @@ check_value <- function(x, y, name = "value", lowercase = FALSE) { candidates_col <- y[order(as.vector(relative_distances))] |> cli::cli_vec(list("vec-trunc" = 5)) cli::cli_abort( - "Can't find {.var {name}}: {.val {missing_value}} in: {.or {.str {candidates_col}}}.", + "Can't find {.var {name}}: {.val {missing_values}} in: {.or {.str {candidates_col}}}.", class = "etn_value_not_found", call = rlang::caller_env() ) From ef663d4a195b5424b9acf708d8cfe8805bbdc54a Mon Sep 17 00:00:00 2001 From: PietrH Date: Thu, 2 Jul 2026 11:28:40 +0200 Subject: [PATCH 12/21] Switch to absolute transformation distance over relative --- R/utils.R | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/R/utils.R b/R/utils.R index 3f0534fb..419975a0 100644 --- a/R/utils.R +++ b/R/utils.R @@ -27,12 +27,12 @@ check_value <- function(x, y, name = "value", lowercase = FALSE) { missing_values <- x[!x %in% y] # If the value is not found, check if it's a typo by calculating the # Levenshtein distance. - relative_distances <- utils::adist(missing_values, y) / nchar(y) - # If any candidate string is less than 50% different from the reference, we - # can assume it's a typo and suggest it to the user. + distances <- utils::adist(missing_values, y) + # If any candidate string is less 3 transformations away from the reference, + # we can assume it's a typo and suggest it to the user. candidates_col <- cli::cli_vec(y, list("vec-trunc" = 5)) - if (any(relative_distances <= 0.5)) { - closest_match <- y[which.min(relative_distances)] + if (any(distances <= 3)) { + closest_match <- y[which.min(distances)] # If there are many candidates, truncate the list to 5 items for the error # message. cli::cli_abort( @@ -44,7 +44,7 @@ check_value <- function(x, y, name = "value", lowercase = FALSE) { } else { # Sort the references so the closest matches are mentioned. Only show 5 # members. - candidates_col <- y[order(as.vector(relative_distances))] |> + candidates_col <- y[order(as.vector(distances))] |> cli::cli_vec(list("vec-trunc" = 5)) cli::cli_abort( "Can't find {.var {name}}: {.val {missing_values}} in: {.or {.str {candidates_col}}}.", From 3d992d69c8bb76b7cdf2432b501b6720388df84e Mon Sep 17 00:00:00 2001 From: PietrH Date: Thu, 2 Jul 2026 13:46:57 +0200 Subject: [PATCH 13/21] Test for case where there are multiple typo's in a single entry --- tests/testthat/test-check_value.R | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/testthat/test-check_value.R b/tests/testthat/test-check_value.R index 2e015f37..5d899f35 100644 --- a/tests/testthat/test-check_value.R +++ b/tests/testthat/test-check_value.R @@ -57,3 +57,12 @@ test_that("check_value() can offer a suggestion for typos", { class = "etn_value_not_found_suggest" ) }) + +test_that("check_value() offers suggestions for multiple typos", { + expect_error( + check_value(x = c("2000_Loire", "seeschelde"), + y = c("2011_Loire", "zeeschelde") + ), + class = "etn_value_not_found_suggest" + ) +}) From 7b5327c8c02de1f62eed60f6b52f187bd7e9fa10 Mon Sep 17 00:00:00 2001 From: PietrH Date: Thu, 2 Jul 2026 13:49:02 +0200 Subject: [PATCH 14/21] Use vector support so we can handle multiple value suggestions --- R/utils.R | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/R/utils.R b/R/utils.R index 419975a0..cd9d4d0a 100644 --- a/R/utils.R +++ b/R/utils.R @@ -27,12 +27,19 @@ check_value <- function(x, y, name = "value", lowercase = FALSE) { missing_values <- x[!x %in% y] # If the value is not found, check if it's a typo by calculating the # Levenshtein distance. - distances <- utils::adist(missing_values, y) + distances <- utils::adist(missing_values, y) |> + # Transpose the matrix so we can get the minimum distance per candidate + t() |> + as.data.frame() |> + purrr::set_names(missing_values) # If any candidate string is less 3 transformations away from the reference, # we can assume it's a typo and suggest it to the user. candidates_col <- cli::cli_vec(y, list("vec-trunc" = 5)) - if (any(distances <= 3)) { - closest_match <- y[which.min(distances)] + if (any(purrr::map_lgl(distances, \(dist_for_value) {any(dist_for_value <= 3)}))) { + # We need to repeat the candidates so we can get the minimum distance + # for each missing value + closest_match <- + rep(y, length(missing_values))[purrr::map_int(distances, which.min)] # If there are many candidates, truncate the list to 5 items for the error # message. cli::cli_abort( From c799441fc0431774efec67f90bdb49dce972e61c Mon Sep 17 00:00:00 2001 From: PietrH Date: Thu, 2 Jul 2026 14:11:02 +0200 Subject: [PATCH 15/21] Update sorting to work for multiple values of x: most likely candidates in front, but don't show duplicates --- R/utils.R | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/R/utils.R b/R/utils.R index cd9d4d0a..15d7032b 100644 --- a/R/utils.R +++ b/R/utils.R @@ -51,7 +51,14 @@ check_value <- function(x, y, name = "value", lowercase = FALSE) { } else { # Sort the references so the closest matches are mentioned. Only show 5 # members. - candidates_col <- y[order(as.vector(distances))] |> + candidates_col <- purrr::map(distances, \(dist_for_value){ + y[order(as.vector(dist_for_value))] + }) |> + # Convert into a vector and truncate for the error message + purrr::reduce(rbind) |> + c() |> + # Don't repeat yourself + unique() |> cli::cli_vec(list("vec-trunc" = 5)) cli::cli_abort( "Can't find {.var {name}}: {.val {missing_values}} in: {.or {.str {candidates_col}}}.", From 873f30ee2e6dd3468ccaf443a201f2f3a6a65f97 Mon Sep 17 00:00:00 2001 From: PietrH Date: Thu, 2 Jul 2026 14:11:33 +0200 Subject: [PATCH 16/21] Test on a vector for x --- tests/testthat/test-check_value.R | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/testthat/test-check_value.R b/tests/testthat/test-check_value.R index 5d899f35..4ecd38a9 100644 --- a/tests/testthat/test-check_value.R +++ b/tests/testthat/test-check_value.R @@ -11,6 +11,10 @@ test_that("check_value() returns error for incorrect values", { check_value("invalid", c("a", "b"), name = "param_name"), class = "etn_value_not_found" ) + expect_error( + check_value(c("missing", "misval"), c("a", "b", "c")), + class = "etn_value_not_found" + ) }) test_that("check_value() returns x for correct values", { From 58f50b1a229f6d64503aa10d3d515712aade9aca Mon Sep 17 00:00:00 2001 From: PietrH Date: Thu, 2 Jul 2026 14:27:21 +0200 Subject: [PATCH 17/21] Adjust test so it doesn't hit suggestion threshold --- tests/testthat/test-check_value.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testthat/test-check_value.R b/tests/testthat/test-check_value.R index 4ecd38a9..e93e0f70 100644 --- a/tests/testthat/test-check_value.R +++ b/tests/testthat/test-check_value.R @@ -30,7 +30,7 @@ test_that("check_value() returns x for correct values", { test_that("check_value() can ignore case", { expect_error( - check_value("A", c("a", "B")), + check_value("AAAA", c("aaaa", "BBBB")), class = "etn_value_not_found" ) expect_identical( From f026e15273fbfd933494a5d3313264ef5b1b1d7d Mon Sep 17 00:00:00 2001 From: PietrH Date: Thu, 2 Jul 2026 14:30:42 +0200 Subject: [PATCH 18/21] Add variable for magic number: maximum allowed levenshtein distance for suggestion --- R/utils.R | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/R/utils.R b/R/utils.R index 15d7032b..c12f118a 100644 --- a/R/utils.R +++ b/R/utils.R @@ -8,10 +8,12 @@ #' @param name Name of the parameter. #' @param lowercase If `TRUE`, the case of `x` and `y` values will ignored and #' `x` values will be returned lowercase. +#' @param max_dist Maximum Levenshtein distance to consider a value a typo. +#' This variable is used to offer suggestions when a typo is suspected. #' @returns Error or (lowercase) `x` values. #' @family helper functions #' @noRd -check_value <- function(x, y, name = "value", lowercase = FALSE) { +check_value <- function(x, y, name = "value", lowercase = FALSE, max_dist = 3) { # Remove NA from valid values y <- y[!is.na(y)] From 143144504ca0b798796c82f35f63157120beb6c7 Mon Sep 17 00:00:00 2001 From: PietrH Date: Thu, 2 Jul 2026 14:30:55 +0200 Subject: [PATCH 19/21] Add more detailed documentation for more complex behaviour --- R/utils.R | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/R/utils.R b/R/utils.R index c12f118a..f75a37f5 100644 --- a/R/utils.R +++ b/R/utils.R @@ -2,6 +2,14 @@ #' Check input value against valid values #' +#' This function checks if the input value(s) `x` are present in the valid +#' values `y`. If any value in `x` is not found in `y`, an error is thrown. If +#' `lowercase` is set to `TRUE`, the case of the values will be ignored during +#' the comparison, and the returned values will be converted to lowercase. +#' +#' A suggestion will be offered when a value is not found, based on the +#' Levenshtein distance to the valid values. +#' #' @param x Value(s) to test. #' `NULL` values will automatically pass. #' @param y Value(s) to test against. From ce6af956f5358c237411e8f5096ad0a29df911db Mon Sep 17 00:00:00 2001 From: PietrH Date: Thu, 2 Jul 2026 14:31:02 +0200 Subject: [PATCH 20/21] Style --- R/utils.R | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/R/utils.R b/R/utils.R index f75a37f5..e1ae9373 100644 --- a/R/utils.R +++ b/R/utils.R @@ -45,7 +45,9 @@ check_value <- function(x, y, name = "value", lowercase = FALSE, max_dist = 3) { # If any candidate string is less 3 transformations away from the reference, # we can assume it's a typo and suggest it to the user. candidates_col <- cli::cli_vec(y, list("vec-trunc" = 5)) - if (any(purrr::map_lgl(distances, \(dist_for_value) {any(dist_for_value <= 3)}))) { + if (any(purrr::map_lgl(distances, \(dist_for_value) { + any(dist_for_value <= max_dist) + }))) { # We need to repeat the candidates so we can get the minimum distance # for each missing value closest_match <- From 5d8f52a4a48f71250a1b62225364c9cc715d9780 Mon Sep 17 00:00:00 2001 From: PietrH Date: Thu, 2 Jul 2026 14:31:56 +0200 Subject: [PATCH 21/21] Add reference to inspiration. --- R/utils.R | 2 ++ 1 file changed, 2 insertions(+) diff --git a/R/utils.R b/R/utils.R index e1ae9373..40252910 100644 --- a/R/utils.R +++ b/R/utils.R @@ -10,6 +10,8 @@ #' A suggestion will be offered when a value is not found, based on the #' Levenshtein distance to the valid values. #' +#' This function was inspired by `rlang:::stop_arg_match()`. +#' #' @param x Value(s) to test. #' `NULL` values will automatically pass. #' @param y Value(s) to test against.