diff --git a/.Rbuildignore b/.Rbuildignore index 9a4dcb7..ec2ed9c 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -16,6 +16,7 @@ ^[.]?air[.]toml$ ^\.vscode$ ^jarl\.yml$ +^jarl\.toml$ ^data-raw$ ^AGENTS.md$ ^assets$ diff --git a/CITATION.cff b/CITATION.cff index 10d771d..fcc4aba 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -8,7 +8,7 @@ message: 'To cite package "GeoLocatoR" in publications use:' type: software license: GPL-3.0-or-later title: 'GeoLocatoR: R package for GeoLocator Data Package' -version: 1.3.0 +version: 1.3.1 identifiers: - type: url value: https://geopressure.org/GeoLocatoR/ diff --git a/DESCRIPTION b/DESCRIPTION index f1e44ff..a6d0c88 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: GeoLocatoR Title: R package for GeoLocator Data Package -Version: 1.3.0 +Version: 1.3.1 Authors@R: c( person("Raphaƫl", "Nussbaumer", , "rafnuss@gmail.com", role = c("aut", "cre"), comment = c(ORCID = "0000-0002-8185-1020")), diff --git a/NEWS.md b/NEWS.md index f08207f..1a41d8b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,18 @@ +# GeoLocatoR v1.3.1 + +## Main + +- [Keep the retrieval latitude in `read_soi_gld()`](https://github.com/GeoPressure/GeoLocatoR/commit/315d186). The default column selection (`filter_col = TRUE`) listed `LongitudeRemoved` and `LongitudeAttached` twice and omitted `LatitudeRemoved`, so every retrieval observation created by `read_soi()` had no latitude. Packages created from SOI data with earlier versions should be recreated, or their retrieval latitudes filled. Coordinates recorded as `99999` (unknown in the SOI database) are now read as missing instead of being written as observations. +- [Read dates from the Access file in `read_soi_gld()` on macOS and Linux](https://github.com/GeoPressure/GeoLocatoR/commit/d442338). `mdb-export` wrote them as `MM/DD/YY HH:MM:SS`, which was parsed as `NA`, so every date (attachment, retrieval, RTC, first and last data, delivery) was missing and `read_soi()` created equipment and retrieval observations without a datetime. They are now exported in ISO format. Packages created from an Access file on these systems with earlier versions should be recreated. + +## Minor + +- [Select the columns given in a character `filter_col`](https://github.com/GeoPressure/GeoLocatoR/commit/315d186) in `read_soi_gld()`, which previously always failed, and accept data frames for `data_file` and `order_file` as documented. +- [Read `AccSamplingRate`, `PrioritySensor` and `PriorityMemory` with their actual types](https://github.com/GeoPressure/GeoLocatoR/commit/315d186) (number, text and percentage) instead of logical, which turned their values into `NA`. +- [Warn when `read_soi()` cannot read the data of a tag](https://github.com/GeoPressure/GeoLocatoR/commit/315d186), naming the tag and the error, instead of silently leaving its measurements out. + +**Full Changelog**: + # GeoLocatoR v1.3.0 ## Main diff --git a/R/read_soi.R b/R/read_soi.R index fb89cdc..cc34f32 100644 --- a/R/read_soi.R +++ b/R/read_soi.R @@ -100,6 +100,10 @@ read_soi <- function( } }, error = function(e) { + cli_warn(c( + "!" = "Could not read the data of tag {.val {GDL_ID}} in {.file {directory}}. Its measurements are skipped.", + "x" = conditionMessage(e) + )) list() # Return empty list on error } ) diff --git a/R/read_soi_gld.R b/R/read_soi_gld.R index f4988b3..e3878ad 100644 --- a/R/read_soi_gld.R +++ b/R/read_soi_gld.R @@ -16,9 +16,9 @@ #' memory. Required when `access_file` is not provided. #' @param order_file Path to the GDL order file, or a data frame already loaded #' in memory. Required when `access_file` is not provided. -#' @param filter_col Logical. If `TRUE` (default), returns a predefined subset -#' of columns useful for package creation; if `FALSE`, returns all joined -#' columns. +#' @param filter_col Logical or character vector. If `TRUE` (default), returns +#' a predefined subset of columns useful for package creation; if `FALSE`, +#' returns all joined columns; if a character vector, returns these columns. #' #' @return A data frame combining GDL tag rows and order-level information. #' @@ -36,8 +36,11 @@ read_soi_gld <- function( order_file = NA, filter_col = TRUE ) { - if (!is.na(access_file)) { - if (!is.na(data_file) || !is.na(order_file)) { + # A file path or a data frame counts as provided + provided <- function(x) is.data.frame(x) || !(length(x) == 1 && is.na(x)) + + if (provided(access_file)) { + if (provided(data_file) || provided(order_file)) { cli_warn(c( "!" = "{.arg access_file} was provided alongside {.arg data_file} or {.arg order_file}.", ">" = "We'll use {.arg access_file}." @@ -47,7 +50,7 @@ read_soi_gld <- function( data_file <- data_order_file[[1]] order_file <- data_order_file[[2]] } else { - if (is.na(data_file) || is.na(order_file)) { + if (!provided(data_file) || !provided(order_file)) { cli_abort(c( "x" = "Need {.arg access_file} or both {.arg data_file} and {.arg order_file}." )) @@ -60,6 +63,20 @@ read_soi_gld <- function( d <- read_soi_gld_data(data_file) } + # 99999 encodes an unknown coordinate in the SOI database + d <- mutate( + d, + across( + any_of(c( + "LongitudeAttached", + "LatitudeAttached", + "LongitudeRemoved", + "LatitudeRemoved" + )), + ~ dplyr::if_else(.x == 99999, NA_real_, as.numeric(.x)) + ) + ) + if (is.data.frame(order_file)) { o <- order_file } else { @@ -105,8 +122,7 @@ read_soi_gld <- function( "LatitudeAttached", "UTC_Removed", "LongitudeRemoved", - "LongitudeRemoved", - "LongitudeAttached", + "LatitudeRemoved", # Information needed for package "RingNumber", @@ -118,7 +134,7 @@ read_soi_gld <- function( ) } } else { - gdl <- select(gdl, "filter_col") + gdl <- select(gdl, all_of(filter_col)) } return(gdl) @@ -178,7 +194,7 @@ read_soi_gld_orders <- function(order_file) { BreastHarnessDiameterHead = readr::col_double(), BreastHarnessDiameterTail = readr::col_double(), HarnessDescription = readr::col_character(), - AccSamplingRate = readr::col_logical(), + AccSamplingRate = readr::col_double(), AccXaxisEnabled = readr::col_logical(), AccYaxisEnabled = readr::col_logical(), AccZaxisEnabled = readr::col_logical(), @@ -195,7 +211,7 @@ read_soi_gld_orders <- function(order_file) { TRSchedule = readr::col_character(), LoggingSchedule = readr::col_character(), ActiveAfterWakeUp = readr::col_double(), - PrioritySensor = readr::col_logical(), + PrioritySensor = readr::col_character(), PriorityMemory = readr::col_double() ) ) |> @@ -283,8 +299,8 @@ read_soi_gld_data <- function(data_file) { Frequency = readr::col_double(), MagCalib = readr::col_character(), SleepCurrent = readr::col_double(), - PrioritySensor = readr::col_logical(), - PriorityMemory = readr::col_logical(), + PrioritySensor = readr::col_character(), + PriorityMemory = readr::col_double(), LongitudeRemoved = readr::col_double() ) ) @@ -310,9 +326,18 @@ read_soi_gld_access <- function( )) } + # Exporting needs an Access file and mdb-export or the Access ODBC driver, + # which are not available on CI + # nocov start if (.Platform$OS.type == "unix") { + # mdb-export writes dates as MM/DD/YY by default, which readr cannot parse + # and turns into NA, so request ISO dates explicitly + date_args <- "-D '%Y-%m-%d' -T '%Y-%m-%d %H:%M:%S'" + # Export data - system(glue::glue("mdb-export '{access_file}' GDL_Data > {data_file}")) + system(glue::glue( + "mdb-export {date_args} '{access_file}' GDL_Data > {data_file}" + )) if (!file.exists(data_file)) { cli_abort(c( @@ -322,7 +347,9 @@ read_soi_gld_access <- function( } # Export orders - system(glue::glue("mdb-export '{access_file}' GDL_Orders > {order_file}")) + system(glue::glue( + "mdb-export {date_args} '{access_file}' GDL_Orders > {order_file}" + )) if (!file.exists(order_file)) { cli_abort(c( "x" = "The file {.file {order_file}} does not exist.", @@ -347,6 +374,7 @@ read_soi_gld_access <- function( readr::write_csv(DBI::dbReadTable(con, "GDL_Orders"), order_file) readr::write_csv(DBI::dbReadTable(con, "GDL_Data"), data_file) } + # nocov end invisible(c(data_file, order_file)) } diff --git a/jarl.toml b/jarl.toml new file mode 100644 index 0000000..e5b38f3 --- /dev/null +++ b/jarl.toml @@ -0,0 +1,3 @@ +[lint.undesirable_operator] +# Allow `<<-`, used deliberately to update state from closures; keep `:::` and `->>` banned +operators = ["->>", ":::"] diff --git a/man/read_soi_gld.Rd b/man/read_soi_gld.Rd index eb9c2a6..c7ebe48 100644 --- a/man/read_soi_gld.Rd +++ b/man/read_soi_gld.Rd @@ -22,9 +22,9 @@ memory. Required when \code{access_file} is not provided.} \item{order_file}{Path to the GDL order file, or a data frame already loaded in memory. Required when \code{access_file} is not provided.} -\item{filter_col}{Logical. If \code{TRUE} (default), returns a predefined subset -of columns useful for package creation; if \code{FALSE}, returns all joined -columns.} +\item{filter_col}{Logical or character vector. If \code{TRUE} (default), returns +a predefined subset of columns useful for package creation; if \code{FALSE}, +returns all joined columns; if a character vector, returns these columns.} } \value{ A data frame combining GDL tag rows and order-level information. diff --git a/tests/testthat/test-read_soi_gld.R b/tests/testthat/test-read_soi_gld.R new file mode 100644 index 0000000..0b19896 --- /dev/null +++ b/tests/testthat/test-read_soi_gld.R @@ -0,0 +1,66 @@ +test_that("read_soi_gld() keeps retrieval coordinates and handles column selection", { + d <- tibble( + OrderName = c("O1", "O1"), + GDL_ID = c("A1", "A2"), + GDL_Type = "GDL3pam", + Species = "Upupa epops", + UTC_Attached = as.POSIXct("2020-05-01", tz = "UTC"), + SiteAttached = "Site", + LongitudeAttached = 7.5, + LatitudeAttached = 46.2, + UTC_Removed = as.POSIXct("2021-05-01", tz = "UTC"), + LongitudeRemoved = c(8.31, 99999), + LatitudeRemoved = c(47.47, 99999), + RingNumber = c("R1", "R2"), + FirmwareVersion = "2.4", + HardwareVersion = "2.4", + TotalWeight = 1.5, + Remarks = NA_character_ + ) + o <- tibble( + OrderName = "O1", + NumberOrdered = 2L, + NumberDelivered = 2L, + GDL_Type = "GDL3pam", + Species = "Upupa epops", + Client = "SOI", + ResponsibleP3 = "P3", + ClientCategory = "own", + RemarksProduction = NA_character_, + RemarksOrder = NA_character_ + ) + + # Default selection keeps the retrieval latitude, and 99999 is read as unknown + gdl <- read_soi_gld(data_file = d, order_file = o) + expect_true(all(c("LongitudeRemoved", "LatitudeRemoved") %in% names(gdl))) + expect_equal(gdl$LatitudeRemoved, c(47.47, NA)) + expect_equal(gdl$LongitudeRemoved, c(8.31, NA)) + + # A character vector selects these columns + gdl <- read_soi_gld(data_file = d, order_file = o, filter_col = c("GDL_ID", "Client")) + expect_named(gdl, c("GDL_ID", "Client")) + + expect_error(read_soi_gld(data_file = d), "Need") + expect_warning( + expect_error( + read_soi_gld(access_file = "missing.accdb", data_file = d), + "does not exist" + ), + "access_file" + ) + + # CSV exports are read with their declared column types; partial exports warn about the + # columns they lack + data_file <- tempfile(fileext = ".csv") + order_file <- tempfile(fileext = ".csv") + on.exit(unlink(c(data_file, order_file)), add = TRUE) + write.csv(mutate(d, PrioritySensor = "LIGHT", PriorityMemory = 20), data_file, row.names = FALSE) + write.csv(mutate(o, AccSamplingRate = 25), order_file, row.names = FALSE) + gdl <- suppressWarnings( + read_soi_gld(data_file = data_file, order_file = order_file, filter_col = FALSE) + ) + expect_equal(gdl$LatitudeRemoved, c(47.47, NA)) + expect_identical(gdl$PrioritySensor, c("LIGHT", "LIGHT")) + expect_identical(gdl$PriorityMemory, c(20, 20)) + expect_identical(suppressWarnings(read_soi_gld_orders(order_file))$AccSamplingRate, 25) +})