pivot <-
function(data, compute, variable, by=NULL, by_cols=NULL, filter=NULL,
         show_n=TRUE, na_by_show=TRUE, na_group_show=TRUE, na_remove=TRUE,
         out_names=NULL, sort=NULL, sort_var=NULL,  
         table_prop=c("none", "all", "row", "col"), table_long=NULL,
         factors=TRUE, q_num=4, digits_d=NULL, quiet=getOption("quiet")) {

  # -----------------------------------------------------------
  # ---- preliminaries

  table_prop <- match.arg(table_prop)

  by.miss <- missing(by)
  if (missing(variable)) {  # variable not present as a parameter value
    by.expr <- substitute(by)

    if (is.call(by.expr) && identical(by.expr[[1]], as.name("c"))) { # by=c(...)
      by.vars <- as.list(by.expr)[-1L]
      if (length(by.vars) == 1) {  # c(X) is identical to bare X
        table_long <- FALSE
        variable <- deparse(by.vars[[1]])
        by <- NULL
      } else {
        variable <- deparse(by.vars[[1]])
        by <- as.call(c(as.name("c"), by.vars[-1]))
        if (is.null(table_long)) table_long <- TRUE
      }
    }
    else {  # by is a single name
      table_long <- FALSE
      variable <- deparse(by.expr)
      by <- NULL
    }
    by.miss <- is.null(by)
  }

  else {  # variable is present as a parameter value
    var.nm <- deparse(substitute(variable))
    if (substr(var.nm, 1,2) != "c("  &&  !grepl(":", var.nm)) {  # only do 1 var
      if (!is.numeric(data[[var.nm]])) {
        stop("The variable to aggregate (pivot) must now be numeric.\n\n",
             "All categorical variables such as ", var.nm, " now should be\n",
             "  specified as part of parameter by: by=", var.nm,"\n",
             "If there is no numeric variable to aggregate, then ignore.\n\n")
      }
    }
  }

  if (is.null(table_long)) table_long <- TRUE

  # define functions
  # length(x) is count_n(x) + count_NA(x)
  count_n <- function(x) sum(!is.na(x))
  count_NA <- function(x) sum(is.na(x))
 
  # if a tibble, convert to data frame
  if (any(grepl("tbl", class(data), fixed=TRUE)))
    data <- data.frame(data)

  # subset any specified rows of input data frame
  if (!missing(filter)) {  # subset rows
    r <- eval(substitute(filter), envir=data, enclos=parent.frame())
    r <- r & !is.na(r)  # set missing for a row to FALSE
    if (!any(r)) {
      cat("\n"); stop(call.=FALSE, "\n","------\n",
        "No rows of data with the specified value of\n",
        "filter = ", deparse(substitute(filter)), "\n\n")
    }
    data <- data[r,,drop=FALSE]
  }

  # -----------------------------------------------------------
  # ---- identify the compute functions, variables, and by variables

  # -----------------
  # compute functions

  # value of compute must be a recognized object, such as a function
  # cmpt.vc <- eval(parse(text=nm.cmp))  # access functions from their names
  raw.cmp <- deparse(substitute(compute))  # recognized as class function
  n.cmp <- length(compute)  # n of compute functions, not including quantile

  if (n.cmp == 1)
    nm.cmpt <- raw.cmp
  else {
    raw.cmp <- substr(raw.cmp, 3, nchar(raw.cmp)-1)  # remove "c(", ")"
    nm.cmpt <- unlist(strsplit(raw.cmp, ",", fixed=TRUE))  # get each function
    nm.cmpt <- gsub(" ", "", nm.cmpt, fixed=TRUE)  # remove spaces
  }

  qflag <- FALSE
  if ("quantile" %in% nm.cmpt) {  # remove and process quantile separately
    qflag <- TRUE
    n.cmp <- n.cmp - 1
    if (n.cmp > 0) {
      ipos <- which(nm.cmpt %in% "quantile")
      nm.cmpt <- nm.cmpt[-ipos]  # remove from name list
      compute <- compute[-ipos]  # remove from function list
    }
  }

  tflag <- FALSE
  to_tabu <- FALSE
  if ("table" %in% nm.cmpt) {  # remove and process table separately
    if (!table_long) {
      tflag <- TRUE
      n.cmp <- n.cmp - 1
      if (n.cmp > 0) {
        ipos <- which(nm.cmpt %in% "table")
        nm.cmpt <- nm.cmpt[-ipos]  # remove from name list
        compute <- compute[-ipos]  # remove from function list
      }
    }
    else {
      to_tabu <- TRUE
      nm.cmpt <- "tabulate"  # retain  tabulate  when table_long
      n.cmp <- 1
    }
  }

  # abbreviation dictionary for function names, first 13 require numeric var
  fun.vec <- c("sum", "mean", "mdn", "min", "max", "sd", "var", "IQR", "mad",
               "", "", "skew", "kurt", "tbl", "tbl")
  names(fun.vec) <- c("sum", "mean", "median", "min", "max",
                      "sd", "var", "IQR", "mad", "range", "quantile",
                      "skew", "kurtosis", "table", "tabulate")
  user_def <- logical(length=length(nm.cmpt)) 
  for (i in seq_along(user_def)) {
    if (!(nm.cmpt[i] %in% names(fun.vec))) {
      user_def[i] <- TRUE 
      if (!quiet) cat("User defined function (or typing error):", nm.cmpt[i],
                      "\n\n")
    }
  }

  # ---------
  # variables

  data.vars <- as.list(seq_along(data))
  names(data.vars) <- names(data)

  # get index of variable(s)
  # if variable is a variable that contains the name instead of the
  #  name directly, then ind.var.d is not the index but the name, 
  # which is then assessed on the data frame to identify its column, index
  ind.var.d <- eval(substitute(variable), envir=data.vars, parent.frame())
  if (!is.numeric(ind.var.d))  # ind.var.d is a var name, not a col index
    ind.var.d <- which(names(data) == ind.var.d)
  n.var <- length(ind.var.d)  # n of variables
  nm.var.d <- names(data)[ind.var.d]
  if (n.var == 1 && is.factor(data[,nm.var.d]))  # factors listed first can crash analysis
    data[,nm.var.d] <- as.character(data[,nm.var.d])
  if (is.null(out_names)) {
    out_names <- character(length=length(nm.cmpt) * length(nm.var.d))
    k <- 0
    for (i in seq_along(nm.cmpt)) {  # for each function
      for (j in seq_along(nm.var.d)) {  # for each var to aggregate
        k <- k + 1
          if (!user_def[i]) 
            out_names[k] <- paste(nm.var.d[j], "_", fun.vec[nm.cmpt[i]], sep="")
          else
            out_names[k] <- paste(nm.var.d[j], "_", nm.cmpt[i], sep="")
      }
    }
  }  # end is.null out_names

  # ------- 
  # by vars

  if (!by.miss || nm.cmpt[1] == "tabulate") {
    # get indices of by variable(s)
    x <- str2lang(deparse(substitute(by)))  # char string to type calls
    ind.r.by <- eval(substitute(x), envir=data.vars, parent.frame())
    if (!is.numeric(ind.r.by))
      ind.r.by <- which(names(data) == ind.r.by)
    nm.row.by <- names(data)[ind.r.by]

    if (by.miss)
      ind.r.by <- ind.var.d  # for tabulate, do not have variables

    # save original variable types of by vars because aggregate makes factors
    n.r.by <- length(ind.r.by)
    cls <- character(length=n.r.by)
    for (i in seq_len(n.r.by)) cls[i] <- class(data[,ind.r.by[i]])

    # get indices of by_cols variable(s)
    if (!missing(by_cols)) {
      x <- str2lang(deparse(substitute(by_cols)))  # char string to type calls
      ind.c.by <- eval(substitute(x), envir=data.vars, parent.frame())
      n.c.by <- length(ind.c.by)
      nm.col.by <- names(data)[ind.c.by]
    }
    else {
      ind.c.by <- NULL
      n.c.by <- 0
    }

    # collapse by variables to a single by for aggregation into long form
    n.by <- n.r.by + n.c.by
    ind.by <- c(ind.r.by, ind.c.by)

    # convert by variables to factors
    exc <- if (na_group_show) "" else NA
    if (n.by > 1) {
      data[, ind.by] <- lapply(data[, ind.by], factor, exclude=exc)
      by.vars <- as.list(data[, ind.by])
    }
    else {
      data[, ind.by] <- factor(data[, ind.by], exclude=exc)
      by.vars <- list(data[, ind.by])
      names(by.vars) <- names(data)[ind.r.by]
    }
    if (to_tabu)  # append the pivot variable for table() over the by vars
      ind.by <- c(ind.by, ind.var.d)
  }  # end by is specified or tabulate

  else {
    n.by <- 0
    n.c.by <- 0
  }

  # ------------
  # Not Possible

  if (n.c.by > 0) {
    if (n.cmp > 1)  {
      cat("\n"); stop(call.=FALSE, "\n","------\n",
        "Cannot have more than 1 compute function for making a table.\n\n")
    }
    if (n.var > 1)  {
      cat("\n"); stop(call.=FALSE, "\n","------\n",
        "Cannot have more than 1 variable to aggregate for making a table.\n\n")
    }
  }

  num.cmpt <- nm.cmpt %in% names(fun.vec)[seq_len(13)]
  are.num <- logical(length(ind.var.d))
  for (i in seq_along(ind.var.d))
    if (is.numeric(data[,ind.var.d[i]])) are.num[i] <- TRUE
  if (!all(are.num) && any(num.cmpt))  {
    cat("\n"); stop(call.=FALSE, "\n","------\n",
      "Specified computations: ", nm.cmpt, "\n",
      "Not all specified variables are numeric: ", nm.var.d, "\n\n")
  }
  if (tflag && n.cmp > 0 && by.miss)  {
    cat("\n"); stop(call.=FALSE, "\n","------\n",
      "Cannot have more than one statistic specified\n",
      "  when computing a frequency table over all the data.\n\n")
  }
  if (tflag && n.c.by>0)  {
    cat("\n"); stop(call.=FALSE, "\n","------\n",
      "Cannot specify a  by_cols  parameter value, ", nm.col.by, ",\n",
      "  when using table to compute a frequency table. The values\n",
      "  of the variable listed first goes across the columns.\n\n")
  }
  if (tflag && n.var > 1)  {
    cat("\n"); stop(call.=FALSE, "\n","------\n",
      "Cannot have more than one variable to aggregate\n",
      "  when computing a frequency table.\n\n")
  }
  if (n.c.by > 2)  {
    nms.c <- ""
    for (i in seq_len(n.c.by)) nms.c <- paste(nms.c, names(data)[ind.c.by[i]])
    cat("\n"); stop(call.=FALSE, "\n","------\n",
      "Specified column  by  variables: ", nms.c, "\n",
      "Only two column  by  variables permitted\n\n")
  }
  if (n.var > 1 && n.cmp > 1 && n.by > 0)  {
    cat("\n"); stop(call.=FALSE, "\n","------\n",
      "Cannot have multiple compute functions, multiple variables, \n",
      "  and do aggregation with a by variable. Choose two of the three\n\n")
  }
  if (!is.null(sort)) {
    if (!(sort %in% c( "+", "-"))) {
      cat("\n"); stop(call.=FALSE, "\n","------\n",
        "Parameter  sort  can only be \"+\" or \"-\"\n\n")
    }
  }


  # -------------------------------------------
  # ------- no aggregation, all the data ------

  # if table over all the data, then a single freq dist
  if (by.miss && nm.cmpt[1] == "table") {
    exc <- if (na_group_show) "ifany" else "no"
    tbl <- table(data[,ind.var.d], useNA=exc)
    a <- data.frame(tbl)
    names(a)[1] <- nm.var.d[1]
    names(a)[which(names(a) == "Freq")] <- "n"
    prp <- a$n / sum(tbl)
    a$Prop <- .decdig(prp, digits_d)
    return(a) 
  }

  if (by.miss && nm.cmpt[1] != "tabulate") {
    a <- NULL
    out <- double(length=n.cmp)
    if (n.cmp > 0) {
      # one of compute or variable, output same format as with by vars, 1-line

      if (!(n.cmp>1 && n.var>1)) {
        for (i in seq_len(n.var)) {
          n <- count_n(data[,nm.var.d[i]])
          na <- count_NA(data[,nm.var.d[i]])
          for (j in seq_len(n.cmp)) {
            txt <- paste("data[,", ind.var.d[i], "], na.rm=na_remove", sep="")
            f.call <- paste(nm.cmpt[j], "(", txt, ")", sep="")
            out[j] <- eval(parse(text=f.call))  # evaluate compute function
            a.l <- length(a)
            if (j==1) {  # first (possibly only) compute function
                a <- c(a, n, na, out[j])
                names(a)[a.l+1] <- paste(nm.var.d[i], "_n" ,sep="")
                names(a)[a.l+2] <- paste(nm.var.d[i], "_na", sep="")
                names(a)[a.l+3] <- out_names[i]
            }
            else {  # j > 1, so multiple compute functions
              a <- c(a, out[j])
              names(a)[a.l+1] <- out_names[j]
            }
          }  # end j
        }  # end i

        a <- as.matrix(a)  # turn vector a into a data frame
        a <- data.frame(t(a))
        row.names(a)[1] <- ""
      }  # end just one of compute or variables

      # do both multiple computes and multiple variables with tabular format
      else {  
        for (i in seq_len(n.var)) {
          n <- count_n(data[,nm.var.d[i]])
          na <- count_NA(data[,nm.var.d[i]])
          txt <- paste("data[,", ind.var.d[i], "], na.rm=na_remove", sep="")
          for (j in seq_len(n.cmp)) {
            f.call <- paste(nm.cmpt[j], "(", txt, ")", sep="")
            out[j] <- eval(parse(text=f.call))  # evaluate compute function
          }  # end j
            a <- data.frame(rbind(a, c(n, na, out)))  # a has multiple rows
        }  # end i

        names(a) <- c("n", "na", nm.cmpt)
        row.names(a) <- nm.var.d
      }
    }  # end n.cmp > 0

    if (qflag) {  # do quantiles here
      for (i in seq_len(n.var)) {
        n <- count_n(data[,nm.var.d[i]])
        na <- count_NA(data[,nm.var.d[i]])
        a2 <- quantile(data[,nm.var.d[i]], na.rm=na_remove,
                       probs=seq(0,1,1/q_num))
        a2 <- c(n, na, a2)
        a2 <- data.frame(t(a2))
        names(a2)[n.by+1] <- "_n"
        names(a2)[n.by+2] <- "_na"
        if (i == 1)
          aq <- a2
        else
          aq <- rbind(aq, a2)
      }
      for (i in seq_along(names(aq))) {   # delete ending . in var names
        nm <- names(aq)[i]
        if (substr(nm, nchar(nm), nchar(nm)) == ".")
          names(aq)[i] <- substr(nm, 1, nchar(nm)-1)
      }
      qc <- (n.by + 3):ncol(aq)  # quantile cols only, skip _n/_na names
      names(aq)[qc] <- gsub("X", "p", names(aq)[qc], fixed=TRUE)
      row.names(aq) <- nm.var.d

    # quant and non-quant done separately, here set final a
      if (n.cmp == 0)
        a <- aq
      else 
        a <- cbind(a, aq[,3:ncol(aq)])
    }

    if (!show_n) {
      not.n <- which(grepl("_n$", names(a)))
      not.n <- c(not.n, which(grepl("_na$", names(a))))
      a <- a[,-not.n]
    }
       
    # round, leaves true integers (1.00) displayed as an integer (1)
    for (i.col in seq_len(ncol(a)))
      a[,i.col] <- .decdig(a[,i.col], digits_d)

    # sort
    if (!is.null(sort)) {
      direction <- sort != "+"
      if (!is.null(sort_var)) {
        if (is.numeric(sort_var))
          s.col <- sort_var
        else {
          s.col <- which(names(a) == sort_var)
          if (length(s.col) == 0)
            stop(call.=FALSE, "\n------\n",
                 "sort_var \"", sort_var, "\" not found in column names.\n\n")
        }
      }
      else
        s.col <- ncol(a)  # s.col is the number of the sort column
      a <- a[order(a[[s.col]], decreasing=direction), ]
    }
    if (length(nm.var.d) == 1) {
      names(a) <- gsub(nm.var.d, "", names(a), fixed=TRUE)
      names(a) <- gsub("_", "", names(a), fixed=TRUE)
    }
    return(a)
  }  # no by specified and not tabulate


  # ------------------------------------------
  # --------------- tabulate -----------------

    if (nm.cmpt[1] == "tabulate") {
      use_na <- if (!na_by_show) "no" else "ifany"
      a <- table(data[, ind.by], useNA=use_na)
      if (n.c.by != 0) {
        if (table_prop == "all") a <- proportions(a)
        if (table_prop == "row") a <- proportions(a, 1)
        if (table_prop == "col") a <- proportions(a, 2)
        if (!is.null(digits_d)) a <- round(a, digits_d)
      }
      else
        a <- as.data.frame(a)
      names(a)[ncol(a)] <- "n"

      return(a)
    }

  # ------------------------------------------
  # --------------- aggregate ----------------

  # aggregate over a numerical variable (by vars become factors)
  # na.action is the NA parameter for aggregate, but does not work the
  #   same as na.rm, which is used here and is a parameter of mean(), etc.   
  if (n.cmp == 1) {
    if (qflag || tflag) 
      a <- aggregate(data[,ind.var.d], by=by.vars, drop=FALSE, FUN=compute[[1]],
                     na.rm=na_remove)  # compute for a cell even if missing
    else
      a <- aggregate(data[,ind.var.d], by=by.vars, drop=FALSE, FUN=compute,
                     na.rm=na_remove)  # compute for a cell even if missing
    if (n.var == 1)
      names(a)[ncol(a)] <- nm.var.d[1]
  }

  else if (n.cmp > 1) { 
    # do the 1st computation
    a <- aggregate(data[,ind.var.d], by=by.vars, drop=FALSE, FUN=compute[[1]],
                   na.rm=na_remove)  # compute for a cell even if a missing
    # do the remaining computations
    for (i in seq(2L, n.cmp)) {
      ac <- aggregate(data[,ind.var.d], by=by.vars, drop=FALSE,
               FUN=compute[[i]], na.rm=na_remove)
      names(ac)[ncol(ac)] <- paste(names(ac)[ncol(ac)], i, sep="")  # no dups
      a <- merge(a, ac, by=names(by.vars), sort=FALSE)
    }
  }

  n <- aggregate(data[,ind.var.d], by=by.vars, drop=FALSE, FUN=count_n)
  if (ncol(n) == 2) names(n)[2] <- nm.var.d[1]  # 1 var, aggregate names x
  n_NA <- aggregate(data[,ind.var.d], by=by.vars, drop=FALSE, FUN=count_NA)
  if (ncol(n_NA) == 2) names(n_NA)[2] <- nm.var.d[1]
  # merge columns to form complete aggregate data frame: a
  # pull by vars and aggregated var from original a
  if (n.cmp > 0) {
    a <- merge(a, n, by=names(by.vars), sort=FALSE)
    a <- merge(a, n_NA, by=names(by.vars), sort=FALSE)
  }
  else
    a <- merge(n, n_NA, by=names(by.vars), sort=FALSE)

  # if there is a non-quantile computation function
  if (n.cmp > 0) {
    if (length(ind.by) == 1)
      names(a)[1] <- names(data)[ind.r.by]

    # Dates never go to factors
    for (i in seq_len(n.r.by))
      if (cls[i] == "Date") a[,i] <- as.Date(as.character(a[,i]))

    if (!factors) {  # retain original variable type of by vars
      for (i in seq_len(n.r.by)) {
        if (cls[i] == "character") a[,i] <- as.character(a[,i])
        if (cls[i] == "integer") a[,i] <- as.integer(as.character(a[,i]))
        if (cls[i] == "logical") a[,i] <- as.logical(as.character(a[,i]))
      }
    }

    a2 <- a[, 1:n.by, drop=FALSE]  # temp, start by var(s) in the first col(s)

    if (n.cmp == 1) {
      col.val <- matrix(, nrow=0, ncol=3)
      for (i in seq_len(n.var)) {  # identify variables in a
        if (n.var == 1)  # by position
          col.val <- rbind(col.val, c(n.by+1,n.by+2,n.by+3))  # next 3 cols
        else  # by name, agg structure includes names
          col.val <- rbind(col.val, 
                           which(grepl(nm.var.d[i], names(a), fixed=TRUE)))
      }
      # add columns to temp a2 data frame
      for (i in seq_len(n.var)) {
         a2 <- cbind(a2, a[, col.val[i,2:3]])
         a2 <- cbind(a2, a[, col.val[i,1]])
      }
      a <- a2

      k <- n.by
      for (i in seq_len(n.var)) {  # for each variable, cover n, na, aggregated stat

        if (n.var==1) { 
          names(a)[k+1] <- "n"
          names(a)[k+2] <- "na"
        }
        else {
          names(a)[k+1] <- paste(nm.var.d[i], "_n", sep="")
          names(a)[k+2] <- paste(nm.var.d[i], "_na", sep="")
        }
          names(a)[k+3] <- nm.var.d[i]
        k <- k + 3
      }
    }  # end n.cmp==1

    else {  # n.cmp > 1 and n.var == 1
      a2 <- cbind(a2, a[, (ncol(a)-1):ncol(a)])
      a2 <- cbind(a2, a[, (n.by+1):(n.by+n.cmp)])
      a <- a2

      k <- n.by
      names(a)[k+1] <- "n"
      names(a)[k+2] <- "na"
      for (i in seq_len(n.cmp)) {
        names(a)[k+2+i] <- nm.var.d[1]  # all agg vars get the var name
      }
    }  # end n.cmp > 1 and n.var==1

      ind.var <- which(names(a) %in% names(data)[ind.var.d])  # variables in a
      names(a)[ind.var] <- out_names
  }  # end n.cmp>0

  # table computation
  if (tflag) {
    out_names <- NULL  # does not apply to a table where the names are levels

    # get _n and _na variables
    a1 <- aggregate(data[,ind.var.d], by=by.vars, drop=FALSE, FUN=count_n)
    a2 <- aggregate(data[,ind.var.d], by=by.vars, drop=FALSE, FUN=count_NA)
    amd1 <- merge(a1, a2, by=names(by.vars), sort=FALSE)
    names(amd1)[n.by+1] <- "_n"  # uniform _n/_na so 598 removal catches both
    names(amd1)[n.by+2] <- "_na"

    # calculate the freqs level by level of the aggregation variable
    lvl <- sort(unique(data[,ind.var.d]))
    for (i in seq_along(lvl)) {
      dtmp <- data[data[,ind.var.d]==lvl[i], , drop=FALSE]  # lvl[i] subset
      if (n.by > 1)
        by.v <- as.list(dtmp[,ind.by])
      else
        by.v <- list(dtmp[,ind.by])
      names(by.v) <- nm.row.by
      a2 <- aggregate(dtmp[,ind.var.d], by=by.v, drop=FALSE, FUN=count_n)
      names(a2)[ncol(a2)] <- lvl[i]
      for (j in seq_len(nrow(a2)))
        if (is.na(a2[j,ncol(a2)])) a2[j,ncol(a2)] <- 0
      if (i == 1)
        amd2 <- a2  # 1st level of by variable
      else
        amd2 <- merge(amd2, a2, by=names(by.vars), sort=FALSE)
    }
    amd <- merge(amd2, amd1, by=names(by.vars), sort=FALSE) 
    
    if (table_prop %in% c("all", "row", "col")) {
      nca <- ncol(amd)
      ll <- length(lvl)

      dp <- amd[, (n.by+1):(n.by+ll), drop=FALSE]  # pull out freq table
      tdb <- as.matrix(dp)

      if (table_prop == "all") tdb <- proportions(tdb)
      if (table_prop == "row") tdb <- proportions(tdb,1)
      if (table_prop == "col") tdb <- proportions(tdb,2)

      # if no row or col entries, proportions leaves a NaN, so make 0
      tdb <- replace(tdb, is.nan(tdb), 0)

      if (!quiet) {
        txt <- if (table_prop == "col") "column" else table_prop
        cat("\nProportions computed over", txt, "cells\n\n")
      }
      amd <- cbind(amd[, seq_len(n.by), drop=FALSE], tdb,
                   amd[, (n.by+ll+1):nca, drop=FALSE])  # replace freqs with props
    }  # end table_prop

    if (n.cmp == 0) {  # table is only computation: show bare n, na
      names(amd)[names(amd) == "_n"]  <- "n"
      names(amd)[names(amd) == "_na"] <- "na"
      a <- amd
    }
    else {  # more than just table already processed
      not.n <- which(grepl("_n$", names(amd)))
      not.n <- c(not.n, which(grepl("_na$", names(amd))))
      amd <- amd[,-not.n]  # remove what will be redundant columns
      a <- cbind(a,amd[(n.by+1):ncol(amd)])
    }
    if (is.factor(data[,ind.var.d]))
      names(a)[(n.by+3):ncol(a)] <- levels(data[,ind.var.d])
  }  # end tflag


  # quantile computation
  if (qflag) {
    by_i <- 1 / q_num
    aq <- aggregate(data[,ind.var.d], by=by.vars, drop=FALSE, FUN=quantile,
                   probs=seq(0,1,by_i), na.rm=na_remove) 
    am <- as.matrix(aq)  # convert aggregation to a data frame
    amd <- data.frame(am)
    nm.v <- nm.var.d[1]
    qc <- (n.by + 1):ncol(amd)  # quantile cols only, not by-var names
    names(amd)[qc] <- gsub("x.", "p", names(amd)[qc], fixed=TRUE)
    for (i in seq_along(names(amd))) {   # delete ending . in var names
      nm <- names(amd)[i]
      if (substr(nm, nchar(nm), nchar(nm)) == ".")
        names(amd)[i] <- substr(nm, 1, nchar(nm)-1)
    }
    amd[] <- lapply(amd[], type.convert, as.is=TRUE)  # recover true var types
    if (n.cmp == 0) {  # quantile is only specified compute function
      ss <- merge(n, n_NA, by=names(by.vars), sort=FALSE)
      a <- merge(ss, amd, by=names(by.vars), sort=FALSE)
      names(a)[n.by+1] <- paste(nm.v, "_n", sep="")
      names(a)[n.by+2] <- paste(nm.v, "_na", sep="")
    }
    else {  # more than just quantiles already processed
      a <- cbind(a,amd[(n.by+1):ncol(amd)])
    }
  }  # end qflag


  # ----- post-computation processing -----
  # ---------------------------------------

  # missing by variables data: set NA for _n, _na to 0 or remove if specified
  if (!("n" %in% names(a)))  # multi var names the column "var_n", not "n"
    n_ind <- which(grepl("_n$", names(a)))
  else
    n_ind <- which(names(a) == "n")
  if (!("na" %in% names(a)))  # single var names the column "na", not "var_na"
    na_ind <- which(grepl("_na$", names(a)))
  else
    na_ind <- which(names(a) == "na")
  ind0 <- logical(nrow(a))  # initializes to FALSE
  for (i in seq_len(nrow(a))) {
    for (j in seq_along(n_ind)) {
      if (is.na(a[i,n_ind[j]])) a[i,n_ind[j]] <- 0
    }
    for (j in seq_along(na_ind)) {
      if (is.na(a[i,na_ind[j]])) {
        a[i,na_ind[j]] <- 0
        ind0[i] <- TRUE
      }
    }
  }  # end seq_len(nrow(a))
  if (!na_by_show) a <- a[!ind0,]

  # drop n_ and na_ columns if specified
  if (!show_n) {
    not.n <- c(n_ind, na_ind)
    a <- a[,-not.n]
  }

  # get decimal digits to display column by column
  ind.var <- seq(n.by+1, ncol(a))
  for (i.col in ind.var)
    a[,i.col] <- .decdig(a[,i.col], digits_d)

  # sort if specified
  if (!is.null(sort)) {
    direction <- sort != "+"
    if (!is.null(sort_var)) {
      if (is.numeric(sort_var))
        s.col <- sort_var
      else {
        s.col <- which(names(a) == sort_var)
        if (length(s.col) == 0)
          stop(call.=FALSE, "\n------\n",
               "sort_var \"", sort_var, "\" not found in column names.\n\n")
      }
    }
    else
      s.col <- ncol(a)  # s.col is the number of the sort column
    a <- a[order(a[[s.col]], decreasing=direction), ]
  }

  # --- option to reshape long form a to wide form ---
  # --------------------------------------------------

  # function to abbreviate col headings for constructing a table (not a df)
  abrev <- function (x, room) {  # preserve a space, abbreviate if needed
    has.space <- grepl(" ", x, fixed=TRUE)
    if (has.space) x <- sub(" ", "`", x, fixed=TRUE) 
    x <- abbreviate(x, room-2)
    if (has.space) x <- sub("`", " ", x, fixed=TRUE) 
    return(x)
  }

  if (n.c.by > 0) {

    nm.var.a <- names(a)[ind.var]  # preserve original names

    # delete variables _n and _na
    if ("n" %in% names(a))
      n.ind <- which(names(a) == "n")
    else
      n.ind <- which(grepl("_n$", names(a)))
    if ("na"  %in% names(a))
      miss.ind <- which(names(a) == "na")
    else
      miss.ind <- which(grepl("_na$", names(a)))
    a <- a[, -c(n.ind, miss.ind)]

    # re-reference pivot variables in aggregated data frame a
    i.v <- which(names(a) %in% nm.var.a)
    i.r <- which(names(a) %in% nm.row.by)
    i.c <- which(names(a) %in% nm.col.by)

    # first column by variable
    if (n.c.by == 1) {
      i1.r <- i.r
      i1.c <- i.c
    }
    else if (n.c.by == 2) {
      i1a.r <- i.r
      i1b.r <- i.c[1]  # move first col by var to the row by
      i1.r <- c(i1a.r, i1b.r)
      i1.c <- i.c[2]
    }

    # convert a data frame to reshaped w data frame
    # reshape only works on one col by variable at a time
    # reshape adds verbose variable names
    w <- reshape(a, direction="wide", idvar=names(a)[i1.r],
                    timevar=names(a)[i1.c], v.names=names(a)[i.v])
   # second column by variable
    if (n.c.by == 2) {
      i2.r <- i.r
      i2.c <- i.c[1]
      w <- reshape(w, direction="wide", idvar=names(a)[i2.r],
                      timevar=names(a)[i2.c])
    }
    rownames(w) <- c()  # remove row names
    # drop cols with all NA
    # current set up is for complete crossing of all levels
    # if drop all NA cols, need to have separate levels 2 for each level 1
    # no_col_na: dropping all-NA columns disabled; kept for future use

  # headings
  # move reshaped data frame w to a kable character vector
  # --------

    # if var by_cols names larger than by row name, pad with "|"
    mx <- max(nchar(names(a)[i.c]))
    nm.r1 <- names(a)[i.r[1]]
    if (nchar(nm.r1) < mx) {
      dff <- mx - nchar(nm.r1)
      buf <- paste(rep("|", dff), sep="", collapse="")
      nm.buf <- paste(names(a)[i.r[1]], buf, sep="")
      names(w)[1] <- nm.buf
    }

    # convert reshaped w to kable k, vector of lines of characters
    # remove all by names from w, get " -" locations from kable
    for (i in seq(n.r.by+1, length(names(w)))) names(w)[i] <- " "
    k <- knitr::kable(w, format="pandoc", caption=" ", align="r")
    # get starting positions of columns for labels from k --- line
    g <- gregexpr(" -", k[4], fixed=TRUE)
    g <- unlist(g)

    # get line of blank chars to fill later, including 1st by col name
    nc <- nchar(k[4])
    ln1 <- paste(rep(" ", nc), sep="", collapse="")
    ln1 <- paste(" ", names(a)[i.c[1]], ln1, sep="")

    # fill level labels for one by_cols var
    if (n.c.by == 1) {
      c1.nm <- names(a)[i.c[1]]
      substr(ln1, 2, 2 + nchar(c1.nm)) <- c1.nm
      lvl1 <- levels(a[,i.c[1]])

      for (i in seq_len(max(0L, length(lvl1) - 1L))) {
        room <- g[i+1]-g[i]
        if (nchar(lvl1[i]) > room-1)
          lvl1[i] <- abrev(lvl1[i], room)
      }

      for (i in seq_along(lvl1)) {
        if (is.na(lvl1[i])) lvl1[i] <- "NA" 
        start <- g[n.r.by + (i-1)] + 1
        stop <- start + nchar(lvl1[i])
        room <- stop - start
        substr(ln1, start, stop) <- lvl1[i]
      }
    }

    # two by_cols vars
    else if (n.c.by == 2) {
      lvl1 <- levels(a[,i.c[1]])
      lvl2 <- levels(a[,i.c[2]])

      for (i in seq_len(max(0L, length(lvl2) - 1L))) {  # stop one short: last needs no room check
        room <- g[i+1]-g[i]
        if (is.na(lvl2[i])) lvl2[i] <- "NA" 
        if (nchar(lvl2[i]) > room-1)
          lvl2[i] <- abrev(lvl2[i], room)
      }

      # line with labels for first by_cols var (1st level line)
      # for each lvl1 item, skip lvl2 cols for placement
      for (i in seq_along(lvl1)) {
        if (is.na(lvl1[i])) lvl1[i] <- "NA" 
        start <- g[n.r.by + (i-1)*length(lvl2)] + 1
        stop <- start + nchar(lvl1[i])
        substr(ln1, start, stop) <- lvl1[i]
      }

      ln2 <- paste(rep(" ", nc), sep="", collapse="")  # blank line
      ln2 <- paste(" ", names(a)[i.c[2]], ln2, sep="") # add var name

      # line with labels for second by_cols var
      c2.nm <- names(a)[i.c[2]]
      substr(ln2, 2, 2 + nchar(c2.nm)) <- c2.nm
      for (k in seq_along(lvl1)) {
        for (i in seq_along(lvl2)) {
          if (is.na(lvl2[i])) lvl2[i] <- "NA" 
          start <- g[n.r.by + (k-1)*length(lvl2) + (i-1)] + 1
          stop <- start + nchar(lvl2[i])
          substr(ln2, start, stop) <- lvl2[i]
        }
      }
    }  # end n.c.by == 2

    # caption, with added blank line
    cpt <- paste(nm.cmpt[1], "of", names(data)[ind.var.d], "\n")
    w <- knitr::kable(w, format="pandoc", caption=cpt, align="r")

    if (n.c.by == 1)  # set second line of table w
      w[2] <- ln1
    else
      w[2] <- paste(ln1, "\n", ln2, sep="")

    # clear any "|" padding that may be present
    for (i in seq_len(nchar(w[3])))
      if (substr(w[3],i,i) == "|") substr(w[3],i,i) <- " "

    # when knitr processing, wrap kable table with "```"
    if (!is.null(options()$knitr.in.progress)) {
      w[1] <- paste("```", "\n", w[1], sep="")
      w[length(w)] <- paste(w[length(w)], "\n", "```", sep="")
    }
    a <- w
  }  # end n.c.by > 0
  # ------------------------------------------

  # display constant (single-valued) columns from the drilled-down data
  # data <- na.omit(data)  # remove missing data for comparison of values
  nm <- character(length=ncol(data))
  k <- 0
  for (i in seq_len(ncol(data))) {  # identify constant (single-valued) columns
    if (length(unique(data[,i])) == 1) {
      k <- k + 1
      nm[k] <- names(data)[i]  # name of the constant-valued variable
    }
  }
  nm.TF <- (names(data) %in% nm[seq_len(k)])  # TRUE for each constant column
  if (any(nm.TF)) {
    for (i in seq_len(ncol(data))) {
      if (nm.TF[i]) if (is.numeric(data[[i]]) && !.is.integer((data[[i]])))
        nm.TF[i] <- FALSE  # drop numeric vars that are not integers
    }
    for (i in seq_len(ncol(data))) {
      dval <- data[1,i]
      if (class(data[1,i]) == class(Sys.Date()))  # == "Date" is flagged no
        dval <- as.character(data[1,i])
      if (is.factor(data[1,i])) dval <- as.character(data[1,i])
      if (nm.TF[i])
        cat(names(data)[i], ": ", dval, sep="", "\n")
    }
    cat("\n")
  }

  return(a)
}
