diff --git a/DESCRIPTION b/DESCRIPTION index 3bda659..3e1abd7 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -7,7 +7,8 @@ Authors@R: c( ) Description: Helper functions to work with spreadsheets and the "A1:D10" style of cell range specification. -Depends: R (>= 3.0.0) +Depends: + R (>= 3.0.0) License: MIT + file LICENSE LazyData: true URL: https://github.com/jennybc/cellranger @@ -15,3 +16,4 @@ BugReports: https://github.com/jennybc/cellranger/issues Suggests: covr, testthat +RoxygenNote: 5.0.1.9000 diff --git a/NAMESPACE b/NAMESPACE index 4e76b34..9cb1b85 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,4 +1,4 @@ -# Generated by roxygen2 (4.1.1): do not edit by hand +# Generated by roxygen2: do not edit by hand S3method(as.cell_limits,"NULL") S3method(as.cell_limits,cell_limits) diff --git a/R/cell-limits.R b/R/cell-limits.R index 6d18d93..6d84ef4 100644 --- a/R/cell-limits.R +++ b/R/cell-limits.R @@ -5,13 +5,15 @@ #' operations on a spreadsheet. Downstream code can be written assuming cell #' limits are stored in a valid \code{cell_limits} object. #' -#' A \code{cell_limits} object is a list with two components: +#' A \code{cell_limits} object is a list with three components: #' #' \itemize{ #' \item \code{ul} vector specifying upper left cell of target rectangle, of #' the form \code{c(ROW_MIN, COL_MIN)} #' \item \code{lr} vector specifying lower right cell of target rectangle, of #' the form \code{c(ROW_MAX, COL_MAX)} +#' \item \code{wsn} string specifying worksheet name, which may be \code{NA}, +#' meaning it's unspecified #' } #' #' This follows the spreadsheet convention where a cell range is described as @@ -29,6 +31,7 @@ #' #' @param ul vector identifying upper left cell of target rectangle #' @param lr vector identifying lower right cell of target rectangle +#' @param wsn string containing worksheet name, optional #' @param x input to convert into a \code{cell_limits} object #' #' @return a \code{cell_limits} object @@ -39,13 +42,17 @@ #' cell_limits(c(NA, 7)) #' cell_limits(lr = c(3, 7)) #' +#' cell_limits(c(1, 3), c(1, 5), "Sheet1") +#' cell_limits(c(1, 3), c(1, 5), "Spaces are evil") +#' #' dim(as.cell_limits("A1:F10")) #' #' @export cell_limits <- function(ul = c(NA_integer_, NA_integer_), - lr = c(NA_integer_, NA_integer_)) { + lr = c(NA_integer_, NA_integer_), + wsn = NA_character_) { - stopifnot(length(ul) == 2L, length(lr) == 2L) + stopifnot(length(ul) == 2L, length(lr) == 2L, length(wsn) == 1L) ul <- as.integer(ul) lr <- as.integer(lr) @@ -54,16 +61,16 @@ cell_limits <- function(ul = c(NA_integer_, NA_integer_), stopifnot(all(NA_or_pos(ul))) stopifnot(all(NA_or_pos(lr))) - if(is.na(ul[1]) && !is.na(lr[1])) ul[1] <- 1L - if(is.na(ul[2]) && !is.na(lr[2])) ul[2] <- 1L + if (is.na(ul[1]) && !is.na(lr[1])) ul[1] <- 1L + if (is.na(ul[2]) && !is.na(lr[2])) ul[2] <- 1L rows <- c(ul[1], lr[1]) cols <- c(ul[2], lr[2]) - if(!anyNA(rows)) stopifnot(rows[1] <= rows[2]) - if(!anyNA(cols)) stopifnot(cols[1] <= cols[2]) + if (!anyNA(rows)) stopifnot(rows[1] <= rows[2]) + if (!anyNA(cols)) stopifnot(cols[1] <= cols[2]) - structure(list(ul = ul, lr = lr), + structure(list(ul = ul, lr = lr, wsn = wsn), class = c("cell_limits", "list")) } @@ -72,9 +79,10 @@ cell_limits <- function(ul = c(NA_integer_, NA_integer_), print.cell_limits <- function(x, ...) { ul <- ifelse(is.na(x$ul), "-", as.character(x$ul)) lr <- ifelse(is.na(x$lr), "-", as.character(x$lr)) + wsn <- if (is.na(x$wsn)) "" else paste0(" in '", x$wsn, "'") cat("\n", + lr[1], ", ", lr[2], ")", wsn, ">\n", sep = "") } @@ -101,6 +109,8 @@ as.cell_limits.NULL <- function(x) cell_limits() #' as.cell_limits("A1:D8") #' as.cell_limits("R5C11") #' as.cell_limits("R2C3:R6C9") +#' as.cell_limits("Sheet1!R2C3:R6C9") +#' as.cell_limits("'Spaces are evil'!R2C3:R6C9") #' #' @export as.cell_limits.character <- function(x) { @@ -108,30 +118,34 @@ as.cell_limits.character <- function(x) { stopifnot(length(x) == 1L) x_orig <- x - x <- rm_dollar_signs(x) - - y <- unlist(strsplit(x, ":")) - stopifnot(length(y) %in% 1:2) + y <- unlist(strsplit(x, "!")) + wsn <- if (length(y) > 1) y[[1]] else NA_character_ + wsn <- remove_single_quotes(wsn) + raw_rg <- y[[length(y)]] + raw_rg <- rm_dollar_signs(raw_rg) - y <- rep_len(y[!grepl("\\s+", y)], 2) + rg <- unlist(strsplit(raw_rg, ":")) + stopifnot(length(rg) %in% 1:2) + rg <- rep_len(rg[!grepl("\\s+", rg)], 2) RC_regex <- "^R([0-9]+)C([0-9]+$)" A1_regex <- "^[A-Za-z]{1,3}[0-9]+$" - if(all(grepl(A1_regex, y))) { - y <- A1_to_RC(y) - } else if(!all(grepl(RC_regex, y))) { + if (all(grepl(A1_regex, rg))) { + rg <- A1_to_RC(rg) + } else if (!all(grepl(RC_regex, rg))) { stop("Trying to set cell limits, but requested range is invalid:\n", - x_orig) + x_orig) } - m <- regexec("^R([0-9]+)C([0-9]+$)", y) - m2 <- regmatches(y, m) + m <- regexec("^R([0-9]+)C([0-9]+$)", rg) + m2 <- regmatches(rg, m) jfun <- function(x) as.integer(x[2:3]) cell_limits( jfun(m2[[1]]), - jfun(m2[[2]]) + jfun(m2[[2]]), + wsn ) } @@ -140,6 +154,8 @@ as.cell_limits.character <- function(x) { #' #' @param x a cell_limits object #' @param RC logical, requesting "R1C1" positioning notation +#' @param wsn logical, specifying whether worksheet name should be prepended to +#' the range, e.g. \code{Sheet1!A1:D4} #' #' @return length one character vector holding a cell range, in either A1 or #' R1C1 positioning notation @@ -149,19 +165,26 @@ as.cell_limits.character <- function(x) { #' as.range(rgCL) #' as.range(rgCL, RC = TRUE) #' +#' rgCL_ws <- cell_limits(ul = c(1, 2), lr = c(7, 6), wsn = "A Sheet") +#' as.range(rgCL_ws) +#' as.range(rgCL_ws, RC = TRUE, wsn = TRUE) +#' #' @export -as.range <- function(x, RC = FALSE) { +as.range <- function(x, RC = FALSE, wsn = FALSE) { - stopifnot(inherits(x, "cell_limits")) + stopifnot(inherits(x, "cell_limits"), + isTOGGLE(RC), isTOGGLE(wsn)) - if(any(is.na(unlist(x)))) return(NA_character_) + if (anyNA(unlist(x[c("ul", "lr")]))) return(NA_character_) - range <- c(paste0("R", x$ul[1], "C", x$ul[2]), - paste0("R", x$lr[1], "C", x$lr[2])) - - if(!RC) { + range <- c(row_col_to_RC(x$ul), row_col_to_RC(x$lr)) + if (!RC) { range <- RC_to_A1(range) } + range <- paste(range, collapse = ":") - paste(range, collapse = ":") + if (wsn && !is.na(x$wsn)) { + range <- paste(add_single_quotes(x$wsn), range, sep = "!") + } + range } diff --git a/R/row-col-to-from-RC.R b/R/row-col-to-from-RC.R new file mode 100644 index 0000000..f5f91a0 --- /dev/null +++ b/R/row-col-to-from-RC.R @@ -0,0 +1 @@ +row_col_to_RC <- function(x) paste0("R", x[1], "C", x[2]) diff --git a/R/utils.R b/R/utils.R index 6328c77..c672908 100644 --- a/R/utils.R +++ b/R/utils.R @@ -1,6 +1,14 @@ rm_dollar_signs <- function(x) gsub('$', '', x, fixed = TRUE) -char0_to_NA <- function(x) if(length(x) < 1) NA_character_ else x +char0_to_NA <- function(x) if (length(x) < 1) NA_character_ else x isTOGGLE <- function(x) is.null(x) || isTRUE(x) || identical(x, FALSE) +add_single_quotes <- function(x) { + if (grepl("\\s+", x)) { + x <- paste0("'", x, "'") + } + x +} + +remove_single_quotes <- function(x) gsub("^'|'$", "", x) diff --git a/man/A1_to_RC.Rd b/man/A1_to_RC.Rd index 33bef2d..4f20057 100644 --- a/man/A1_to_RC.Rd +++ b/man/A1_to_RC.Rd @@ -1,4 +1,4 @@ -% Generated by roxygen2 (4.1.1): do not edit by hand +% Generated by roxygen2: do not edit by hand % Please edit documentation in R/A1-to-from-RC.R \name{A1_to_RC} \alias{A1_to_RC} @@ -22,5 +22,6 @@ A1_to_RC("AZ10") A1_to_RC("AZ$10") A1_to_RC(c("A1", "AZ10")) A1_to_RC(c("", NA, "Q0")) + } diff --git a/man/RC_to_A1.Rd b/man/RC_to_A1.Rd index fee5b18..75a4da7 100644 --- a/man/RC_to_A1.Rd +++ b/man/RC_to_A1.Rd @@ -1,4 +1,4 @@ -% Generated by roxygen2 (4.1.1): do not edit by hand +% Generated by roxygen2: do not edit by hand % Please edit documentation in R/A1-to-from-RC.R \name{RC_to_A1} \alias{RC_to_A1} @@ -20,5 +20,6 @@ RC_to_A1("R1C1") RC_to_A1("R10C52") RC_to_A1(c("R1C1", "R10C52")) RC_to_A1(c("", NA, "R0C0")) + } diff --git a/man/anchored.Rd b/man/anchored.Rd index 0deaae8..fdda005 100644 --- a/man/anchored.Rd +++ b/man/anchored.Rd @@ -1,4 +1,4 @@ -% Generated by roxygen2 (4.1.1): do not edit by hand +% Generated by roxygen2: do not edit by hand % Please edit documentation in R/anchor.R \name{anchored} \alias{anchored} @@ -22,7 +22,7 @@ column or variable names of a two-dimensional input; if omitted, will be determined by checking whether \code{input} has column names} \item{byrow}{logical, indicating whether a one-dimensional input should run - down or to the right} +down or to the right} } \value{ a \code{\link{cell_limits}} object @@ -70,5 +70,6 @@ dim(anchored(input = input)) anchored(input = input, byrow = TRUE) as.range(anchored(input = input, byrow = TRUE), RC = TRUE) dim(anchored(input = input, byrow = TRUE)) + } diff --git a/man/as.range.Rd b/man/as.range.Rd index 6aefac6..d32ae6a 100644 --- a/man/as.range.Rd +++ b/man/as.range.Rd @@ -1,15 +1,18 @@ -% Generated by roxygen2 (4.1.1): do not edit by hand +% Generated by roxygen2: do not edit by hand % Please edit documentation in R/cell-limits.R \name{as.range} \alias{as.range} \title{Convert a cell_limits object to a cell range} \usage{ -as.range(x, RC = FALSE) +as.range(x, RC = FALSE, wsn = FALSE) } \arguments{ \item{x}{a cell_limits object} \item{RC}{logical, requesting "R1C1" positioning notation} + +\item{wsn}{logical, specifying whether worksheet name should be prepended to +the range, e.g. \code{Sheet1!A1:D4}} } \value{ length one character vector holding a cell range, in either A1 or @@ -22,5 +25,10 @@ Convert a cell_limits object to a cell range rgCL <- cell_limits(ul = c(1, 2), lr = c(7, 6)) as.range(rgCL) as.range(rgCL, RC = TRUE) + +rgCL_ws <- cell_limits(ul = c(1, 2), lr = c(7, 6), wsn = "A Sheet") +as.range(rgCL_ws) +as.range(rgCL_ws, RC = TRUE, wsn = TRUE) + } diff --git a/man/cell_cols.Rd b/man/cell_cols.Rd index 6e8f067..1c25c92 100644 --- a/man/cell_cols.Rd +++ b/man/cell_cols.Rd @@ -1,4 +1,4 @@ -% Generated by roxygen2 (4.1.1): do not edit by hand +% Generated by roxygen2: do not edit by hand % Please edit documentation in R/cell-rows-cell-cols.R \name{cell_cols} \alias{cell_cols} @@ -8,7 +8,7 @@ cell_cols(x) } \arguments{ \item{x}{vector of column limits; if character, converted to numeric; if - length greater than two, min and max will be taken with \code{NA.rm = TRUE}} +length greater than two, min and max will be taken with \code{NA.rm = TRUE}} } \value{ a \code{\link{cell_limits}} object @@ -31,5 +31,6 @@ cell_cols(c(3, NA, 10)) cell_cols("C:G") cell_cols(c("B", NA)) cell_cols(LETTERS) + } diff --git a/man/cell_limits.Rd b/man/cell_limits.Rd index 15ef96d..3d9cf98 100644 --- a/man/cell_limits.Rd +++ b/man/cell_limits.Rd @@ -1,4 +1,4 @@ -% Generated by roxygen2 (4.1.1): do not edit by hand +% Generated by roxygen2: do not edit by hand % Please edit documentation in R/cell-limits.R \name{cell_limits} \alias{as.cell_limits} @@ -10,7 +10,7 @@ \title{Create a cell_limits object} \usage{ cell_limits(ul = c(NA_integer_, NA_integer_), lr = c(NA_integer_, - NA_integer_)) + NA_integer_), wsn = NA_character_) \method{dim}{cell_limits}(x) @@ -27,6 +27,8 @@ as.cell_limits(x) \item{lr}{vector identifying lower right cell of target rectangle} +\item{wsn}{string containing worksheet name, optional} + \item{x}{input to convert into a \code{cell_limits} object} } \value{ @@ -39,13 +41,15 @@ operations on a spreadsheet. Downstream code can be written assuming cell limits are stored in a valid \code{cell_limits} object. } \details{ -A \code{cell_limits} object is a list with two components: +A \code{cell_limits} object is a list with three components: \itemize{ \item \code{ul} vector specifying upper left cell of target rectangle, of the form \code{c(ROW_MIN, COL_MIN)} \item \code{lr} vector specifying lower right cell of target rectangle, of the form \code{c(ROW_MAX, COL_MAX)} + \item \code{wsn} string specifying worksheet name, which may be \code{NA}, + meaning it's unspecified } This follows the spreadsheet convention where a cell range is described as @@ -67,11 +71,18 @@ cell_limits(c(NA, 7), c(3, NA)) cell_limits(c(NA, 7)) cell_limits(lr = c(3, 7)) +cell_limits(c(1, 3), c(1, 5), "Sheet1") +cell_limits(c(1, 3), c(1, 5), "Spaces are evil") + dim(as.cell_limits("A1:F10")) + as.cell_limits("A1") as.cell_limits("Q24") as.cell_limits("A1:D8") as.cell_limits("R5C11") as.cell_limits("R2C3:R6C9") +as.cell_limits("Sheet1!R2C3:R6C9") +as.cell_limits("'Spaces are evil'!R2C3:R6C9") + } diff --git a/man/cell_rows.Rd b/man/cell_rows.Rd index b327680..1793fa6 100644 --- a/man/cell_rows.Rd +++ b/man/cell_rows.Rd @@ -1,4 +1,4 @@ -% Generated by roxygen2 (4.1.1): do not edit by hand +% Generated by roxygen2: do not edit by hand % Please edit documentation in R/cell-rows-cell-cols.R \name{cell_rows} \alias{cell_rows} @@ -8,7 +8,7 @@ cell_rows(x) } \arguments{ \item{x}{numeric vector of row limits; if length greater than two, min and - max will be taken with \code{NA.rm = TRUE}} +max will be taken with \code{NA.rm = TRUE}} } \value{ a \code{\link{cell_limits}} object @@ -28,5 +28,6 @@ cell_rows(4:16) cell_rows(c(3, NA, 10)) dim(cell_rows(1:5)) + } diff --git a/man/cellranger.Rd b/man/cellranger.Rd index 0566f8a..2ca390d 100644 --- a/man/cellranger.Rd +++ b/man/cellranger.Rd @@ -1,4 +1,4 @@ -% Generated by roxygen2 (4.1.1): do not edit by hand +% Generated by roxygen2: do not edit by hand % Please edit documentation in R/cellranger-package.r \docType{package} \name{cellranger} diff --git a/man/letter_to_num.Rd b/man/letter_to_num.Rd index b1ed504..15a2e57 100644 --- a/man/letter_to_num.Rd +++ b/man/letter_to_num.Rd @@ -1,4 +1,4 @@ -% Generated by roxygen2 (4.1.1): do not edit by hand +% Generated by roxygen2: do not edit by hand % Please edit documentation in R/A1-to-from-RC.R \name{letter_to_num} \alias{letter_to_num} @@ -19,5 +19,6 @@ Convert column IDs from letter representation to integer letter_to_num('Z') letter_to_num(c('AA', 'ZZ', 'ABD', 'ZZZ')) letter_to_num(c(NA, '')) + } diff --git a/man/num_to_letter.Rd b/man/num_to_letter.Rd index 00d6208..c5e266d 100644 --- a/man/num_to_letter.Rd +++ b/man/num_to_letter.Rd @@ -1,4 +1,4 @@ -% Generated by roxygen2 (4.1.1): do not edit by hand +% Generated by roxygen2: do not edit by hand % Please edit documentation in R/A1-to-from-RC.R \name{num_to_letter} \alias{num_to_letter} @@ -21,5 +21,6 @@ num_to_letter(900) num_to_letter(18278) num_to_letter(c(25, 52, 900, 18278)) num_to_letter(c(NA, 0, 4.8, -4)) + } diff --git a/tests/testthat/test-cell-specification.R b/tests/testthat/test-cell-specification.R index 6724e32..a38aeea 100644 --- a/tests/testthat/test-cell-specification.R +++ b/tests/testthat/test-cell-specification.R @@ -69,6 +69,15 @@ test_that("Cell range is converted to a cell_limit object and vice versa", { expect_equal(as.range(rgCL), rgA1) expect_equal(as.range(rgCL, RC = TRUE), rgRC) + rgA1sheet <- "sheet!A1:C4" + rgRCsheet <- "sheet!R1C1:R4C3" + rgCLwsn <- cell_limits(ul = c(1, 1), lr = c(4, 3), wsn = "sheet") + expect_equal(as.cell_limits(rgA1sheet), rgCLwsn) + expect_equal(as.cell_limits(rgRCsheet), rgCLwsn) + expect_equal(as.range(rgCLwsn), rgA1) + expect_equal(as.range(rgCLwsn, RC = TRUE), rgRC) + expect_equal(as.range(rgCLwsn, RC = TRUE, wsn = TRUE), rgRCsheet) + rgA1 <- "E7" rgA1A1 <- "E7:E7" rgRC <- "R7C5" @@ -81,11 +90,29 @@ test_that("Cell range is converted to a cell_limit object and vice versa", { expect_equal(as.range(rgCL), rgA1A1) expect_equal(as.range(rgCL, RC = TRUE), rgRCRC) + rgA1sheet <- "sheet!E7" + rgA1A1sheet <- "sheet!E7:E7" + rgRCsheet <- "sheet!R7C5" + rgRCRCsheet <- "sheet!R7C5:R7C5" + rgCLsheet <- cell_limits(ul = c(7, 5), lr = c(7, 5), wsn = "sheet") + expect_equal(as.cell_limits(rgA1sheet), rgCLsheet) + expect_equal(as.cell_limits(rgRCsheet), rgCLsheet) + expect_equal(as.cell_limits(rgA1A1sheet), rgCLsheet) + expect_equal(as.cell_limits(rgRCRCsheet), rgCLsheet) + expect_equal(as.range(rgCLsheet, wsn = TRUE), rgA1A1sheet) + expect_equal(as.range(rgCLsheet, RC = TRUE, wsn = TRUE), rgRCRCsheet) + rgCL <- cell_limits(ul = c(NA, 1), lr = c(4, NA)) expect_true(is.na(as.range(rgCL))) }) +test_that("Whitespace-contained sheet names gain/lose single quotes", { + x <- cell_limits(ul = c(1, 1), lr = c(4, 3), wsn = "aaa bbb") + expect_identical(as.range(x, wsn = TRUE), "'aaa bbb'!A1:C4") + expect_identical(as.cell_limits("'aaa bbb'!A1:C4"), x) +}) + test_that("Bad cell ranges throw errors", { expect_error(as.cell_limits("eggplant")) @@ -162,6 +189,8 @@ test_that("Print method works", { expect_output(print(cell_limits(c(NA, 7), c(3, NA))), "", fixed = TRUE) + expect_output(print(cell_limits(c(NA, 7), c(3, NA), "a sheet")), + "", fixed = TRUE) })