R/table-object.R

Defines functions st_clone names.stobject print.stobject tab_edit st_caption st_edit st_filter st_mutate st_select st_drop st_it st_bold st_units st_args st_sizes st_hline st_clear_grouped st_clear_reps st_sumrow st_blank st_rename st_span_split st_span st_space st_files st_right st_left st_center st_align st_notes_glo st_noteconf st_notes_detach st_notes_sub st_notes_rm st_notes_str st_notes_app st_notes st_panel st_make is.ptobject is.stobject st_data st_new.pmtable st_new.data.frame st_new stop_if_ptobject check_st

Documented in names.stobject print.stobject st_align st_args st_blank st_bold st_caption st_center st_clear_grouped st_clear_reps st_clone st_data st_drop st_edit st_files st_filter st_hline st_it st_left st_make st_mutate st_new st_new.data.frame st_new.pmtable st_noteconf st_notes st_notes_app st_notes_detach st_notes_glo st_notes_rm st_notes_str st_notes_sub st_panel st_rename st_right st_select st_sizes st_space st_span st_span_split st_sumrow st_units tab_edit

check_st <- function(x) {
  assert_that(
    is.stobject(x),
    msg = "the first argument (x) must be an st object"
  )
}

stop_if_ptobject <- function(x) {
  if(is.ptobject(x)) {
    caller <- as.character(sys.call(-1))[1]
    stop(
      glue("the {caller}() function cannot be used to operate on pmtble objects."),
      call. = FALSE
    )
  }
}

st_arg_names <- c(
  "data", "panel", "notes",
  "align", "r_file", "output_file", "output_dir",
  "span", "span_split", "cols_rename", "cols_blank",
  "sumrows", "note_config", "clear_reps", "clear_grouped_reps",
  "hline_at", "hline_from", "sizes", "units", "drop",
  "lt_cap_text", "lt_cap_macro", "lt_cap_label", "lt_cap_short", "lt_continue",
  "caption", "path.type",
  "args"
)

#' Create an st object
#'
#' The st object will collect various configuration settings and pass those
#' to [stable()] when the object is passed to [st_make()].
#'
#' @details
#' Methods are included for `data.frame` and `pmtable`, an object that comes
#' from one of the data summary functions (e.g. [pt_cont_wide()], or
#' [pt_cat_long()] or [pt_demographics()]).
#'
#' If using the data frame method, the user should filter or subset so that
#' the data (`x`) contains exactly the rows (and columns) to be processed;
#' pmtables will not add or remove rows prior to processing `x`.
#'
#' @param x either a data frame or an object of class `pmtable`; see details.
#' @param ... additional arguments which will eventually get passed to the
#' table render function (e.g. [stable()] or [stable_long()]).
#'
#' @return
#' And object with class `stobject` which can get piped to other functions. The
#' `pmtable` method returns an object that also has class `ptobject`.
#'
#' @examples
#' ob <- st_new(ptdata())
#' ob <- st_data(ptdata())
#'
#' ob <- st_new(pt_data_inventory(pmt_obs))
#'
#' @export
st_new <- function(x, ...) UseMethod("st_new")
#' @rdname st_new
#' @export
st_new.data.frame <- function(x, ...) {
  e <- new.env()
  e$data <- x
  e$args <- list(...)
  structure(e, class = c("stobject", "environment"), argnames = st_arg_names)
}
#' @rdname st_new
#' @export
st_new.pmtable <- function(x, ...) {
  valid_arg_names <- c(
    "data", "panel", "cols_rename", "align", "notes", "cols_extra",
    "cols_blank", "span", "span_split", "units", "bold_cols", "sumrows"
  )
  incoming <- names(x)
  if(!all(incoming %in% valid_arg_names)) {
    stop("internal error: invalid item in pmtable object.")
  }
  ans <- st_new(x$data)
  foo <- lapply(incoming, function(slot) {
    assign(slot, value = x[[slot]], envir = ans)
  })
  structure(
    ans,
    class = c("stobject", "ptobject", "environment"),
    argnames = st_arg_names
  )
}

#' @rdname st_new
#' @export
st_data <- function(x,...) st_new(x = x,...)

is.stobject <- function(x) inherits(x, "stobject")
is.ptobject <- function(x) inherits(x, "ptobject")

#' Convert st object to table output
#'
#' @param x an stobject
#' @param ... other arguments passed to [stable()]
#' @param .preview if `TRUE`, pass result to [st_preview()]
#' @param .cat if `TRUE`, pass result to [st_wrap()]; use this option to write
#' table text to standard output while creating Rmarkdown documents with pdf
#' output
#' @param long render as longtable
#'
#' @return The latex code for the table.
#'
#' @examples
#'
#' library(dplyr)
#'
#' ob <- st_new(ptdata())
#'
#' st_make(ob)
#'
#' st_make(ob, .cat = TRUE)
#'
#' \dontrun{
#' st_make(ob, .preview = TRUE)
#' }
#'
#' @export
st_make <- function(x, ..., .preview = FALSE, .cat = FALSE, long = FALSE) {
  check_st(x)
  long <- isTRUE(long)
  # accumulated by the functions
  args <- as.list(x)
  # misc args passed in via st_args
  if(is.list(x$args)) {
    args <- combine_list(args, x$args)
  }
  dots <- list(...)
  if(length(dots) > 0) {
    args <- combine_list(args,dots)
  }
  if(long) {
    ans <- do.call(stable_long, args)
    if(isTRUE(.preview)) {
      warning("cannot preview a long table; use st2report() instead", call.=FALSE)
      .preview <- FALSE
    }
  } else {
    ans <- do.call(stable, args)
  }
  if(.preview) { # nocov start
    .null <- st_preview(ans)
    return(invisible(ans))
  }
  if(.cat) {
    .null <- pt_wrap(ans,stdout())
    return(invisible(ans))
  } # nocov end

  return(ans)
}

#' Add panel information to st object
#'
#' See the `panel` argument to [stable()]. This function cannot be used to
#' operate on pmtable objects.
#'
#' @param x an stobject
#' @param ... passed to [rowpanel()]
#'
#' @examples
#' library(dplyr)
#'
#' ob <- st_new(ptdata())
#'
#' ob %>% st_panel("STUDY") %>% st_make()
#'
#' @export
st_panel <- function(x, ...) {
  check_st(x)
  stop_if_ptobject(x)
  panel <- rowpanel(...)
  assert_that(is.rowpanel(panel))
  x$panel <- panel
  x
}

#' Add note information to st object
#'
#' See the `notes` and `note_config` arguments passed to [stable()] and then to
#' [tab_notes()]. The function can be called multiple times and can accumulate
#' `notes` data in various ways. Use [st_notes_app()] as a short cut to append
#' a note to the previous line and [st_notes_str()] to convert all existing
#' notes into a single string.
#'
#' @param x an stobject.
#' @param ... character; one or more table notes.
#' @param esc passed to [tab_escape()]; use `NULL` to bypass escaping the notes.
#' @param config named list of arguments for [noteconf()].
#' @param collapse a character string to separate notes which are pasted
#' together when flattening or appending; this should usually end in a single
#' space (see default).
#' @param append logical; if `TRUE`, then incoming notes are appended to the
#' previous, single note in the notes list. When `...` contains multiple
#' notes, then the notes are pasted together first.
#' @param to_string logical; if `TRUE`, then all notes are collapsed to a single
#' string.
#'
#' @examples
#' library(dplyr)
#'
#' ob <- st_new(ptdata())
#'
#' ob %>% st_notes("ALB: albumin (g/dL)") %>% stable()
#'
#' @seealso
#' [st_notes_detach()], [st_notes_rm()], [st_notes_str()], [st_notes_app()]
#'
#' @export
st_notes <- function(x, ..., esc = NULL, config = NULL, collapse = "; ",
                     append = FALSE, to_string = FALSE) {
  check_st(x)
  notes <- unlist(list(...))
  if(!is.null(notes)) {
    assert_that(is.character(notes))
    if(is.character(esc)) {
      notes <- tab_escape(notes, esc = esc)
    }
    append <- isTRUE(append)    && length(x$notes) > 0
    tostr  <- isTRUE(to_string) && length(notes)   > 1
    if(tostr || append) {
      notes <- paste0(notes, collapse = collapse)
    }
    if(isTRUE(append)) {
      l <- length(x$notes)
      x$notes[l] <- paste0(c(x$notes[l], notes), collapse = collapse)
    } else {
      x$notes <- c(x$notes, notes)
    }
  }
  if(is.list(config)) {
    x$note_config <- do.call(noteconf, config)
  }
  x
}

#' Append a note to the previous position of a note vector
#'
#' @details
#' Note that the call to [st_notes()] will force in the argument
#' `append = TRUE`.
#'
#' @param ... passed to [st_notes()].
#'
#' @export
st_notes_app <- function(...) {
  st_notes(..., append = TRUE)
}

#' Convert existing note vector into a single string
#'
#' @inheritParams st_notes
#'
#' @return
#' An updated object with class `stobject`, which can be piped to other
#' functions.
#'
#' @export
st_notes_str <- function(x, collapse = "; ") {
  check_st(x)
  if(length(x$notes) == 0) return(x)
  x$notes <- paste0(x$notes, collapse = collapse)
  x
}

#' Remove notes from the table
#'
#' The can be useful when manipulating an object from one of the pmtable
#' functions (e.g. [pt_cont_long()] or [pt_demographics()], when notes are
#' automatically added to the table.
#'
#' @inheritParams st_notes
#'
#' @return
#' An updated object with class `stobject`, which can be piped to other
#' functions.
#'
#' @export
st_notes_rm <- function(x) {
  rm("notes", envir = x)
  x
}

#' Edit lines in table notes
#'
#' This function allows the replacement of _an entire line_ in table notes.
#' The line which is replaced is matched by a regular expression or identified
#' directly with the integer position  in the notes vector to replace.
#'
#' @details
#' A warning is generated if there are no notes already existing in `x`. A
#' warning is also generated if a regular expression fails to match any lines.
#' In case multiple lines are matched, only the first matching line is
#' substituted.
#'
#' @inheritParams st_notes
#' @param where a regular expression for finding a line in table notes to
#' replace; alternatively, this can be an integer specifying the line to
#' replace.
#' @param replacement the replacement text for line matching by `where`.
#' @param fixed passed to [grep()] when `where` is character.
#'
#' @return
#' An updated object with class `stobject`, which can be piped to other
#' functions.
#'
#' @export
st_notes_sub <- function(x, where, replacement, fixed = FALSE) {
  check_st(x)
  if(length(x$notes)==0) {
    warning("did not find any notes in the object; returning.")
    return(x)
  }
  assert_that(
    inherits(where, c("character", "numeric")),
    msg = "`where` must be either character or numeric."
  )
  assert_that(length(where)==1)
  if(is.character(where)) {
    m <- grep(where, x$notes, fixed = fixed)
    if(length(m)==0) {
      warning("did not find any matching notes; returning.")
      return(x)
    }
    where <- m[1]
  } else {
    where <- floor(where)
  }
  note_number <- where
  assert_that(note_number >= 0 && note_number <= length(x$notes))
  x$notes[note_number] <- replacement
  x
}

#' Detach table notes from the table
#'
#' Detached notes are rendered underneath the table, in a separate minipage
#' format. By default, there is an `hline` rendered between the table and the
#' notes. It is common to adjust the width of the minipage holding the notes
#' depending on the width of the table and the extent of the notes.
#'
#' @inheritParams st_notes
#' @param width passed to [noteconf()] via [st_noteconf()].
#' @param type passed to [noteconf()] via [st_noteconf()]; this argument should
#' not be changed if detached notes are desired.
#' @param ... other arguments passed to [noteconf()] via [st_noteconf()].
#'
#' @return
#' An updated object with class `stobject`, which can be piped to other
#' functions.
#'
#' @export
st_notes_detach <- function(x, width = 0.8, type = "minipage", ...) {
  check_st(x)
  st_noteconf(x, width = width, type = type, ...)
}

#' Add note config information to st object
#'
#' See the `note_config` argument passed to [stable()] and then to
#' [tab_notes()].
#'
#' @param x an stobject.
#' @param ... named arguments passed to [noteconf()].
#'
#' @examples
#' library(dplyr)
#'
#' ob <- st_new(ptdata())
#'
#' ob %>%
#'   st_notes("ALB: albumin (g/dL)") %>%
#'   st_noteconf(type = "minipage") %>%
#'   st_make()
#'
#' @export
st_noteconf <- function(x,...) {
  check_st(x)
  x$note_config <- noteconf(...)
  x
}

#' @rdname st_noteconf
#' @export
st_notes_conf <- st_noteconf

#' Add table notes based on acronyms from a tex glossary file
#'
#' @param x an stobject.
#' @param glossary a glossary object generated from [read_glossary()] or
#' [as_glossary()].
#' @param ... unquoted names matching those names in `glossary`.
#' @param sep character to separate name and value.
#' @param collapse a character used to collapse definitions into a
#' single string.
#' @param width if numeric, [st_notes_detach()] will be called with `width`
#' argument.
#' @param labels a character vector or comma-separates string of definition
#' labels to get appended to the end of names passed in `...`.
#'
#' @examples
#' library(dplyr)
#'
#' file <- system.file("glo", "glossary.tex", package = "pmtables")
#'
#' x <- read_glossary(file)
#'
#' st_new(stdata()) %>%
#'   st_notes_glo(x, WT, CRCL, SCR, width = 1) %>%
#'   stable()
#'
#' @seealso [glossary_notes()], [read_glossary()]
#' @export
st_notes_glo <- function(x, glossary, ..., sep = ": ", collapse = "; ",
                         labels = NULL, width = NULL) {
  if(!is.list(glossary) || !is_named(glossary)) {
    abort("`glossary` must be a named list.")
  }
  labels <- cvec_cs(labels)
  labels <- c(new_names(enquos(...)), labels)
  if(!length(labels)) labels <- names(glossary)
  notes <- build_glossary_notes(glossary, labels, sep, collapse)
  if(is.numeric(width)) {
    x <- st_notes_detach(x, width = width)
  }
  st_notes(x, notes)
}

#' Add column alignment information to st object
#'
#' See the `align` argument to [stable()]. This function may be called several
#' times to specify column alignment.
#'
#' @param x an stobject.
#' @param ... named arguments passed to [cols_align()].
#'
#' @return
#' An updated version of `x`.
#'
#' @examples
#' library(dplyr)
#'
#' ob <- st_new(ptdata())
#'
#' ob %>% st_align(.default = 'l') %>% stable()
#'
#' ob %>% st_center(N = 'l') %>% stable()
#'
#' # This is only to illustrate multiple calls
#' ob %>%
#'   st_center(N = 'l') %>%
#'   st_align(STUDY = col_ragged(2), WT = 'r') %>%
#'   st_right(N = 'c') %>%
#'   stable()
#'
#' @export
st_align <- function(x, ...) {
  check_st(x)
  if("align" %in% ls(x)) {
    x$align <- update(x$align, ...)
  } else {
    x$align <- cols_align(...)
  }
  x
}

#' @rdname st_align
#' @export
st_center <- function(x,...) {
  st_align(x, .default = "c", ...)
}

#' @rdname st_align
#' @export
st_left <- function(x,...) {
  st_align(x, .default = "l", ...)
}

#' @rdname st_align
#' @export
st_right <- function(x,...) {
  st_align(x, .default = "r", ...)
}

#' Add file name information to st object
#'
#' See the `r_file` and `output_file` arguments passed to [stable()] and then
#' to [tab_notes()].
#'
#' @inheritParams tab_notes
#'
#' @param x an stobject.
#' @param r set `r_file`, passed to [stable()].
#' @param output set `output_file`, the `.tex` file containing rendered
#' table code; this may be the base file name or the full path to the
#' file; passed to [stable()].
#' @param output_dir directory for `output_file`; this will be overridden with
#' a warning if `output_file` contains the full path to the file.
#' @param esc passed to [tab_escape()]; use `NULL` to bypass escaping.
#'
#' @examples
#' library(dplyr)
#'
#' ob <- st_new(ptdata())
#'
#' ob %>% st_files(r = "foo.R", output = "foo.tex") %>% stable()
#'
#' @export
st_files <- function(x, r = getOption("mrg.script", NULL), output = NULL,
                     output_dir = getOption("pmtables.dir"),
                     path.type = NULL, esc = NULL) {
  check_st(x)
  if(!missing(r)) {
    if(!is.null(esc)) r <- tab_escape(r, esc = esc)
    x$r_file <- r
  }
  if(!missing(output)) {
    if(!is.null(esc)) output <- tab_escape(output, esc = esc)
    x$output_file <- output
  }
  if(is.character(output_dir)) {
    x$output_dir <- output_dir
  }
  if(is.character(path.type)) {
    x$path.type <- path.type
  }
  x
}

#' Add row and column spacing information to st object
#'
#' See the `sizes` argument to [stable()] and the `row_space` and `col_space`
#' arguments to [tab_size()].
#'
#' @param x an stobject
#' @param row set `row_space`, passed to [stable()] and then to [tab_size()]
#' @param col set `col_space`, passed to [stable()] and then to [tab_size()]
#'
#' @examples
#' library(dplyr)
#'
#' ob <- st_new(ptdata())
#'
#' ob %>% st_space(row = 1) %>% st_make()
#'
#' @export
st_space <- function(x, row = 1.5, col = 5) {
  check_st(x)
  if(is.null(x$sizes)) {
    x$sizes <- tab_size(row = row, col = col)
  } else {
    x$sizes$row_space <- row
    x$sizes$col_space <- col
  }
  x
}

#' Add column spanner to st object
#'
#' See the `span` argument to [stable()]. This function can be called multiple
#' times and will accumulate `span` data.
#'
#' @param x an stobject
#' @param split if `TRUE`, then [st_span_split()] is called
#' @param ... passed to [colgroup()] or [st_span_split()] if `split` is `TRUE`
#'
#' @examples
#' library(dplyr)
#'
#' ob <- st_new(ptdata())
#'
#' ob %>% st_span("Covariates", WT:ALB) %>% st_make()
#'
#' @export
st_span <- function(x, ..., split = FALSE) {
  if(isTRUE(split)) {
    return(st_span_split(x, ..., split = split))
  }
  check_st(x)
  span <- colgroup(..., split = split)
  if(is.null(x$span)) {
    x$span <- list(span)
    return(x)
  }
  if(is.list(x$span)) {
    x$span <- c(x$span, list(span))
    return(x)
  }
  x
}

#' Add column split spanner information to st object
#'
#' See the `span_split` argument passed to [stable()] and then [tab_spanners()].
#'
#' @param x an stobject
#' @param split passed to [colsplit()], if `split` is `FALSE`, then
#' an error is generated
#' @param ... passed to [colsplit()]
#'
#' @details
#' There can only be one `span_split` per table; if `st_span_split` is
#' called more than once in a pipeline, a warning will be issued on every
#' call after the first one and only the latest `span_split` data will be
#' retained in the table.
#'
#' @examples
#' library(dplyr)
#'
#' file <- system.file("datasets", "with-dots.RDS", package = "pmtables")
#'
#' data <- readRDS(file)
#'
#' st_new(data) %>% st_span_split('.') %>% st_make()
#'
#' @export
st_span_split <- function(x, ..., split = TRUE) {
  assert_that(
    isTRUE(split),
    msg = "the `split` argument is FALSE; use `st_span()` instead."
  )
  check_st(x)
  stop_if_ptobject(x)
  span <- colsplit(..., split = split)
  if(!is.null(x$span_split)) {
    warning(
      "`span_split` is already set and will be replaced",
      call. = FALSE
    )
  }
  x$span_split <- span
  x
}

#' Add column rename information to st object
#'
#' See the `cols_rename` argument passed to [stable()] and then [tab_cols()].
#' This function can be called multiple times and will accumulate `cols_rename`
#' data.
#'
#' @param x an stobject
#' @param ... column rename items in `new-name = old-name` format; passed
#' to [stable()] as `cols_rename`
#' @param .list a named list of rename data with the format
#' `old-name = new-name`; this specification is similar passing items via
#' `...`, but note that rename specification is reversed. The intended use for
#' this argument is to utilize list output from the `yspec` package which takes
#' the form `column-name = short-name` (e.g. `WT = weight` for the `WT` column).
#'
#' @examples
#' library(dplyr)
#'
#' st_new(stdata()) %>% st_rename(weight = WT) %>% stable()
#'
#' st_new(stdata()) %>% st_rename(.list = list(WT = "weight")) %>% stable()
#'
#' @export
st_rename <- function(x, ..., .list = NULL) {
  check_st(x)
  if(!is.null(.list)) {
    # This is also checked in new_names, but asserting here too to avoid breakage
    assert_that(is_named(.list))
    .old <- names(.list)
    .new <- unlist(.list, use.names = FALSE)
    if(!any(.old %in% names(x[["data"]]))) {
      warning(
        "rename data was passed as `.list`, but zero columns were matched\n",
        "please check that the list was properly specified (?st_rename)",
        call.=FALSE
      )
    }
    .list <- setNames(.old, .new)
    l <- new_names(.list)
  } else {
    l <- new_names(enquos(...))
  }
  x$cols_rename <- c(x$cols_rename, l)
  x$cols_rename <- x$cols_rename[!duplicated(x$cols_rename)]
  x
}

#' Add column blank information to st object
#'
#' See the `cols_blank` argument passed to [stable()] and then [tab_cols()].
#' This function can be called multiple times and will accumulate `cols_blank`
#' data.
#'
#' @param x an stobject
#' @param ... quoted or unquoted column names to be passed to [stable()] as
#' `cols_blank`
#'
#' @export
st_blank <- function(x,...) {
  check_st(x)
  l <- new_names(enquos(...))
  x$cols_blank <- c(x$cols_blank, l)
  x
}

#' Add summary row information to st object
#'
#' See the `sumrows` argument to [stable()]. This function can be called
#' multiple times and will accumulate `sumrows` data.
#'
#' @inheritParams sumrow
#' @param x an stobject
#' @param pattern a regular expression to search in the data frame; when this
#' argument is provided, `rows` are calculated using [df_grep_rows()]
#' @param cols a character vector of column names to search
#' @param ... passed to [sumrow()]
#'
#' @details
#' Please take careful note of the argument order for [st_sumrow()] compared to
#' [sumrow()].
#'
#' @export
st_sumrow <- function(x, pattern = NULL, cols = names(x$data), rows = integer(0),
                      ...) {
  check_st(x)
  if(is.character(pattern)) {
    rows <- df_grep_rows(x$data, pattern, cols)
  }
  sumr <- sumrow(rows = rows, ...)
  if(is.list(x$sumrows)) {
    x$sumrows <- c(x$sumrows, list(sumr))
    return(x)
  }
  x$sumrows <- list(sumr)
  x
}

#' Add clear rep information to st object
#'
#' See the `clear_reps` argument passed to [stable()] and then to
#' [tab_clear_reps()].
#'
#' @param x an stobject
#' @param ... quoted or unquoted column names passed to [stable()] as
#' `clear_reps`
#' @param .now if `TRUE`, the data is immediately processed; otherwise, the
#' processing is done after the pipeline completes
#'
#' @export
st_clear_reps <- function(x, ..., .now = FALSE) {
  check_st(x)
  dots <- enquos(...)
  if(length(dots) > 0) {
    cols <- new_names(dots)
    if(isTRUE(.now)) {
      x$data <- tab_clear_reps(x$data, clear_reps = cols)
    } else {
      x$clear_reps <- cols
    }
  }
  x
}

#' @rdname st_clear_reps
#' @export
st_clear_grouped <- function(x, ...) {
  check_st(x)
  dots <- enquos(...)
  if(length(dots) > 0) {
    cols <- new_names(dots)
    x$clear_grouped_reps <- cols
  }
  x
}

#' Add hline information to st object
#'
#' See the `hline_at` and `hline_from` arguments passed to [stable()] and
#' then to [tab_hlines()],
#'
#' @param x and stobject
#' @param pattern a regular expression to find rows where an `hline` will be
#' placed; passed to [stringr::str_detect()]
#' @param cols data columns to scan using `pattern`
#' @param n number of `hlines` to add when a hit is found
#' @param at logical or integer locations for hline passed to [stable()] as
#' `hline_at`
#' @param from character column name used to divide a table; passed to
#' [stable()] as `hline_from`
#' @param nudge push an hline down or up in the table; only applies to
#' indices found from using either the `at` or `pattern` arguments
#'
#' @export
st_hline <- function(x, pattern = NULL, cols = names(x$data), n = 1,
                     at = NULL, from = NULL, nudge = 0) {
  check_st(x)
  if(!missing(at)) {
    if(is.logical(at)) at <- which(at)
    at <- at + nudge
    if(n > 1) at <- sort(rep(at, n))
    x$hline_at <- c(x$hline_at,at)
  }
  if(!missing(from)) {
    x$hline_from <- c(x$hline_from,from)
  }
  if(is.character(pattern)) {
    hline_re <- unique(find_hline_df(x$data, pattern, cols))
    hline_re <- hline_re[hline_re > 1] + nudge
    if(n > 1) {
      hline_re <- sort(rep(hline_re,n))
    }
    x$hline_at <- c(x$hline_at, hline_re)
  }
  x
}

#' Add table size information to st object
#'
#' See the `sizes` argument to [stable()], passed to [tab_size()].
#'
#' @param x an stobject
#' @param ... passed to [tab_size()]
#'
#' @export
st_sizes <- function(x,...) {
  check_st(x)
  x$sizes <- tab_size(...)
  x
}

#' Add other arguments to st object
#'
#' The arguments entered here are passed to [stable()] or [stable_long()].
#'
#' @param x an stobject
#' @param ... named arguments to be passed to [stable()]
#'
#' @export
st_args <- function(x,...) {
  check_st(x)
  args <- list(...)
  assert_that(
    is_named(args),
    msg = "arguments passed to st_args must be named"
  )
  if(length(args) > 0) {
    x$args <- combine_list(x$args, args)
  }
  x
}

#' Add unit information to st object
#'
#' See the `units` argument to [stable()]. This function cannot be used to
#' work on pmtable objects.
#'
#' @details
#' Units can be passed either as
#' `name=value` pairs or as a named list with [st_args()]. Units can
#' alternatively be passed as an argument to [stable()] as a pre-formed, named
#' list using [st_args()].  Passing as an argument this way will overwrite units
#' specified with [st_units()]. It is recommended to use either [st_units()] or
#' [st_args()] but not both.
#'
#' @param x an stobject
#' @param ... named items of the form `COL = unit` or a named list
#' of units
#' @param parens if `TRUE`, parens will be added to any unit whose first
#' character is not `(`
#'
#' @export
st_units <- function(x, ..., parens = TRUE) {
  check_st(x)
  stop_if_ptobject(x)
  units <- flatten(list(...))
  units <- map(units, trimws)
  if(isTRUE(parens)) {
    u <- unlist(units, use.names = FALSE)
    w <- substr(u, 1, 1) != "(" & nchar(u) > 0
    units[w] <- paste0("(", units[w], ")")
  }
  if(is.list(x$units)) {
    x$units <- combine_list(x$units, units)
  } else {
    x$units <- units
  }
  x
}

#' Render table data in bold or italic font
#'
#' These functions modify the input data frame prior to passing it to
#' [stable()] or [stable_long()].
#'
#' @param x an stobject
#' @param cols columns to make bold
#' @param pattern passed to [tex_bold()] or [tex_it()]
#'
#' @export
st_bold <- function(x, cols, pattern = "*") {
  check_st(x)
  cols <- new_names(cols)
  assert_that(all(cols %in% names(x$data)))
  for(col in cols) {
    x$data[[col]] <- tex_bold(as.character(x$data[[col]]), pattern = pattern)
  }
  x
}

#' @rdname st_bold
#' @export
st_it <- function(x, cols, pattern = "*") {
  check_st(x)
  cols <- new_names(cols)
  assert_that(all(cols %in% names(x$data)))
  for(col in cols) {
    x$data[[col]] <- tex_it(as.character(x$data[[col]]), pattern = pattern)
  }
  x
}

#' Drop data columns
#'
#' See the `drop` argument to [stable()].
#'
#' @param x an stobject
#' @param ... column names to drop
#'
#' @export
st_drop <- function(x, ...) {
  check_st(x)
  dots <- new_names(enquos(...))
  x$drop <- c(x$drop, dots)
  x
}

#' Filter, select, or mutate data
#'
#' These functions modify the input data frame prior to passing it to
#' [stable()] or [stable_long()].
#'
#' @param x an stobject.
#' @param ... passed to [dplyr::select()], [dplyr::mutate()], or
#' [dplyr::filter()].
#'
#' @details
#' - `st_select` calls `dplyr::select` on the data
#' - `st_mutate` calls `dplyr::mutate` on the data
#' - `st_filter` calls `dplyr::filter` on the data
#'
#' @examples
#' tab <- pt_data_inventory(pmt_obs, by = "FORM")
#' obj <- st_new(tab)
#' st_filter(obj, FORM != "troche")
#' st_select(obj, -contains("BQL"))
#' st_mutate(obj, FORM = ifelse(FORM=="tablet", "ODT", FORM))
#'
#' @export
st_select <- function(x, ...) {
  check_st(x)
  x$data <- select(x$data, ...)
  x
}

#' @rdname st_select
#' @export
st_mutate <- function(x, ...) {
  check_st(x)
  x$data <- mutate(x$data, ...)
  x
}

#' @rdname st_select
#' @export
st_filter <- function(x, ...) {
  check_st(x)
  x$data <- filter(x$data, ...)
  x
}

#' Edit table contents
#'
#' These functions modify the input data frame prior to passing to [stable()]
#' or [stable_long()].
#'
#' @param x an stobject
#' @param ... arguments passed to [tab_edit()]
#'
#' @export
st_edit <- function(x, ...) {
  check_st(x)
  x$data <- tab_edit(x$data, ...)
  x
}

#' Set table caption
#'
#' @param x an stobject.
#' @param text caption text to be parsed.
#' @param ... additional arguments passed to [as.caption()]; see details.
#'
#' @details
#' A short title for the table can be specified in one of two ways. First, the
#' short title can be specified by including it in brackets (`[]`) as the
#' first characters in `text`. Alternatively, short title text can get passed
#' separately into `st_caption()` under the argument `short`, which is
#' is eventually handled by [as.caption()].
#'
#' By default, [as.caption()] will repeat the short title at the start of the
#' main caption text. The user can pass `repeat_short = FALSE` into
#' `st_caption()` (and eventually to [as.caption()]) to suppress this behavior.
#'
#'
#' @examples
#' tab <- st_new(stdata())
#' tab <- st_caption(tab, "[Full covariate model estimates]. Run number 101.")
#' tab$caption
#'
#' text <- stable(tab)
#' text
#'
#' @export
st_caption <- function(x, text, ...) {
  check_st(x)
  text <- paste0(text, collapse = " ")
  x$caption <- as.caption(text, ...)
  x
}

#' @param data a data frame
#' @param pattern passed to [stringr::str_replace()]
#' @param replacement passed to [stringr::str_replace()]
#' @param cols data columns to check for `pattern`
#' @rdname st_edit
#' @export
tab_edit <- function(data, pattern, replacement, cols = names(data)) {
  if(!missing(cols)) {
    cols <- cols[cols %in% names(data)]
  }
  if(length(cols)==0) return(data)
  for(col in cols) {
    data[[col]] <- str_replace(data[[col]],pattern,replacement)
  }
  data
}

#' Methods for stobject
#'
#' @param x an stobject
#' @param ... other arguments
#' @export
print.stobject <- function(x, ...) {
  print(as_tibble(x$data))
}

#' @rdname print.stobject
#' @export
names.stobject <- function(x) names(x$data)


#' Clone an stobject
#'
#' @param x an stobject object.
#'
#' @return
#' A copy (`y`) of `x` such that `y` can be modified without modifying `x`.
#'
#' @examples
#' x <- st_new(stdata())
#' y <- st_clone(x)
#'
#' y$data$STUDY <- NULL
#' x$data
#' y$data
#'
#' # Get back to a regular environment
#' class(x) <- "environment"
#' class(y) <- "environment"
#'
#' x
#' y
#'
#' @export
st_clone <- function(x) {
  if(!inherits(x, "stobject")) {
    stop("Can only clone stobjects.")
  }
  y <- env_clone(x)
  attributes(y) <- attributes(x)
  y
}
metrumresearchgroup/pmtables documentation built on Oct. 27, 2024, 5:16 p.m.