R/utilities.R

Defines functions get_summary_codomains get_math_codomains determine_mathish_codomain check_summary_args maybe_get_data_summary_math make_maybe_get_summary stop_invalid_math stype_abort is_truth check_number_positive check_internal_name swap_function is_auto_computed decide_auto_compute check_internal_names print_footer print_missing print_numeric_summary print_context stype_str make_stype_ptype2 make_stype_restorator make_stype_constructor new_stype_vctr

Documented in check_internal_name check_internal_names check_number_positive check_summary_args decide_auto_compute determine_mathish_codomain get_math_codomains get_summary_codomains is_auto_computed make_maybe_get_summary make_stype_constructor make_stype_ptype2 make_stype_restorator maybe_get_data_summary_math new_stype_vctr stop_invalid_math stype_abort stype_str swap_function

#' The internal builder of (basic) stype vctrs
#' 
#' Used instead of `vctrs::new_vctr` in each builder in order to keep the 
#' attributes consistent across stype vctrs.
#' 
#' @param x the vector data
#' @param .internal_name `internal_name` attribute 
#' @param .data_summary [`data_summary`] attribute
#' @param .context [`context`] attribute
#' @param .extra_descriptor `extra_descriptors` attribute a `list` of descriptor
#'  functions
#' @param .class passed to the `class` argument of (e.g.) the `new_fun`
#' @param new_fun the `vctrs` `new_*` function to use. Defaults to 
#'    [`vctrs::new_vctr()`]. The first argument must the "data" argument called 
#'    ".data" in new_vctr and "x" in new_factor and "fields" in new_rcrd.
#' @keywords internal
#' @importFrom vctrs new_vctr
new_stype_vctr <- function(x, .internal_name, .data_summary, .context,
                           .extra_descriptors, .auto_compute_summary,
                           .class,
                           new_fun = vctrs::new_vctr,
                           ...){
  
  check_internal_name(.internal_name)
  
  new_fun(
    x, 
    ... ,
    # Attributes available to be set by developer
    internal_name = .internal_name,
    data_summary  = .data_summary, 
    context       = .context, 
    extra_descriptors = .extra_descriptors,
    auto_compute_summary = .auto_compute_summary,
    
    # Automatic attributes
    # stype version is kept in order to do compatibility checks in the future
    stype_version = utils::packageVersion(pkg = "stype"),
    
    # Class
    class = .class)
}

#' Create a constructor function for stype vectors
#' 
#' @param typeFUN the \code{new_} function that creates vdctors of the type
#' @param ptypeFUN the prototype of the vector
#' @param castFUN optionally cast the data by \code{\link[vctrs]{vec_cast}}
#' @param dataFUN a function to apply to the data before \code{describe}ing the
#'     the data. Defaults to \code{identity}.
#' @keywords internal
make_stype_constructor <- function(typeFUN, 
                                   ptypeFUN, 
                                   castFUN = ptypeFUN, 
                                   dataFUN = identity){
  function(x = ptypeFUN(), 
           internal_name = "", 
           context, 
           auto_compute_summary = auto_compute_default,
           extra_descriptors = list()){

      x <- vctrs::vec_cast(x, castFUN())
      vctrs::vec_assert(x, ptype = ptypeFUN())
      
      assertthat::assert_that(
        is_truth(auto_compute_summary),
        msg = "auto_compute_summary must be TRUE or FALSE."
      )
      
      dsum <- 
        `if`(
          auto_compute_summary,
            describe( dataFUN(x), .descriptors = extra_descriptors ),
            data_summary() )
      
      if(missing(context)){
        context <- methods::new("context")
      }

      typeFUN(
        x,
        .internal_name = check_internal_name(internal_name),
        .data_summary = dsum,
        .context = context,
        .auto_compute_summary = auto_compute_summary,
        .extra_descriptors = extra_descriptors
      )
  }
}

#' Create a vec_restore function for basic stype types
#' @inheritParams make_stype_constructor
#' @keywords internal
make_stype_restorator <- function(typeFUN){
  function(x, to, ..., n = NULL) {
    
    # Maintain meta-info
    iname   <- attr(to, "internal_name")
    edesc   <- attr(to, "extra_descriptors")
    context <- get_context(to)
    # Update data summary
    auto    <- attr(to, "auto_compute_summary")
    dsum <- 
      `if`(
        auto,
        describe( vctrs::vec_data(x), .descriptors = edesc ),
        data_summary() )


    typeFUN(
      x, 
      .internal_name = iname, 
      .data_summary = dsum, 
      .context = context,
      .extra_descriptors = edesc,
      .auto_compute_summary = auto)
  }
}

#' Create a stype<->stype ptype2 function
#' @param constructorFUN the constructor function to use.
#' @keywords internal
make_stype_ptype2 <- function(constructorFUN){
  function(x, y, ...){
    compare_contexts(x, y)
    check_internal_names(x, y)
    
    constructorFUN(internal_name = get_internal_name(x), 
                   context = get_context(x),
                   auto_compute_summary = decide_auto_compute(x, y))
  }

}
#' String Representation of the Statistical Type
#'
#' Provides a short phrase identifying the statistical type.
#'
#' @param x A stype object.
#' @return A string.
#' @export
stype_str <- function(x) {
  assertthat::assert_that(
    is(x, "stype"),
    msg = "not a stype object"
  )
  vctrs::vec_ptype_full(x)
}


#' @importFrom crayon bold red combine_styles
boldmag <- crayon::combine_styles(crayon::bold, crayon::magenta)

#' A printer for stype contexts
#' @noRd
#' @param x a \code{stype} variable
print_context <- function(x){
  ctxt <- get_context(x)
  if(is_empty(ctxt)){ return("")}
  prps <- methods::slot(get_purpose(ctxt), "study_role") 
  prps <- if(length(prps) == 0) "<undefined>" else prps
  sprintf("Purpose: %s\n", paste(prps, collapse = ", "))
}

#' A printer for basic stype data_summaries
#' @noRd
#' @param x a numeric value
#' @param label the label to use on the LHS of "="
#' @param format the format to use for the value
print_numeric_summary <- function(x, label, format = '%.3f'){
  fmt <- paste0("%s = ", format)
  sprintf(fmt, label, x)
}

#' A printer for the has_missing item
#' @noRd
#' @param x a \code{stype} variable
print_missing <- function(x){
  if (get_data_summary(x, "has_missing")){
    boldmag(print_numeric_summary(get_data_summary(x, "n_missing"), "Missing", '%d'))
  } else {
    ""
  }
}

#' A printer for the footer of a stype
#' @noRd
#' @param x a \code{stype} variable
#' @param stats a named \code{character} vector whose names identify which items from the 
#' \code{data_summary} to include in the footer and the elements are the labels
#' @importFrom purrr imap 
#' @importFrom crayon "%+%"
print_footer <- function(x, stats){
  
  # if the data summary is empty, there's no footer to print.
  if (is_not_computed(attr(x, "data_summary"))){
    return(x)
  }
  
  # If the length of the vector is 0, there's no footer to print
  if (length(x) == 0) { return(x) }
  
  assertthat::assert_that(
    length(names(stats)) == length(stats),
    msg = "stats must be a named vector."
  )
  
  sstats <- paste0(purrr::imap(
     .x = stats,
     .f = ~ {
       .x$printer(get_data_summary(x, .y), .x$label)
     }),
     collapse = "; ")
  
  cxtp <- print_context(x)
  miss <- print_missing(x)
  
  cat(sstats %+% {if (miss != "") "; " else ""} %+% miss %+% "\n" %+%
      cxtp,
      sep = "")
}

#' Check that internal names are equal
#' @param x a stype vector
#' @param y another stype vector
#' @keywords internal
check_internal_names <- function(x, y){
  assertthat::assert_that(
    get_internal_name(x) == get_internal_name(y),
    msg = "internal names of x and y must match"
  )
}

#' In the case that both x and y not equal to the default,
#' return not the default;
#' otherwise, return the default
#' @keywords internal
decide_auto_compute <- function(x, y){
  x <- attr(x, "auto_compute_summary")
  y <- attr(y, "auto_compute_summary")
  if (x != auto_compute_default && y != auto_compute_default ) !auto_compute_default
  else  auto_compute_default
}

#' Is this data_summary to be auto computed?
#' @keywords internal
is_auto_computed <- function(x){
  attr(x, "auto_compute_summary")
}

#' Swap the called function for a different function
#' @param cl a call
#' @param fn the new function
#' @keywords internal
swap_function <- function(cl, fn){
  cl[[1]] <- fn
  cl
}

#' Check that a character vector has length 0 or 1
#' @param x what to check
#' @keywords internal
check_internal_name <- function(x){
  assertthat::assert_that(
    is.character(x) && length(x) <= 1,
    msg = "An internal name should be a single string"
  )
  x
}

#' Check whether a value is a positive scalar number
#' @param x what to check
#' @keywords internal
check_number_positive <- function(x){
  is.numeric(x) && (length(x) == 1L) && (! is.na(x)) && (x > 0)
}

#' Check if an object is either TRUE or FALSE
#' @param x what to check
#' @noRd
is_truth <- function(x) {
  (is.logical(x)
    && (length(x) == 1L)
    && (! is.na(x))
  )
}

#' Create custom \code{rlang::abort} Class for Stype Objects
#'
#' modeled after \code{vctrs:::stop_vctrs}
#' @param msg a string containing the error message.
#' @param class a character vector with error classes, if any, to add in
#' addition to "stype_err".
#' @param ... additional arguments passed to \code{abort}
#' @importFrom rlang abort
#' @keywords internal
stype_abort  <- function(msg = NULL, class = NULL, ...) {
  abort(msg, class = c(class, "stype_err"), ...)
}

#' Throw 'stype_invalid_math' Error
#'
#' Helper function used to throw an error with a standardized class for the case
#' where an unsupported math operation is called for a given stype type. This is
#' intended to be explicitly invoked by internal routines in situations where we
#' don't want the S3 dispatch to fall through to vctrs handling of math
#' operators, which amounts to invoking the operator in base with the raw data
#' as the input for types that are supported, and then restoring the appropriate
#' vctrs but not stype type (for types that are not supported by vctrs an error
#' is thrown immediately).
#' @param x stype vector
#' @param op the math operation
#' @param class optional class argument passed to \code{stype_abort}
#' @param ... additional arguments passed to \code{stype_abort}
#' @keywords internal
stop_invalid_math <- function(x, op, class = NULL, ...) {
  msg  <- c(sprintf("%s is not defined for stype class %s.", op, class(x)[1L]),
            i = "Try casting to the stype prototype with as_canonical.")
  stype_abort(msg, class = c(class, "stype_invalid_math"), ...)
}

#' Creates a function for getting a summary value
#' 
#' @param summary_name name of the value in the stype's \code{data_summary}
#' @param fn the function to use in the case that the summary is not already computed.
#' @keywords  internal
#' @seealso maybe_get_data_summary_math
make_maybe_get_summary <- function(summary_name, fn) {
  function(x, na.rm, ...){
    `if`(is_auto_computed(x) && na.rm,
         get_data_summary(x, summary_name),
         fn(vctrs::vec_data(x), na.rm = na.rm, ...))
  }
}

#' Gets a summary value already computed in a data_summary
#' 
#' Gets a value from a stype vector's data_summary in the case the summary is 
#' auto computed AND the `na.rm` argument is TRUE.
#' @param summary_name name of the value in the stype's \code{data_summary}
#' @inheritParams vctrs::vec_math_base
#' @keywords  internal
maybe_get_data_summary_math <- function(summary_name, .fn, .x, ...){
  dots <- list(...)
  `if`(is_auto_computed(.x) && dots$na.rm,
       get_data_summary(.x, summary_name),
       vec_math_base(.fn, .x, ...))
}

#' Checks that summary function ellipsis arguments only 
#' @keywords internal
check_summary_args <- function(...){
  dots <- list(...)

  assert_that(
    length(dots) == 1,
    msg = "stype does not support summary methods on more than 1 vector. Solution: Cast to the canonical type to relieve this itch."
  )
}

#' Get the codomain of math/summary function for stype vectors
#' @param fns character vector of vector names
#' @param x a dummy stype vector
#' @keywords internal
determine_mathish_codomain <- function(x, fns){
  
  purrr::map(
    fns,
    .f = ~ {
      attempt <- try(do.call(.x, args = list(x)), silent = TRUE)
      
      if (is(attempt, "try-error")) {
        "undefined"
      } else {
        vctrs::vec_ptype_full(attempt)
      }
      
    }
  )
}

#' Get Math codomains
#' @inheritParams determine_mathish_codomain
#' @importFrom methods Math Summary getGroupMembers
#' @keywords internal
get_math_codomains <- function(x){
  mt <- getGroupMembers(Math)
  determine_mathish_codomain(x, setNames(mt, mt))
}

#' Get Summary codomains
#' @inheritParams determine_mathish_codomain
#' @keywords internal
get_summary_codomains <- function(x){
  mt <- getGroupMembers(Summary)
  determine_mathish_codomain(x, setNames(mt, mt))
}
novisci/stype documentation built on July 28, 2022, 7:44 a.m.