R/PivotTable.R

#' R6 class that represents a pivot table.
#'
#' @description
#' The `PivotTable` class is the primary class for
#' constructing and interacting with a pivot table.
#'
#' @docType class
#' @importFrom R6 R6Class
#' @importFrom data.table data.table is.data.table
#' @import htmlwidgets
#' @import htmltools
#' @export
#' @format \code{\link{R6Class}} object.
#' @examples
#' # The package vignettes include extensive examples of working with the
#' # PivotTable class.
#' library(pivottabler)
#' pt <- PivotTable$new()
#' pt$addData(bhmtrains)
#' pt$addColumnDataGroups("TrainCategory")
#' pt$addRowDataGroups("TOC")
#' pt$defineCalculation(calculationName="TotalTrains",
#' summariseExpression="n()")
#' pt$renderPivot()

PivotTable <- R6::R6Class("PivotTable",
  public = list(

    #' @description
    #' Create a new `PivotTable` object.
    #' @param processingLibrary The package to use when processing data.
    #' Must be one of "auto" (which today is dplyr), "dplyr" or "data.table".
    #' @param evaluationMode Either "batch" (default) or "sequential" (legacy).
    #' @param argumentCheckMode The level of argument checking to perform.
    #' Must be one of "auto", "none", "minimal", "basic", "balanced" (default)
    #' or "full".
    #' @param theme A theme to use to style the pivot table. Either:\cr
    #' (1) The name of a built in theme, or\cr
    #' (2) A list of simple style settings, or\cr
    #' (3) A `PivotStyles` object containing a full set of styles.\cr
    #' See the "Styling" vignette for many examples.
    #' @param replaceExistingStyles Default `FALSE` to retain existing styles in
    #' the styles collection and add specified styles as new custom styles.
    #' Specify `TRUE` to update the definitions of existing styles.
    #' @param tableStyle Styling to apply to the table.  Either:\cr
    #' (1) The name of a built in style, or\cr
    #' (2) A list of CSS style declarations, e.g.\cr
    #' `list("font-weight"="bold", "color"="#0000FF")`, or\cr
    #' (3) A `PivotStyle` object.
    #' @param headingStyle Styling to apply to the headings.
    #' See the `tableStyle` argument for details.
    #' @param cellStyle Styling to apply to the normal cells.
    #' See the `tableStyle` argument for details.
    #' @param totalStyle Styling to apply to the total cells.
    #' See the `tableStyle` argument for details.
    #' @param compatibility A list containing compatibility options to force
    #' legacy behaviours.  See the NEWS file for details.
    #' @param traceEnabled Default `FALSE`.  Specify `TRUE` to generate a trace
    #' for debugging purposes.
    #' @param traceFile If tracing is enabled, the location to generate the trace file.
    #' @return A new `PivotTable` object.
    initialize = function(processingLibrary="auto", evaluationMode="batch", argumentCheckMode="auto",
                          theme=NULL, replaceExistingStyles=FALSE,
                          tableStyle=NULL, headingStyle=NULL, cellStyle=NULL, totalStyle=NULL,
                          compatibility=NULL, traceEnabled=FALSE, traceFile=NULL) {
      checkArgument(4, TRUE, "PivotTable", "initialize", processingLibrary, missing(processingLibrary), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("auto", "dplyr", "data.table"))
      checkArgument(4, TRUE, "PivotTable", "initialize", evaluationMode, missing(evaluationMode), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("batch", "sequential"))
      checkArgument(4, TRUE, "PivotTable", "initialize", argumentCheckMode, missing(argumentCheckMode), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("auto", "none", "minimal", "basic", "balanced", "full"))
      checkArgument(4, TRUE, "PivotTable", "initialize", theme, missing(theme), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("character", "list", "PivotStyles"), allowedListElementClasses="character")
      checkArgument(4, TRUE, "PivotTable", "initialize", replaceExistingStyles, missing(replaceExistingStyles), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
      checkArgument(4, TRUE, "PivotTable", "initialize", tableStyle, missing(tableStyle), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("character", "list", "PivotStyle"))
      checkArgument(4, TRUE, "PivotTable", "initialize", headingStyle, missing(headingStyle), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("character", "list", "PivotStyle"))
      checkArgument(4, TRUE, "PivotTable", "initialize", cellStyle, missing(cellStyle), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("character", "list", "PivotStyle"))
      checkArgument(4, TRUE, "PivotTable", "initialize", totalStyle, missing(totalStyle), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("character", "list", "PivotStyle"))
      checkArgument(4, TRUE, "PivotTable", "initialize", compatibility, missing(compatibility), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list", allowedListElementClasses=c("character", "integer", "numeric", "logical"))
      checkArgument(4, TRUE, "PivotTable", "initialize", traceEnabled, missing(traceEnabled), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
      checkArgument(4, TRUE, "PivotTable", "initialize", traceFile, missing(traceFile), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
      if(argumentCheckMode=="auto") {
        if (length(strsplit(packageDescription("pivottabler")$Version, "\\.")[[1]]) > 3) {
          message("Development version of pivottabler detected: Using argumentCheckMode=full.\nThis may reduce performance. To override, specify the argumentCheckMode explicitly.\nargumentCheckMode values: none, minimal, basic, balanced (the normal default), full.")
          private$p_argumentCheckMode <- 4
        }
        else private$p_argumentCheckMode <- 3
      }
      else if(argumentCheckMode=="none") private$p_argumentCheckMode <- 0
      else if(argumentCheckMode=="minimal") private$p_argumentCheckMode <- 1
      else if(argumentCheckMode=="basic") private$p_argumentCheckMode <- 2
      else if(argumentCheckMode=="balanced") private$p_argumentCheckMode <- 3
      else if(argumentCheckMode=="full") private$p_argumentCheckMode <- 4
      else stop("PivotTable$initialize():  Unknown argumentCheckMode encountered.", call. = FALSE)
      private$p_traceEnabled <- traceEnabled
      if(private$p_traceEnabled&(!is.null(traceFile))) {
        private$p_traceFile <- file(traceFile, open="w")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$new", "Creating new Pivot Table...")
      private$p_compatibility <- compatibility
      if(processingLibrary=="auto") {
        if(requireNamespace(package="dplyr", quietly=TRUE)==TRUE) private$p_processingLibrary <- "dplyr"
        else if(requireNamespace(package="data.table", quietly=TRUE)==TRUE) private$p_processingLibrary <- "data.table"
        else stop("PivotTable$initialize():  No suitable processing library found.  Please install either the dplyr package or the data.table package.", call. = FALSE)
      }
      else if(processingLibrary=="data.table") {
        if(requireNamespace(package="data.table", quietly=TRUE)==FALSE) {
          stop("PivotTable$initialize():  data.table package cannot be loaded.  Please check the package is installed and working.", call. = FALSE)
        }
        private$p_processingLibrary <- "data.table"
      }
      else if(processingLibrary=="dplyr") {
        if(requireNamespace(package="dplyr", quietly=TRUE)==FALSE) {
          stop("PivotTable$initialize():  dplyr package cannot be loaded.  Please check the package is installed and working.", call. = FALSE)
        }
        private$p_processingLibrary <- "dplyr"
      }
      else stop("PivotTable$initialize():  Unknown processingLibrary encountered.", call. = FALSE)
      private$p_evaluationMode <- evaluationMode
      private$p_defaults <- list()
      private$p_lastInstanceId <- 0
      # Create the basic parts of the pivot table
      private$p_data <- PivotData$new(parentPivot=self)
      private$p_rowGroup <- PivotDataGroup$new(parentPivot=self, parentGroup=NULL, rowOrColumn="row", isLevelTotal=TRUE)
      private$p_columnGroup <- PivotDataGroup$new(parentPivot=self, parentGroup=NULL, rowOrColumn="column", isLevelTotal=TRUE)
      private$p_rowGrpHeaders <- list()
      private$p_calculationsPosition <- NULL
      private$p_calculationGroups <- PivotCalculationGroups$new(parentPivot=self)
      private$p_cells <- PivotCells$new(self)
      private$p_htmlRenderer <- PivotHtmlRenderer$new(parentPivot=self)
      private$p_latexRenderer <- PivotLatexRenderer$new(parentPivot=self)
      private$p_openxlsxRenderer <- PivotOpenXlsxRenderer$new(parentPivot=self)
      private$p_timings <- list()
      # apply theming and styles
      if(is.null(theme)) {
        private$p_styles <- getTheme(parentPivot=self, themeName="default")
      }
      else {
        if("PivotStyles" %in% class(theme)) { private$p_styles <- theme }
        else if("list" %in% class(theme)) {
          private$p_styles <- getSimpleColoredTheme(parentPivot=self, themeName="coloredTheme", theme=theme)
        }
        else if("character" %in% class(theme)) {
          if(tolower(trimws(theme))=="none") { theme <- "blank" }
          private$p_styles <- getTheme(parentPivot=self, themeName=theme)
        }
      }
      if(!is.null(tableStyle)) {
        if("PivotStyle" %in% class(tableStyle)) { tableStyle <- tableStyle$declarations }
        if("list" %in% class(tableStyle)) {
          if(private$p_styles$isExistingStyle(private$p_styles$tableStyle)&&(!replaceExistingStyles)) {
            private$p_styles$getStyle(private$p_styles$tableStyle)$setPropertyValues(declarations=tableStyle)
            tableStyle <- private$p_styles$tableStyle
          }
          else {
            private$p_styles$addStyle(styleName="customTableStyle", declarations=tableStyle)
            tableStyle <- "customTableStyle"
          }
        }
        if("character" %in% class(tableStyle)) { private$p_styles$tableStyle <- tableStyle }
      }
      if(!is.null(headingStyle)) {
        if("PivotStyle" %in% class(headingStyle)) { headingStyle <- headingStyle$declarations }
        # root style
        rootStyle <- headingStyle
        if("list" %in% class(rootStyle)) {
          if(private$p_styles$isExistingStyle(private$p_styles$rootStyle)&&(!replaceExistingStyles)) {
            private$p_styles$getStyle(private$p_styles$rootStyle)$setPropertyValues(declarations=rootStyle)
            rootStyle <- private$p_styles$rootStyle
          }
          else {
            private$p_styles$addStyle(styleName="customRootStyle", declarations=rootStyle)
            rootStyle <- "customRootStyle"
          }
        }
        if("character" %in% class(rootStyle)) { private$p_styles$rootStyle <- rootStyle }
        # row heading style
        rowHeaderStyle <- headingStyle
        if("list" %in% class(rowHeaderStyle)) {
          if(private$p_styles$isExistingStyle(private$p_styles$rowHeaderStyle)&&(!replaceExistingStyles)) {
            private$p_styles$getStyle(private$p_styles$rowHeaderStyle)$setPropertyValues(declarations=rowHeaderStyle)
            rowHeaderStyle <- private$p_styles$rowHeaderStyle
          }
          else {
            private$p_styles$addStyle(styleName="customRowHeadingStyle", declarations=rowHeaderStyle)
            rowHeaderStyle <- "customRowHeadingStyle"
          }
        }
        if("character" %in% class(rowHeaderStyle)) { private$p_styles$rowHeaderStyle <- rowHeaderStyle }
        # column heading style
        colHeaderStyle <- headingStyle
        if("list" %in% class(colHeaderStyle)) {
          if(private$p_styles$isExistingStyle(private$p_styles$colHeaderStyle)&&(!replaceExistingStyles)) {
            private$p_styles$getStyle(private$p_styles$colHeaderStyle)$setPropertyValues(declarations=colHeaderStyle)
            colHeaderStyle <- private$p_styles$colHeaderStyle
          }
          else {
            private$p_styles$addStyle(styleName="customColHeadingStyle", declarations=colHeaderStyle)
            colHeaderStyle <- "customColHeadingStyle"
          }
        }
        if("character" %in% class(colHeaderStyle)) { private$p_styles$colHeaderStyle <- colHeaderStyle }
      }
      if(!is.null(cellStyle)) {
        if("PivotStyle" %in% class(cellStyle)) { cellStyle <- cellStyle$declarations }
        if("list" %in% class(cellStyle)) {
          if(private$p_styles$isExistingStyle(private$p_styles$cellStyle)&&(!replaceExistingStyles)) {
            private$p_styles$getStyle(private$p_styles$cellStyle)$setPropertyValues(declarations=cellStyle)
            cellStyle <- private$p_styles$cellStyle
          }
          else {
            private$p_styles$addStyle(styleName="customCellStyle", declarations=cellStyle)
            cellStyle <- "customCellStyle"
          }
        }
        if("character" %in% class(cellStyle)) { private$p_styles$cellStyle <- cellStyle }
      }
      if(!is.null(totalStyle)) {
        if("PivotStyle" %in% class(totalStyle)) { totalStyle <- totalStyle$declarations }
        if("list" %in% class(totalStyle)) {
          if(private$p_styles$isExistingStyle(private$p_styles$totalStyle)&&(!replaceExistingStyles)) {
            private$p_styles$getStyle(private$p_styles$totalStyle)$setPropertyValues(declarations=totalStyle)
            totalStyle <- private$p_styles$totalStyle
          }
          else {
            private$p_styles$addStyle(styleName="customTotalStyle", declarations=totalStyle)
            totalStyle <- "customTotalStyle"
          }
        }
        if("character" %in% class(totalStyle)) { private$p_styles$totalStyle <- totalStyle }
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$new", "Created new Pivot Table.")
      return(invisible())
    },

    #' @description
    #' Specify default values for some function arguments.
    #' @param ... Default values to specify.  See details.
    #' @details
    #' Defaults can be set for the following arguments of
    #' `pt$addRowDataGroups()` and `pt$addColumnDataGroups()`:
    #' `logical` values: `addTotal`, `expandExistingTotals`, `visualTotals`.
    #' `character` values:  `totalPosition`, `totalCaption`.
    #' `list` or `logical` values:  `outlineBefore`, `outlineAfter`, `outlineTotal`.\cr
    #' Errors are generated for default values that could not be set.\cr
    #' Warnings are generated for attempts to set defaults that aren't supported.\cr
    #' See the "A1. Appendix" vignette for more details.
    #' @return No return value.
    setDefault = function(...) {
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$setDefault", "Setting defaults...")
      # get the defaults
      args <- list(...)
      # process each of the defaults...
      invaligArgs <- vector("character", 0)
      for(argName in names(args)) {
        validArg <- FALSE
        # skip defaults with no name
        if(is.null(argName)||(nchar(argName)==0)) {
          stop("PivotTable$setDefault():  Default with no name encountered.  Arguments must be specified in the form name=value.", call. = FALSE)
        }
        # get value
        argValue <- args[[argName]]
        # logical defaults
        if(argName %in% c("addTotal", "expandExistingTotals", "visualTotals")) {
          checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", paste0("setDefault(", argName, ")"), argValue, FALSE, allowMissing=FALSE, allowNull=FALSE, allowedClasses="logical")
          private$p_defaults[[argName]] <- isTRUE(argValue)
          validArg <- TRUE
        }
        # character defaults
        if(argName %in% c("totalPosition", "totalCaption")) {
          allowedValues <- NULL
          if(argName=="totalPosition") allowedValues <- c("before", "after")
          checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", paste0("setDefault(", argName, ")"), argValue, FALSE, allowMissing=FALSE, allowNull=FALSE, allowedClasses="character", allowedValues=allowedValues)
          private$p_defaults[[argName]] <- argValue
          validArg <- TRUE
        }
        # outline settings
        if(argName %in% c("outlineBefore", "outlineAfter", "outlineTotal")) {
          checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", paste0("setDefault(", argName, ")"), argValue, FALSE, allowMissing=FALSE, allowNull=FALSE, allowedClasses="list")
          cleanOutline <- NULL
          if(argName=="outlineBefore") cleanOutline <- cleanOutlineArg(self, argValue)
          else if(argName=="outlineAfter") cleanOutline <- cleanOutlineArg(self, argValue, defaultCaption="")
          else if(argName=="outlineTotal") cleanOutline <- cleanOutlineArg(self, argValue, defaultCaption="Total", defaultIsEmpty=FALSE)
          if(!is.null(cleanOutline)) private$p_defaults[[argName]] <- cleanOutline
          validArg <- TRUE
        }
        # was it a valid argument?
        if(validArg==FALSE) {
          invaligArgs[length(invaligArgs)+1] <- argName
        }
      }
      # warn about any unknown arguments
      if(length(invaligArgs)>0) {
        warning(paste0("PivotTable$setDefault():  Unknown default(s) encountered: ", paste(invaligArgs,collapse=", ")), call. = FALSE)
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$setDefault", "Set defaults.")
    },

    #' @description
    #' Get the default value of an argument.
    #' @param argValue The name and value of the argument.
    #' @param useDefault Specify `TRUE` to use the default.
    #' @details
    #' Both the argument name and argument value are taken from the `argValue`
    #' argument.  The name is obtained from `as.character(substitute(argValue))`.
    #' This function is designed to easily slot into existing code, e.g.
    #' `getDefault1(addTotal, missing(addTotal))`.
    #' @return The current value of the argument or the default value.
    getDefault1 = function(argValue=NULL, useDefault=NULL) {
      argName <- as.character(substitute(argValue))
      return(invisible(self$getDefault2(argName=argName, argValue=argValue, useDefault=useDefault)))
    },

    #' @description
    #' Get the default value of an argument.
    #' @param argName The name of the argument.
    #' @param argValue The current value of the argument.
    #' @param useDefault Specify `TRUE` to use the default.
    #' @return The current value of the argument or the default value.
    getDefault2 = function(argName=NULL, argValue=NULL, useDefault=NULL) {
      if(!isTRUE(useDefault)) return(invisible(argValue))
      defaultNames <- names(private$p_defaults)
      if(argName %in% defaultNames) return(invisible(private$p_defaults[[argName]]))
      else return(invisible(argValue))
    },

    #' @description
    #' Get the default value of an argument.
    #' @param argName The name of the argument.
    #' @return The default value.
    getDefault3 = function(argName) {
      defaultNames <- names(private$p_defaults)
      if(argName %in% defaultNames) return(invisible(private$p_defaults[[argName]]))
      else return(invisible(NULL))
    },

    #' @description
    #' Get the next unique object instance identifier.
    #' @details
    #' R6 classes cannot be easily compared to check if two variables are both
    #' referring to the same object instance.  Instance ids are a mechanism to
    #' work around this problem.  Each data group and cell is assigned an
    #' instance id during object creation, which enables reliable reference
    #' comparisons.
    #' @return An integer instance id.
    getNextInstanceId = function() { # used for reliable object instance comparisons (since R6 cannot easily compare object instances)
      private$p_lastInstanceId <- private$p_lastInstanceId + 1
      return(invisible(private$p_lastInstanceId))
    },

    #' @description
    #' Add a data frame with the specified name to the pivot table.
    #' @param dataFrame The data frame to add.
    #' @param dataName The name to be used to refer to the data frame.
    #' If no name is specified, the data frame variable name from the
    #' calling code is used, retrieved via `deparse(substitute(dataFrame))`.
    #' @details
    #' The name is used to refer to the data frame when generating data groups
    #' or defining calculations.  The pivot table tracks the first data frame
    #' added as the default data frame, so if only a single data frame is used,
    #' it is typically not necessary to ever explicitly refer to the name.
    #' Pivot tables are typically based on a single data frame, however it
    #' is possible to build a pivot table that uses data from multiple data
    #' frames.
    #' @return The `PivotData` object managing the data frames for the pivot table.
    addData = function(dataFrame=NULL, dataName=NULL) {
      timeStart <- proc.time()
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addData", dataFrame, missing(dataFrame), allowMissing=FALSE, allowNull=FALSE, allowedClasses="data.frame")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addData", dataName, missing(dataName), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$addData", "Adding data to Pivot Table...")
      dn <- dataName
      if(is.null(dn)) dn <- deparse(substitute(dataFrame))
      private$p_data$addData(dataFrame, dataName=dn)
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$addData", "Added data to Pivot Table.")
      private$addTiming(paste0("addData(", dn, ")"), timeStart)
      return(invisible(private$p_data))
    },

    #' @description
    #' Add a data frame containing totals data with the specified name and
    #' variables to the pivot table.
    #' @param dataFrame The data frame to add.
    #' @param dataName The name of the data frame to associate these totals with.
    #' @param variableNames A vector specifying how the aggregate data/totals in
    #' the data frame are grouped.
    #' @details
    #' When generating pivot tables, the package typically calculates cell values.
    #' However, the package can also use provided values (i.e. carry out no
    #' calculations).  This presents a challenge in that the sub-totals and totals
    #' in a pivot table display values at a higher aggregation level than the
    #' normal cells in the body of the pivot table.  This method allows further
    #' data frames to be specified that contain aggregated versions of the data.
    #' See the "Calculations" vignette for details and an example.
    #' @return No return value.
    addTotalData = function(dataFrame=NULL, dataName=NULL, variableNames=NULL) {
      timeStart <- proc.time()
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addTotalData", dataFrame, missing(dataFrame), allowMissing=FALSE, allowNull=FALSE, allowedClasses="data.frame")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addTotalData", dataName, missing(dataName), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addTotalData", variableNames, missing(variableNames), allowMissing=FALSE, allowNull=TRUE, allowedClasses="character")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$addTotalData", "Adding totals data to Pivot Table...")
      private$p_data$addTotalData(dataFrame, dataName=dataName, variableNames=variableNames)
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$addTotalData", "Added totals data to Pivot Table.")
      private$addTiming("addTotalData()", timeStart)
      return(invisible())
    },

    #' @description
    #' Retrieve the data groups at the specified level or levels in the column
    #' groups hierarchy.
    #' @param level An integer value or vector specifying one or more level numbers.
    #' Level 1 represents the first visible level of data groups.
    #' @param collapse A logical value specifying whether the return value should be
    #' simplified.  See details.
    #' @details
    #' If `level` is a vector:  If `collapse` is `FALSE`, then a list of lists is
    #' returned, if `collapse` is `TRUE`, then a single combined list is returned.
    #' @return A list containing `PivotDataGroup` objects.
    getColumnGroupsByLevel = function(level=NULL, collapse=FALSE) {
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getColumnGroupsByLevel", "Getting level column groups...")
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getColumnGroupsByLevel", level, missing(level), allowMissing=FALSE, allowNull=FALSE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getColumnGroupsByLevel", collapse, missing(collapse), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
      }
      # multiple levels
      if(length(level)>1) {
        fx <- function(x) { return(self$getColumnGroupsByLevel(level=x)) }
        if(isTRUE(collapse)) { return(invisible(unlist(lapply(level, fx)))) }
        else { return(invisible(lapply(level, fx))) }
      }
      # single level
      if(level<1) stop("PivotTable$getColumnGroupsByLevel():  level must be greater than or equal to one.", call. = FALSE)
      levelCount <- self$columnGroupLevelCount
      if(level>levelCount) stop(paste0("PivotTable$getColumnGroupsByLevel():  level must be less than or equal to ", levelCount, "."), call. = FALSE)
      grps <- private$p_columnGroup$getLevelGroups(level=level)
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getColumnGroupsByLevel", "Got level column groups.", list(count = length(grps)))
      return(invisible(grps))
    },

    #' @description
    #' [Deprecated: Use topColumnGroups instead]
    #' Retrieve the first level of column data groups.
    #' @return A list containing `PivotDataGroup` objects.
    getTopColumnGroups = function() {
      .Deprecated(new="topColumnGroups")
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getTopColumnGroups", "Getting top level column groups...")
      grps <- private$p_columnGroup$childGroups
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getTopColumnGroups", "Got top level column groups.", list(count = length(grps)))
      return(invisible(grps))
    },

    #' @description
    #' [Deprecated: Use leafColumnGroups instead]
    #' Retrieve the bottom level of column data groups.
    #' @return A list containing `PivotDataGroup` objects.
    getLeafColumnGroups = function() {
      .Deprecated(new="leafColumnGroups")
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getLeafColumnGroups", "Getting leaf level column groups...")
      leafGroups = list()
      grps <- private$p_columnGroup$getLeafGroups(leafGroups)
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getLeafColumnGroups", "Got leaf level column groups.", list(count = length(grps)))
      return(invisible(grps))
    },

    #' @description
    #' Retrieve the leaf-level data group associated with a specific column or
    #' columns.
    #' @param c An integer column number or an integer vector of column numbers.
    #' @return A `PivotDataGroup` object or a list of `PivotDataGroup` objects.
    getLeafColumnGroup = function(c=NULL) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getLeafColumnGroup", c, missing(c), allowMissing=FALSE, allowNull=FALSE, allowedClasses=c("integer", "numeric"))
      }
      # multiple columns
      if(length(c)>1) {
        return(invisible(lapply(c, self$getLeafColumnGroup)))
      }
      # single column
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getLeafColumnGroup", "Getting leaf level column group...")
      if((private$p_evaluated)&&(!is.null(private$p_cells))) {
        # retrieve directly from the cells instance
        grp <- private$p_cells$getColumnGroup(c=c)
      }
      else {
        # fall back to the slower method (requires navigating through the hierarchy)
        grps <- self$leafColumnGroups
        if(length(grps)>c) grp <- grps[[c]]
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getLeafColumnGroup", "Got leaf level column group.")
      return(invisible(grp))
    },

    #' @description
    #' Add a new column data group at the top level of the column group
    #' hierarchy.  The new group is added as the last child unless an index
    #' is specified.
    #' @details See the "Irregular Layout" vignette for details and examples.
    #' @param variableName A character value that specifies the name of the
    #' variable in the data frame that the group relates to and will filter.
    #' @param filterType Must be one of "ALL", "VALUES", or "NONE" to specify
    #' the filter type:\cr
    #' ALL means no filtering is applied.\cr
    #' VALUEs is the typical value used to specify that `variableName` is
    #' filtered to only `values`.\cr
    #' NONE means no data will match this data group.
    #' @param values A vector that specifies the filter values applied to
    #' `variableName` to select the data to match this row/column in the pivot
    #' table.
    #' @param doNotExpand Default value `FALSE` - specify `TRUE` to
    #' prevent the high-level methods such as `addDataGroups()` from adding
    #' child groups.
    #' @param isEmpty Default value `FALSE`, specify `TRUE` to mark that
    #' this group contains no data (e.g. if
    #' it is part of a header or outline row)
    #' @param isOutline Default value `FALSE` - specify `TRUE` to mark
    #' that this data group is an outline group.
    #' @param styleAsOutline Default value `FALSE` - specify `TRUE` to style
    #' this data group as an outline group.  Only applicable when
    #' `isOutline` is `TRUE`.
    #' @param captionTemplate A character value that specifies the template
    #' for the data group caption, default "{values}".
    #' @param caption Effectively a hard-coded caption that overrides the
    #' built-in logic for generating a caption.
    #' @param isTotal Default `FALSE` - specify `TRUE` to mark that this
    #' data group is a total.
    #' @param isLevelSubTotal Default `FALSE` - specify `TRUE` to mark that
    #' this data group is a sub-total within a level.
    #' @param isLevelTotal Default `FALSE` - specify `TRUE` to mark that
    #' this data group is level total.
    #' @param calculationGroupName For calculation groups, this character value
    #' specifies the calculation group that `calculationName` belongs to.
    #' @param calculationName For calculation groups, this character value
    #' specifies the name of the calculation.
    #' @param baseStyleName The style name for the data group.
    #' @param styleDeclarations A list of CSS style declarations to overlay
    #' on top of the base style.
    #' @param insertAtIndex An integer that specifies the index in the list
    #' of child groups where the new group should be inserted.
    #' @param insertBeforeGroup Specifies an existing group that the new group
    #'  should be inserted before.
    #' @param insertAfterGroup Specifies an existing group that the new group
    #'  should be inserted after
    #' @param mergeEmptySpace A character value that specifies how empty space
    #' should be merged. This is typically only used with outline groups
    #' (so applies to row groups only, not column groups).  Must be one of
    #' "doNotMerge", "dataGroupsOnly", "cellsOnly", "dataGroupsAndCellsAs1" or
    #' "dataGroupsAndCellsAs2".  See the "Regular Layout" vignette
    #' for more information.
    #' @param cellBaseStyleName The style name for cells related to this data
    #' group.
    #' @param cellStyleDeclarations A list of CSS style declarations to overlay
    #' on top of the base style for cells related to this data group
    #' @param sortAnchor Used to specify sort behaviour for outline groups, must
    #' be one of "fixed", "next" or "previous".
    #' @param resetCells Default `TRUE` to reset any cells that currently exist
    #' in the pivot table and trigger a recalculation of the pivot table when
    #' it is next rendered.
    #' @return The new `PivotDataGroup` object.
    addColumnGroup = function(variableName=NULL, filterType="ALL", values=NULL,
                             doNotExpand=FALSE, isEmpty=FALSE, isOutline=FALSE, styleAsOutline=FALSE,
                             captionTemplate="{value}", caption=NULL,
                             isTotal=FALSE, isLevelSubTotal=FALSE, isLevelTotal=FALSE,
                             calculationGroupName=NULL, calculationName=NULL,
                             baseStyleName=NULL, styleDeclarations=NULL,
                             insertAtIndex=NULL, insertBeforeGroup=NULL, insertAfterGroup=NULL,
                             mergeEmptySpace=NULL, cellBaseStyleName=NULL, cellStyleDeclarations=NULL,
                             sortAnchor=NULL, resetCells=TRUE) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnGroup", variableName, missing(variableName), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnGroup", filterType, missing(filterType), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("ALL", "VALUES", "NONE"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnGroup", values, missing(values), allowMissing=TRUE, allowNull=TRUE, mustBeAtomic=TRUE)
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnGroup", doNotExpand, missing(doNotExpand), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnGroup", isEmpty, missing(isEmpty), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnGroup", isOutline, missing(isOutline), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnGroup", styleAsOutline, missing(styleAsOutline), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnGroup", captionTemplate, missing(captionTemplate), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnGroup", caption, missing(caption), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("character", "integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnGroup", isTotal, missing(isTotal), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnGroup", isLevelSubTotal, missing(isLevelSubTotal), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnGroup", isLevelTotal, missing(isLevelTotal), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnGroup", calculationGroupName, missing(calculationGroupName), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnGroup", calculationName, missing(calculationName), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnGroup", baseStyleName, missing(baseStyleName), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnGroup", styleDeclarations, missing(styleDeclarations), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list", allowedListElementClasses=c("character", "integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnGroup", insertAtIndex, missing(insertAtIndex), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnGroup", insertBeforeGroup, missing(insertBeforeGroup), allowMissing=TRUE, allowNull=TRUE, allowedClasses="PivotDataGroup")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnGroup", insertAfterGroup, missing(insertAfterGroup), allowMissing=TRUE, allowNull=TRUE, allowedClasses="PivotDataGroup")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnGroup", mergeEmptySpace, missing(mergeEmptySpace), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character", allowedValues=c("doNotMerge", "dataGroupsOnly", "cellsOnly", "dataGroupsAndCellsAs1", "dataGroupsAndCellsAs2"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnGroup", cellBaseStyleName, missing(cellBaseStyleName), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnGroup", cellStyleDeclarations, missing(cellStyleDeclarations), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list", allowedListElementClasses=c("character", "integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnGroup", sortAnchor, missing(sortAnchor), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character", allowedValues=c("fixed", "next", "previous"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnGroup", resetCells, missing(resetCells), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
      }
      if(private$p_argumentCheckMode==TRUE) private$p_traceEnabled("PivotTable$addColumnGroup", "Adding column group...",
                                                                   list(captionTemplate=captionTemplate, caption=caption,
                                                                        doNotExpand=doNotExpand, isEmpty=isEmpty, isOutline=isOutline, styleAsOutline=styleAsOutline,
                                                                        isTotal=isTotal, isLevelSubTotal=isLevelSubTotal, isLevelTotal=isLevelTotal,
                                                                        variableName=variableName, values=values,
                                                                        calculationGroupName=calculationGroupName, calculationName=calculationName,
                                                                        baseStyleName=baseStyleName, styleDeclarations=styleDeclarations,
                                                                        mergeEmptySpace=mergeEmptySpace, cellBaseStyleName=cellBaseStyleName,
                                                                        cellStyleDeclarations=cellStyleDeclarations, sortAnchor=sortAnchor,
                                                                        resetCells=resetCells))
      # default to style as outline if not specified
      if(isOutline && missing(styleAsOutline)) styleAsOutline <- TRUE
      # add the group
      grp <- private$p_columnGroup$addChildGroup(variableName=variableName, filterType=filterType, values=values,
                                                 doNotExpand=doNotExpand, isEmpty=isEmpty, isOutline=isOutline, styleAsOutline=styleAsOutline,
                                                 captionTemplate=captionTemplate, caption=caption,
                                                 isTotal=isTotal, isLevelSubTotal=isLevelSubTotal, isLevelTotal=isLevelTotal,
                                                 calculationGroupName=calculationGroupName, calculationName=calculationName,
                                                 baseStyleName=baseStyleName, styleDeclarations=styleDeclarations,
                                                 insertAtIndex=insertAtIndex, insertBeforeGroup=insertBeforeGroup, insertAfterGroup=insertAfterGroup,
                                                 mergeEmptySpace=mergeEmptySpace, cellBaseStyleName=cellBaseStyleName, cellStyleDeclarations=cellStyleDeclarations,
                                                 sortAnchor=sortAnchor, resetCells=resetCells)
      if(private$p_argumentCheckMode==TRUE) private$p_traceEnabled("PivotTable$addColumnGroup", "Added column group.")
      return(invisible(grp))
    },

    #' @description
    #' Add multiple new data groups to the column group hierarchy
    #' based on the distinct values in a data frame
    #' column or using explicitly specified data values.
    #' See the "Data Groups" vignette for example usage.
    #' @details
    #' There are broadly three different ways to call `addColumnDataGroups()`:\cr
    #' (1) dataName=name, fromData=TRUE, onlyCombinationsThatExist=TRUE - which
    #' considers the ancestors of each existing data group to generate only those
    #' combinations of values that exist in the data frame.\cr
    #' (2) dataName=name, fromData=TRUE, onlyCombinationsThatExist=FALSE - which
    #' ignores the ancestors of each existing data group and simply adds every
    #' distinct value of the specified variable under every existing data group,
    #' which can result in combinations of values in the pivot table that don't
    #' exist in the data frame (i.e. blank rows/columns in the pivot table).\cr
    #' (3) fromData=FALSE, explicitListOfValues=list(...) - simply adds every
    #' value from the specified list under every existing data group.
    #' @param variableName The name of the related column in the data frame(s) of
    #'   the pivot table.
    #' @param atLevel The level number that specifies where to add the new
    #' groups.  Level 1 = on the first visible level of the hierarchy.
    #' `NULL` = create a new level at the bottom of the hierarchy for the new
    #' groups.
    #' @param fromData Default `TRUE` to generate the new data groups based on the
    #' data values that exist in the `variableName` column in the named data frame.
    #' If `FALSE`, then `explicitListOfValues` must be specified.
    #' @param dataName The name of the data frame (as specified in
    #' `pt$addData()`) to read the data group values from.
    #' @param dataSortOrder Must be one of "asc", "desc", "custom" or "none".
    #' @param customSortOrder A vector values sorted into the desired order.
    #' @param caption The template of data group captions to generate,
    #' default "{value}".
    #' @param dataFormat A character, list or custom function to format the
    #' data value.
    #' @param dataFmtFuncArgs A list that specifies any additional arguments to
    #' pass to a custom format function.
    #' @param onlyCombinationsThatExist Default `TRUE` to generate only
    #' combinations of data groups that exist in the data frame.
    #' @param explicitListOfValues A list of explicit values to create data
    #' groups from.  A data group is created for each element of the list.
    #' If a list element is vector of values (with length greater than 1),
    #' then a data group is created for multiple values instead of just
    #' a single value.
    #' @param calculationGroupName The calculation group that the new data groups
    #' are related to.
    #' @param expandExistingTotals Default `FALSE`, which means totals are not
    #' broken down in multi-level hierarchies.
    #' @param addTotal Default `TRUE`, which means sub-total and total data groups
    #' are automatically added.
    #' @param visualTotals Default `FALSE`, which means visual totals are disabled.
    #' See the "Data Groups" vignette for more details about visual totals.
    #' @param totalPosition Either "before" or "after" to specify where total groups
    #' are created, default "after".
    #' @param totalCaption The caption to display on total groups, default "Total".
    #' @param onlyAddGroupIf A filter expression that can be used to more finely
    #' control whether data groups are created at different locations in the
    #' hierarchy.  There must be at least one row that matches this filter and the
    #' filters from the ancestor groups in order that the child group is created.
    #' E.g. `MaxDisplayLevel>5`.
    #' @param preGroupData Default `TRUE`, which means that the pivot table
    #' pre-calculates the distinct combinations of variable values to reduce the CPU
    #' time and elapsed time required to generate data groups.
    #' Cannot be used in conjunction with the
    #' @param baseStyleName The name of the style applied to this data group (i.e.
    #'   this row/column heading).  The style must exist in the `PivotStyles` object
    #'   associated with the PivotTable.
    #' @param styleDeclarations CSS style declarations that can override the base
    #' style, expressed as a list, e.g. `list("font-weight"=bold")`.
    #' @return A list of new `PivotDataGroup` objects that have been added.
    addColumnDataGroups = function(variableName=NULL, atLevel=NULL, fromData=TRUE, # atLevel=1 is the top level, (since 1 is the top level as visible to the user)
                                   dataName=NULL, dataSortOrder="asc", customSortOrder=NULL, caption="{value}", dataFormat=NULL, dataFmtFuncArgs=NULL,
                                   onlyCombinationsThatExist=TRUE, explicitListOfValues=NULL, calculationGroupName=NULL,
                                   expandExistingTotals=FALSE, addTotal=TRUE, visualTotals=FALSE, totalPosition="after", totalCaption="Total",
                                   onlyAddGroupIf=NULL, preGroupData=TRUE, baseStyleName=NULL, styleDeclarations=NULL) {
      timeStart <- proc.time()
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnDataGroups", variableName, missing(variableName), allowMissing=FALSE, allowNull=FALSE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnDataGroups", atLevel, missing(atLevel), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnDataGroups", fromData, missing(fromData), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnDataGroups", dataName, missing(dataName), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnDataGroups", dataSortOrder, missing(dataSortOrder), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("asc", "desc", "custom", "none"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnDataGroups", customSortOrder, missing(customSortOrder), allowMissing=TRUE, allowNull=TRUE, mustBeAtomic=TRUE)
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnDataGroups", caption, missing(caption), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnDataGroups", dataFormat, missing(dataFormat), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("character", "list", "function"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnDataGroups", dataFmtFuncArgs, missing(dataFmtFuncArgs), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnDataGroups", onlyCombinationsThatExist, missing(onlyCombinationsThatExist), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnDataGroups", explicitListOfValues, missing(explicitListOfValues), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list", listElementsMustBeAtomic=TRUE)
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnDataGroups", calculationGroupName, missing(calculationGroupName), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnDataGroups", expandExistingTotals, missing(expandExistingTotals), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnDataGroups", addTotal, missing(addTotal), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnDataGroups", visualTotals, missing(visualTotals), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnDataGroups", totalPosition, missing(totalPosition), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("before", "after"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnDataGroups", totalCaption, missing(totalCaption), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnDataGroups", onlyAddGroupIf, missing(onlyAddGroupIf), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnDataGroups", preGroupData, missing(preGroupData), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnDataGroups", baseStyleName, missing(baseStyleName), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnDataGroups", styleDeclarations, missing(styleDeclarations), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list", allowedListElementClasses=c("character", "integer", "numeric"))
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$addColumnDataGroups", "Adding column groups...",
                   list(variableName=variableName, atLevel=atLevel, fromData=fromData,
                        dataName=dataName, dataSortOrder=dataSortOrder, customSortOrder=customSortOrder,
                        caption=caption, dataFormat=dataFormat, dataFmtFuncArgs=dataFmtFuncArgs,
                        onlyCombinationsThatExist=onlyCombinationsThatExist, explicitListOfValues=explicitListOfValues,
                        calculationGroupName=calculationGroupName, expandExistingTotals=expandExistingTotals,
                        addTotal=addTotal, visualTotals=visualTotals, totalPosition=totalPosition, totalCaption=totalCaption,
                        onlyAddGroupIf=onlyAddGroupIf, preGroupData=preGroupData, baseStyleName=baseStyleName, styleDeclarations=styleDeclarations))
      if((!is.null(styleDeclarations))&&(length(styleDeclarations)!=length(names(styleDeclarations))))
        stop("PivotTable$addColumnDataGroups(): One or more style declarations are missing a name.", call. = FALSE)
      levelsBelow <- NULL
      if((!is.null(atLevel))&&(atLevel>0)) levelsBelow <- atLevel - 1
      grp <- private$p_columnGroup$addDataGroups(variableName=variableName, atLevel=levelsBelow, fromData=fromData,
                                                 dataName=dataName, dataSortOrder=dataSortOrder, customSortOrder=customSortOrder,
                                                 caption=caption, dataFormat=dataFormat, dataFmtFuncArgs=dataFmtFuncArgs,
                                                 onlyCombinationsThatExist=onlyCombinationsThatExist, explicitListOfValues=explicitListOfValues,
                                                 calculationGroupName=calculationGroupName,
                                                 expandExistingTotals=self$getDefault1(expandExistingTotals, missing(expandExistingTotals)),
                                                 addTotal=self$getDefault1(addTotal, missing(addTotal)),
                                                 visualTotals=self$getDefault1(visualTotals, missing(visualTotals)),
                                                 totalPosition=self$getDefault1(totalPosition, missing(totalPosition)),
                                                 totalCaption=self$getDefault1(totalCaption, missing(totalCaption)),
                                                 onlyAddGroupIf=onlyAddGroupIf, preGroupData=preGroupData, baseStyleName=baseStyleName, styleDeclarations=styleDeclarations)
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$addColumnDataGroups", "Added column groups.")
      private$addTiming(paste0("addColumnDataGroups(", variableName, ")"), timeStart)
      return(invisible(grp))
    },

    #' @description
    #' Normalise the column data group hierarchy so that all branches have the
    #' same number of levels - accomplished by adding empty child data groups
    #' where needed.
    #' @param resetCells Default `TRUE` to reset any cells that currently exist
    #' in the pivot table and trigger a recalculation of the pivot table when
    #' it is next rendered.
    #' @return A list of new `PivotDataGroup` objects that have been added.
    normaliseColumnGroups = function(resetCells=TRUE) {
      timeStart <- proc.time()
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$normaliseColumnGroups", "Normalising column groups...")
      groupsAdded <- private$p_columnGroup$normaliseDataGroup(resetCells)
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$normaliseColumnGroups", "Normalised column groups.", list(groupsAdded = groupsAdded))
      private$addTiming("normaliseColumnGroups", timeStart)
      return(invisible())
    },

    #' @description
    #' Sort column data groups either by the data group data value, caption,
    #' a custom order or based on calculation result values.
    #' @param levelNumber The level number to sort the data groups, e.g.
    #' level 1 (default) sorts the data groups at level 1 of the hierarchy
    #' (which is the first visible level of data groups).
    #' @param orderBy Must be either "value", "caption", "calculation",
    #' "customByValue" or "customByCaption".\cr
    #' "value" sorts by the raw (i.e. unformatted) group value.\cr
    #' "caption" sorts by the formatted character group caption.\cr
    #' "calculation" sorts using one of the calculations defined in the pivot table.\cr
    #' "customValue" sorts by the raw (i.e. unformatted) group value according to
    #' the specified custom sort order.\cr
    #' "customCaption" sorts by the formatted character group caption according to
    #' the specified custom sort order.
    #' @param customOrder A vector values sorted into the desired order.
    #' @param sortOrder Must be either "asc" or "desc".
    #' @param calculationGroupName If sorting using a calculation, the name of the
    #' calculation group containing the specified calculation.
    #' @param calculationName If sorting using a calculation, the name of the
    #' calculation.
    #' @param fromIndex A boundary to limit the sort operation.
    #' @param toIndex A boundary to limit the sort operation.
    #' @param resetCells Default `TRUE` to reset any cells that currently exist
    #' in the pivot table and trigger a recalculation of the pivot table when
    #' it is next rendered.
    #' @return No return value.
    sortColumnDataGroups = function(levelNumber=1, orderBy="calculation", customOrder=NULL, sortOrder="desc",
                                    calculationGroupName="default", calculationName=NULL,
                                    fromIndex=NULL, toIndex=NULL, resetCells=TRUE) {
      timeStart <- proc.time()
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "sortColumnDataGroups", levelNumber, missing(levelNumber), allowMissing=TRUE, allowNull=FALSE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "sortColumnDataGroups", orderBy, missing(orderBy), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("value","caption","calculation","customByValue","customByCaption"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "sortColumnDataGroups", customOrder, missing(customOrder), allowMissing=TRUE, allowNull=TRUE, mustBeAtomic=TRUE)
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "sortColumnDataGroups", sortOrder, missing(sortOrder), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("asc","desc"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "sortColumnDataGroups", calculationGroupName, missing(calculationGroupName), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "sortColumnDataGroups", calculationName, missing(calculationName), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "sortColumnDataGroups", fromIndex, missing(fromIndex), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "sortColumnDataGroups", toIndex, missing(toIndex), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "sortColumnDataGroups", resetCells, missing(resetCells), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$sortColumnDataGroups", "Sorting column data groups...",
                    list(levelNumber=levelNumber, orderBy=orderBy, customOrder=customOrder, sortOrder=sortOrder,
                         calculationGroupName=calculationGroupName, calculationName=calculationName,
                         fromIndex=fromIndex, toIndex=toIndex, resetCells=resetCells))
      if(levelNumber<1) stop("PivotTable$sortColumnDataGroups():  levelNumber must be 1 or above.", call. = FALSE)
      private$p_columnGroup$sortDataGroups(levelNumber=levelNumber-1, orderBy=orderBy, customOrder=customOrder, sortOrder=sortOrder,
                                           calculationGroupName=calculationGroupName, calculationName=calculationName,
                                           fromIndex=fromIndex, toIndex=toIndex, resetCells=resetCells)
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$sortColumnDataGroups", "Sorted column data groups.")
      private$addTiming("sortColumnDataGroups", timeStart)
      return(invisible())
    },

    #' @description
    #' Retrieve the data groups at the specified level or levels in the row groups
    #' hierarchy.
    #' @param level An integer value or vector specifying one or more level numbers.
    #' Level 1 represents the first visible level of data groups.
    #' @param collapse A logical value specifying whether the return value should be
    #' simplified.  See details.
    #' @details
    #' If `level` is a vector:  If `collapse` is `FALSE`, then a list of lists is
    #' returned, if `collapse` is `TRUE`, then a single combined list is returned.
    #' @return A list containing `PivotDataGroup` objects.
    getRowGroupsByLevel = function(level=NULL, collapse=FALSE) {
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getRowGroupsByLevel", "Getting level row groups...")
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getRowGroupsByLevel", level, missing(level), allowMissing=FALSE, allowNull=FALSE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getRowGroupsByLevel", collapse, missing(collapse), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
      }
      # multiple levels
      if(length(level)>1) {
        fx <- function(x) { return(self$getRowGroupsByLevel(level=x)) }
        if(isTRUE(collapse)) { return(invisible(unlist(lapply(level, fx)))) }
        else { return(invisible(lapply(level, fx))) }
      }
      # single level
      if(level<1) stop("PivotTable$getRowGroupsByLevel():  level must be greater than or equal to one.", call. = FALSE)
      levelCount <- self$rowGroupLevelCount
      if(level>levelCount) stop(paste0("PivotTable$getRowGroupsByLevel():  level must be less than or equal to ", levelCount, "."), call. = FALSE)
      grps <- private$p_rowGroup$getLevelGroups(level=level)
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getRowGroupsByLevel", "Got level row groups", list(count = length(grps)))
      return(invisible(grps))
    },

    #' @description
    #' [Deprecated: Use topRowGroups instead]
    #' Retrieve the first level of row data groups.
    #' @return A list containing `PivotDataGroup` objects.
    getTopRowGroups = function() {
      .Deprecated(new="topRowGroups")
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getTopRowGroups", "Getting top level row groups...")
      grps <- private$p_rowGroup$childGroups
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getTopRowGroups", "Got top level row groups", list(count = length(grps)))
      return(invisible(grps))
    },

    #' @description
    #' [Deprecated: Use leafRowGroups instead]
    #' Retrieve the bottom level of row data groups.
    #' @return A list containing `PivotDataGroup` objects.
    getLeafRowGroups = function() {
      .Deprecated(new="leafRowGroups")
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getLeafRowGroups", "Getting leaf level row groups...")
      leafGroups = list()
      grps <- private$p_rowGroup$getLeafGroups(leafGroups)
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getTopRowGroups", "Got leaf level row groups", list(count = length(grps)))
      return(invisible(grps))
    },

    #' @description
    #' Retrieve the leaf-level data group associated with a specific row or rows.
    #' @param r An integer row number or an integer vector of row numbers.
    #' @return A `PivotDataGroup` object or a list of `PivotDataGroup` objects.
    getLeafRowGroup = function(r=NULL) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getLeafRowGroup", r, missing(r), allowMissing=FALSE, allowNull=FALSE, allowedClasses=c("integer", "numeric"))
      }
      # multiple rows
      if(length(r)>1) {
        return(invisible(lapply(r, self$getLeafRowGroup)))
      }
      # single row
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getLeafRowGroup", "Getting leaf level row group...")
      if((private$p_evaluated)&&(!is.null(private$p_cells))) {
        # retrieve directly from the cells instance
        grp <- private$p_cells$getRowGroup(r=r)
      }
      else {
        # fall back to the slower method (requires navigating through the hierarchy)
        grps <- self$leafRowGroups
        if(length(grps)>r) grp <- grps[[r]]
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getLeafRowGroup", "Got leaf level row group.")
      return(invisible(grp))
    },

    #' @description
    #' Add a new column data group at the top level of the row group
    #' hierarchy.  The new group is added as the last child unless an index
    #' is specified.
    #' @details See the "Irregular Layout" vignette for details and examples.
    #' @param variableName A character value that specifies the name of the
    #' variable in the data frame that the group relates to and will filter.
    #' @param filterType Must be one of "ALL", "VALUES", or "NONE" to specify
    #' the filter type:\cr
    #' ALL means no filtering is applied.\cr
    #' VALUEs is the typical value used to specify that `variableName` is
    #' filtered to only `values`.\cr
    #' NONE means no data will match this data group.
    #' @param values A vector that specifies the filter values applied to
    #' `variableName` to select the data to match this row/column in the pivot
    #' table.
    #' @param doNotExpand Default value `FALSE` - specify `TRUE` to
    #' prevent the high-level methods such as `addDataGroups()` from adding
    #' child groups.
    #' @param isEmpty Default value `FALSE`, specify `TRUE` to mark that
    #' this group contains no data (e.g. if
    #' it is part of a header or outline row)
    #' @param isOutline Default value `FALSE` - specify `TRUE` to mark
    #' that this data group is an outline group.
    #' @param styleAsOutline Default value `FALSE` - specify `TRUE` to style
    #' this data group as an outline group.  Only applicable when
    #' `isOutline` is `TRUE`.
    #' @param captionTemplate A character value that specifies the template
    #' for the data group caption, default "{values}".
    #' @param caption Effectively a hard-coded caption that overrides the
    #' built-in logic for generating a caption.
    #' @param isTotal Default `FALSE` - specify `TRUE` to mark that this
    #' data group is a total.
    #' @param isLevelSubTotal Default `FALSE` - specify `TRUE` to mark that
    #' this data group is a sub-total within a level.
    #' @param isLevelTotal Default `FALSE` - specify `TRUE` to mark that
    #' this data group is level total.
    #' @param calculationGroupName For calculation groups, this character value
    #' specifies the calculation group that `calculationName` belongs to.
    #' @param calculationName For calculation groups, this character value
    #' specifies the name of the calculation.
    #' @param baseStyleName The style name for the data group.
    #' @param styleDeclarations A list of CSS style declarations to overlay
    #' on top of the base style.
    #' @param insertAtIndex An integer that specifies the index in the list
    #' of child groups where the new group should be inserted.
    #' @param insertBeforeGroup Specifies an existing group that the new group
    #'  should be inserted before.
    #' @param insertAfterGroup Specifies an existing group that the new group
    #'  should be inserted after
    #' @param mergeEmptySpace A character value that specifies how empty space
    #' should be merged. This is typically only used with outline groups
    #' (so applies to row groups only, not column groups).  Must be one of
    #' "doNotMerge", "dataGroupsOnly", "cellsOnly", "dataGroupsAndCellsAs1" or
    #' "dataGroupsAndCellsAs2".  See the "Regular Layout" vignette
    #' for more information.
    #' @param cellBaseStyleName The style name for cells related to this data
    #' group.
    #' @param cellStyleDeclarations A list of CSS style declarations to overlay
    #' on top of the base style for cells related to this data group
    #' @param sortAnchor Used to specify sort behaviour for outline groups, must
    #' be one of "fixed", "next" or "previous".
    #' @param outlineLinkedGroupId Used to link an outline group to the value
    #' data group which has the child data groups.
    #' @param resetCells Default `TRUE` to reset any cells that currently exist
    #' in the pivot table and trigger a recalculation of the pivot table when
    #' it is next rendered.
    #' @return The new `PivotDataGroup` object.
    addRowGroup = function(variableName=NULL, filterType="ALL", values=NULL,
                           doNotExpand=FALSE, isEmpty=FALSE, isOutline=FALSE, styleAsOutline=FALSE,
                           captionTemplate="{value}", caption=NULL,
                           isTotal=FALSE, isLevelSubTotal=FALSE, isLevelTotal=FALSE,
                           calculationGroupName=NULL, calculationName=NULL,
                           baseStyleName=NULL, styleDeclarations=NULL,
                           insertAtIndex=NULL, insertBeforeGroup=NULL, insertAfterGroup=NULL,
                           mergeEmptySpace=NULL, cellBaseStyleName=NULL, cellStyleDeclarations=NULL,
                           sortAnchor=NULL, resetCells=TRUE) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowGroup", variableName, missing(variableName), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowGroup", filterType, missing(filterType), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("ALL", "VALUES", "NONE"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowGroup", values, missing(values), allowMissing=TRUE, allowNull=TRUE, mustBeAtomic=TRUE)
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowGroup", doNotExpand, missing(doNotExpand), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowGroup", isEmpty, missing(isEmpty), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowGroup", isOutline, missing(isOutline), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowGroup", styleAsOutline, missing(styleAsOutline), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowGroup", captionTemplate, missing(captionTemplate), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowGroup", caption, missing(caption), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("character", "integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowGroup", isTotal, missing(isTotal), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowGroup", isLevelSubTotal, missing(isLevelSubTotal), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowGroup", isLevelTotal, missing(isLevelTotal), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowGroup", calculationGroupName, missing(calculationGroupName), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowGroup", calculationName, missing(calculationName), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowGroup", baseStyleName, missing(baseStyleName), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowGroup", styleDeclarations, missing(styleDeclarations), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list", allowedListElementClasses=c("character", "integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowGroup", insertAtIndex, missing(insertAtIndex), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowGroup", insertBeforeGroup, missing(insertBeforeGroup), allowMissing=TRUE, allowNull=TRUE, allowedClasses="PivotDataGroup")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowGroup", insertAfterGroup, missing(insertAfterGroup), allowMissing=TRUE, allowNull=TRUE, allowedClasses="PivotDataGroup")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowGroup", mergeEmptySpace, missing(mergeEmptySpace), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character", allowedValues=c("doNotMerge", "dataGroupsOnly", "cellsOnly", "dataGroupsAndCellsAs1", "dataGroupsAndCellsAs2"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowGroup", cellBaseStyleName, missing(cellBaseStyleName), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowGroup", cellStyleDeclarations, missing(cellStyleDeclarations), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list", allowedListElementClasses=c("character", "integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowGroup", sortAnchor, missing(sortAnchor), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character", allowedValues=c("fixed", "next", "previous"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowGroup", resetCells, missing(resetCells), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
      }
      if(private$p_argumentCheckMode==TRUE) private$p_traceEnabled("PivotTable$addRowGroup", "Adding row group...",
                                                                   list(captionTemplate=captionTemplate, caption=caption,
                                                                        doNotExpand=doNotExpand, isEmpty=isEmpty, isOutline=isOutline, styleAsOutline=styleAsOutline,
                                                                        isTotal=isTotal, isLevelSubTotal=isLevelSubTotal, isLevelTotal=isLevelTotal,
                                                                        variableName=variableName, values=values,
                                                                        calculationGroupName=calculationGroupName, calculationName=calculationName,
                                                                        baseStyleName=baseStyleName, styleDeclarations=styleDeclarations,
                                                                        mergeEmptySpace=mergeEmptySpace, cellBaseStyleName=cellBaseStyleName,
                                                                        cellStyleDeclarations=cellStyleDeclarations, sortAnchor=sortAnchor,
                                                                        resetCells=resetCells))
      # default to style as outline if not specified
      if(isOutline && missing(styleAsOutline)) styleAsOutline <- TRUE
      # add the group
      grp <- private$p_rowGroup$addChildGroup(variableName=variableName, filterType=filterType, values=values,
                                              doNotExpand=doNotExpand, isEmpty=isEmpty, isOutline=isOutline, styleAsOutline=styleAsOutline,
                                              captionTemplate=captionTemplate, caption=caption,
                                              isTotal=isTotal, isLevelSubTotal=isLevelSubTotal, isLevelTotal=isLevelTotal,
                                              calculationGroupName=calculationGroupName, calculationName=calculationName,
                                              baseStyleName=baseStyleName, styleDeclarations=styleDeclarations,
                                              insertAtIndex=insertAtIndex, insertBeforeGroup=insertBeforeGroup, insertAfterGroup=insertAfterGroup,
                                              mergeEmptySpace=mergeEmptySpace, cellBaseStyleName=cellBaseStyleName, cellStyleDeclarations=cellStyleDeclarations,
                                              sortAnchor=sortAnchor, resetCells=resetCells)
      if(private$p_argumentCheckMode==TRUE) private$p_traceEnabled("PivotTable$addRowGroup", "Added row group.")
      return(invisible(grp))
    },

    #' @description
    #' Add multiple new data groups to the row group hierarchy
    #' based on the distinct values in a data frame
    #' column or using explicitly specified data values.
    #' See the "Data Groups" vignette for example usage.
    #' @details
    #' There are broadly three different ways to call `addRowDataGroups()`:\cr
    #' (1) dataName=name, fromData=TRUE, onlyCombinationsThatExist=TRUE - which
    #' considers the ancestors of each existing data group to generate only those
    #' combinations of values that exist in the data frame.\cr
    #' (2) dataName=name, fromData=TRUE, onlyCombinationsThatExist=FALSE - which
    #' ignores the ancestors of each existing data group and simply adds every
    #' distinct value of the specified variable under every existing data group,
    #' which can result in combinations of values in the pivot table that don't
    #' exist in the data frame (i.e. blank rows/columns in the pivot table).\cr
    #' (3) fromData=FALSE, explicitListOfValues=list(...) - simply adds every
    #' value from the specified list under every existing data group.
    #' @param variableName The name of the related column in the data frame(s) of
    #'   the pivot table.
    #' @param atLevel The level number that specifies where to add the new
    #' groups.  Level 1 = on the first visible level of the hierarchy.
    #' `NULL` = create a new level at the bottom of the hierarchy for the new
    #' groups.
    #' @param fromData Default `TRUE` to generate the new data groups based on the
    #' data values that exist in the `variableName` column in the named data frame.
    #' If `FALSE`, then `explicitListOfValues` must be specified.
    #' @param dataName The name of the data frame (as specified in
    #' `pt$addData()`) to read the data group values from.
    #' @param dataSortOrder Must be one of "asc", "desc", "custom" or "none".
    #' @param customSortOrder A vector values sorted into the desired order.
    #' @param caption The template of data group captions to generate,
    #' default "{value}".
    #' @param dataFormat A character, list or custom function to format the
    #' data value.
    #' @param dataFmtFuncArgs A list that specifies any additional arguments to
    #' pass to a custom format function.
    #' @param onlyCombinationsThatExist Default `TRUE` to generate only
    #' combinations of data groups that exist in the data frame.
    #' @param explicitListOfValues A list of explicit values to create data
    #' groups from.  A data group is created for each element of the list.
    #' If a list element is vector of values (with length greater than 1),
    #' then a data group is created for multiple values instead of just
    #' a single value.
    #' @param calculationGroupName The calculation group that the new data groups
    #' are related to.
    #' @param expandExistingTotals Default `FALSE`, which means totals are not
    #' broken down in multi-level hierarchies.
    #' @param addTotal Default `TRUE`, which means sub-total and total data groups
    #' are automatically added.
    #' @param visualTotals Default `FALSE`, which means visual totals are disabled.
    #' See the "Data Groups" vignette for more details about visual totals.
    #' @param totalPosition Either "before" or "after" to specify where total groups
    #' are created, default "after".
    #' @param totalCaption The caption to display on total groups, default "Total".
    #' @param onlyAddGroupIf A filter expression that can be used to more finely
    #' control whether data groups are created at different locations in the
    #' hierarchy.  There must be at least one row that matches this filter and the
    #' filters from the ancestor groups in order that the child group is created.
    #' E.g. `MaxDisplayLevel>5`.
    #' @param preGroupData Default `TRUE`, which means that the pivot table
    #' pre-calculates the distinct combinations of variable values to reduce the CPU
    #' time and elapsed time required to generate data groups.
    #' Cannot be used in conjunction with the
    #' @param baseStyleName The name of the style applied to this data group (i.e.
    #'   this row/column heading).  The style must exist in the `PivotStyles` object
    #'   associated with the PivotTable.
    #' @param styleDeclarations CSS style declarations that can override the base
    #' style, expressed as a list, e.g. `list("font-weight"=bold")`.
    #' @param header A character value used as the row-group column caption
    #' when row group headers are rendered.
    #' @param outlineBefore Default `FALSE` to disable the creation of outline header
    #' groups.  Specify either `TRUE` or a list of outline group settings to create
    #' outline header groups.  See the "Regular Layout" vignette for details.
    #' @param outlineAfter Default `FALSE` to disable the creation of outline footer
    #' groups.  Specify either `TRUE` or a list of outline group settings to create
    #' outline footer groups.  See the "Regular Layout" vignette for details.
    #' @param outlineTotal  Default `FALSE` to disable the creation of outline
    #' totals.  Specify either `TRUE` or a list of outline group settings to create
    #' outline totals.  See the "Regular Layout" vignette for details.
    #' @param onlyAddOutlineChildGroupIf A filter expression that can be used to
    #' more finely control whether outline child groups are created at different
    #' locations in the hierarchy.  There must be at least one row that matches
    #' this filter and the filters from the ancestor groups in order that the
    #' outline child group is created.  E.g. `MaxDisplayLevel>5`.
    #' See the "Regular Layout" vignette for an example.
    #' @return A list of new `PivotDataGroup` objects that have been added.
    addRowDataGroups = function(variableName=NULL, atLevel=NULL, fromData=TRUE, # atLevel=1 is the top level, (since 1 is the top level as visible to the user)
                                dataName=NULL, dataSortOrder="asc", customSortOrder=NULL, caption="{value}", dataFormat=NULL, dataFmtFuncArgs=NULL,
                                onlyCombinationsThatExist=TRUE, explicitListOfValues=NULL, calculationGroupName=NULL,
                                expandExistingTotals=FALSE, addTotal=TRUE, visualTotals=FALSE, totalPosition="after", totalCaption="Total",
                                onlyAddGroupIf=NULL, preGroupData=TRUE, baseStyleName=NULL, styleDeclarations=NULL, header=NULL,
                                outlineBefore=NULL, outlineAfter=NULL, outlineTotal=NULL, onlyAddOutlineChildGroupIf=NULL) {
     timeStart <- proc.time()
     if(private$p_argumentCheckMode > 0) {
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", variableName, missing(variableName), allowMissing=FALSE, allowNull=FALSE, allowedClasses="character")
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", atLevel, missing(atLevel), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", fromData, missing(fromData), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", dataName, missing(dataName), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", dataSortOrder, missing(dataSortOrder), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("asc", "desc", "custom", "none"))
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", customSortOrder, missing(customSortOrder), allowMissing=TRUE, allowNull=TRUE, mustBeAtomic=TRUE)
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", caption, missing(caption), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character")
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", dataFormat, missing(dataFormat), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("character", "list", "function"))
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", dataFmtFuncArgs, missing(dataFmtFuncArgs), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list")
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", onlyCombinationsThatExist, missing(onlyCombinationsThatExist), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", explicitListOfValues, missing(explicitListOfValues), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list", listElementsMustBeAtomic=TRUE)
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", calculationGroupName, missing(calculationGroupName), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", expandExistingTotals, missing(expandExistingTotals), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", addTotal, missing(addTotal), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", visualTotals, missing(visualTotals), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", totalPosition, missing(totalPosition), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("before", "after"))
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", totalCaption, missing(totalCaption), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character")
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", onlyAddGroupIf, missing(onlyAddGroupIf), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", preGroupData, missing(preGroupData), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", baseStyleName, missing(baseStyleName), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", styleDeclarations, missing(styleDeclarations), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list", allowedListElementClasses=c("character", "integer", "numeric"))
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", header, missing(header), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", outlineBefore, missing(outlineBefore), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("logical", "list"))
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", outlineAfter, missing(outlineAfter), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("logical", "list"))
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", outlineTotal, missing(outlineTotal), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("logical", "list"))
       checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowDataGroups", onlyAddOutlineChildGroupIf, missing(onlyAddOutlineChildGroupIf), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("logical","character"))
     }
     if(private$p_traceEnabled==TRUE) self$trace("PivotTable$addRowDataGroups", "Adding row groups...",
                   list(variableName=variableName, atLevel=atLevel, fromData=fromData,
                   dataName=dataName, dataSortOrder=dataSortOrder, customSortOrder=customSortOrder,
                   caption=caption, dataFormat=dataFormat, dataFmtFuncArgs=dataFmtFuncArgs,
                   onlyCombinationsThatExist=onlyCombinationsThatExist, explicitListOfValues=explicitListOfValues,
                   calculationGroupName=calculationGroupName, expandExistingTotals=expandExistingTotals,
                   addTotal=addTotal, visualTotals=visualTotals, totalPosition=totalPosition, totalCaption=totalCaption,
                   onlyAddGroupIf=onlyAddGroupIf, preGroupData=preGroupData,
                   baseStyleName=baseStyleName, styleDeclarations=styleDeclarations), header=header,
                   outlineBefore=outlineBefore, outlineAfter=outlineAfter, outlineTotal=outlineTotal,
                   onlyAddOutlineChildGroupIf=onlyAddOutlineChildGroupIf)
     if((!is.null(styleDeclarations))&&(length(styleDeclarations)!=length(names(styleDeclarations))))
       stop("PivotTable$addRowDataGroups(): One or more style declarations are missing a name.", call. = FALSE)
      levelsBelow <- NULL
      if((!is.null(atLevel))&&(atLevel>0)) levelsBelow <- atLevel - 1
      # helper function:  if outline==TRUE, then use the default outline setting if we have one
      getDefaultOutline <- function(outline, outlineDefault) {
        if((isTRUE(outline))&&(!is.null(outlineDefault))) return(invisible(outlineDefault))
        return(invisible(outline))
      }
      # add the groups
      grps <- private$p_rowGroup$addDataGroups(variableName=variableName, atLevel=levelsBelow, fromData=fromData,
                                               dataName=dataName, dataSortOrder=dataSortOrder, customSortOrder=customSortOrder,
                                               caption=caption, dataFormat=dataFormat, dataFmtFuncArgs=dataFmtFuncArgs,
                                               onlyCombinationsThatExist=onlyCombinationsThatExist, explicitListOfValues=explicitListOfValues,
                                               calculationGroupName=calculationGroupName,
                                               expandExistingTotals=self$getDefault1(expandExistingTotals, missing(expandExistingTotals)),
                                               addTotal=self$getDefault1(addTotal, missing(addTotal)),
                                               visualTotals=self$getDefault1(visualTotals, missing(visualTotals)),
                                               totalPosition=self$getDefault1(totalPosition, missing(totalPosition)),
                                               totalCaption=self$getDefault1(totalCaption, missing(totalCaption)),
                                               onlyAddGroupIf=onlyAddGroupIf, preGroupData=preGroupData, baseStyleName=baseStyleName, styleDeclarations=styleDeclarations,
                                               outlineBefore=getDefaultOutline(outlineBefore, self$getDefault3("outlineBefore")),
                                               outlineAfter=getDefaultOutline(outlineAfter, self$getDefault3("outlineAfter")),
                                               outlineTotal=getDefaultOutline(outlineTotal, self$getDefault3("outlineTotal")),
                                               onlyAddOutlineChildGroupIf=onlyAddOutlineChildGroupIf)
      if(is.null(header)) {
        header <- variableName
      }
      if (!is.null(header)) {
        if (length(grps)>0) {
          levelNumber <- grps[[1]]$levelNumber
          private$p_rowGrpHeaders[[levelNumber]] <- header
        }
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$addRowDataGroups", "Added row groups.")
      private$addTiming(paste0("addRowDataGroups(", variableName, ")"), timeStart)
      return(invisible(grps))
    },

    #' @description
    #' Normalise the row data group hierarchy so that all branches have the
    #' same number of levels - accomplished by adding empty child data groups
    #' where needed.
    #' @param resetCells Default `TRUE` to reset any cells that currently exist
    #' in the pivot table and trigger a recalculation of the pivot table when
    #' it is next rendered.
    #' @return A list of new `PivotDataGroup` objects that have been added.
    normaliseRowGroups = function(resetCells=TRUE) {
      timeStart <- proc.time()
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$normaliseRowGroups", "Normalising row groups...")
      self$resetCells()
      groupsAdded <- private$p_rowGroup$normaliseDataGroup(resetCells=resetCells)
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$normaliseRowGroups", "Normalised row groups.", list(groupsAdded = groupsAdded))
      private$addTiming("normaliseRowGroups", timeStart)
      return(invisible())
    },

    #' @description
    #' Sort row data groups either by the data group data value, caption,
    #' a custom order or based on calculation result values.
    #' @param levelNumber The level number to sort the data groups, e.g.
    #' level 1 (default) sorts the data groups at level 1 of the hierarchy
    #' (which is the first visible level of data groups).
    #' @param orderBy Must be either "value", "caption", "calculation",
    #' "customByValue" or "customByCaption".\cr
    #' "value" sorts by the raw (i.e. unformatted) group value.\cr
    #' "caption" sorts by the formatted character group caption.\cr
    #' "calculation" sorts using one of the calculations defined in the pivot table.
    #' "customValue" sorts by the raw (i.e. unformatted) group value according to
    #' the specified custom sort order.\cr
    #' "customCaption" sorts by the formatted character group caption according to
    #' the specified custom sort order.
    #' @param customOrder A vector values sorted into the desired order.
    #' @param sortOrder Must be either "asc" or "desc".
    #' @param calculationGroupName If sorting using a calculation, the name of the
    #' calculation group containing the specified calculation.
    #' @param calculationName If sorting using a calculation, the name of the
    #' calculation.
    #' @param fromIndex A boundary to limit the sort operation.
    #' @param toIndex A boundary to limit the sort operation.
    #' @param resetCells Default `TRUE` to reset any cells that currently exist
    #' in the pivot table and trigger a recalculation of the pivot table when
    #' it is next rendered.
    #' @return No return value.
    sortRowDataGroups = function(levelNumber=1, orderBy="calculation", customOrder=NULL, sortOrder="desc",
                                 calculationGroupName="default", calculationName=NULL,
                                 fromIndex=NULL, toIndex=NULL, resetCells=TRUE) {
      timeStart <- proc.time()
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "sortRowDataGroups", levelNumber, missing(levelNumber), allowMissing=TRUE, allowNull=FALSE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "sortRowDataGroups", orderBy, missing(orderBy), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("value","caption","calculation","customByValue","customByCaption"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "sortRowDataGroups", customOrder, missing(customOrder), allowMissing=TRUE, allowNull=TRUE, mustBeAtomic=TRUE)
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "sortRowDataGroups", sortOrder, missing(sortOrder), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("asc","desc"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "sortRowDataGroups", calculationGroupName, missing(calculationGroupName), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "sortRowDataGroups", calculationName, missing(calculationName), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "sortRowDataGroups", fromIndex, missing(fromIndex), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "sortRowDataGroups", toIndex, missing(toIndex), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "sortRowDataGroups", resetCells, missing(resetCells), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$sortRowDataGroups", "Sorting row data groups...",
                    list(levelNumber=levelNumber, orderBy=orderBy, customOrder=customOrder, sortOrder=sortOrder,
                         calculationGroupName=calculationGroupName, calculationName=calculationName,
                         fromIndex=fromIndex, toIndex=toIndex, resetCells=resetCells))
      if(levelNumber<1) stop("PivotTable$sortRowDataGroups():  levelNumber must be 1 or above.", call. = FALSE)
      private$p_rowGroup$sortDataGroups(levelNumber=levelNumber-1, orderBy=orderBy, customOrder=customOrder, sortOrder=sortOrder,
                                           calculationGroupName=calculationGroupName, calculationName=calculationName,
                                        fromIndex=fromIndex, toIndex=toIndex, resetCells=resetCells)
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$sortRowDataGroups", "Sorted row data groups.")
      private$addTiming("sortRowDataGroups", timeStart)
      return(invisible())
    },

    #' @description
    #' Set the row group header associated with a level of the row data group
    #' hierarchy.
    #' @param levelNumber An integer specifying the level number.
    #' @param header A character value specifying the caption.
    #' @details
    #' By default, the row data groups (i.e. row headings) at the left of the pivot
    #' table have no column headings.  This method can specify the headings, which
    #' can be rendered by specifying the `showRowGroupHeaders=TRUE` in the render
    #' methods.
    #' @return No return value.
    setRowDataGroupHeader = function(levelNumber=NULL, header=NULL) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "setRowDataGroupHeader", levelNumber, missing(levelNumber), allowMissing=FALSE, allowNull=FALSE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "setRowDataGroupHeader", header, missing(header), allowMissing=FALSE, allowNull=FALSE, allowedClasses="character")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$setRowDataGroupHeader", "Setting row group header...")
      private$p_rowGrpHeaders[[levelNumber]] <- header
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$setRowDataGroupHeader", "Set row group header.")
    },

    #' @description
    #' Create a new calculation group.  This is rarely needed since the
    #' default group is sufficient for all regular pivot tables.
    #' @param calculationGroupName The name of the new calculation group to create.
    #' @return A `PivotCalculationGroup` object.
    addCalculationGroup = function(calculationGroupName=NULL) {
      timeStart <- proc.time()
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addCalculationGroup", calculationGroupName, missing(calculationGroupName), allowMissing=FALSE, allowNull=FALSE, allowedClasses="character")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$addCalculationGroup", "Adding calculation group...", list(calculationGroupName=calculationGroupName))
      if(private$p_calculationsSet) stop("PivotTable$defineCalculation(): Calculations cannot be moved/added after either addColumnCalculationGroups() or addRowCalculationGroups() has been executed.", call. = FALSE)
      self$resetCells()
      calculationGroup <- private$p_calculationGroups$addCalculationGroup(calculationGroupName)
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$addCalculationGroup", "Added calculation group.")
      private$addTiming("addCalculationGroup", timeStart)
      return(invisible(calculationGroup))
    },

    #' @description
    #' Create a new `PivotCalculation` object.
    #' @param calculationGroupName The name of the calculation group this
    #' calculation will belong to.  The default calculation group will be
    #' used if this parameter is not specified (this is sufficient for all
    #' regular pivot tables).
    #' @param calculationName Calculation unique name.
    #' @param caption Calculation display name
    #' @param visible `TRUE` to show the calculation in the pivot table or `FALSE`
    #' to hide it.  Hidden calculations are typically used as base values for
    #' other calculations.
    #' @param displayOrder The order the calculations are displayed in the
    #' pivot table.
    #' @param filters Any additional data filters specific to this calculation.
    #' This can be a `PivotFilters` object that further restricts the data for the
    #' calculation or a list of individual `PivotFilter` objects that provide more
    #' flexibility (and/or/replace).  See the Calculations vignette for details.
    #' @param format A character, list or custom function to format the calculation
    #' result.
    #' @param fmtFuncArgs A list that specifies any additional arguments to pass to
    #' a custom format function.
    #' @param dataName Specifies which data frame in the pivot table is used for
    #' this calculation (as specified in `pt$addData()`).
    #' @param type The calculation type:  "summary", "calculation", "function" or
    #' "value".
    #' @param valueName For type="value", the name of the column containing the
    #' value to display in the pivot table.
    #' @param summariseExpression For type="summary", either the dplyr expression to
    #' use with dplyr::summarise() or a data.table calculation expression.
    #' @param calculationExpression For type="calculation", an expression to combine
    #' aggregate values.
    #' @param calculationFunction For type="function", a reference to a custom R
    #' function that will carry out the calculation.
    #' @param calcFuncArgs For type="function", a list that specifies additional
    #' arguments to pass to calculationFunction.
    #' @param basedOn A character vector specifying the names of one or more
    #' calculations that this calculation depends on.
    #' @param noDataValue An integer or numeric value specifying the value to use if
    #' no data exists for a particular cell.
    #' @param noDataCaption A character value that will be displayed by the pivot
    #' table if no  data exists for a particular cell.
    #' @param headingBaseStyleName The name of a style defined in the pivot table
    #' to use as the base styling for the data group heading.
    #' @param headingStyleDeclarations A list of CSS style declarations (e.g.
    #' `list("font-weight"="bold")`) to override the base style.
    #' @param cellBaseStyleName The name of a style defined in the pivot table to
    #' use as the base styling for the cells related to this calculation.
    #' @param cellStyleDeclarations A list of CSS style declarations (e.g.
    #' `list("font-weight"="bold")`) to override the base style.
    #' @param resetCells Default `TRUE` to reset any cells that currently exist
    #' in the pivot table and trigger a recalculation of the pivot table when
    #' it is next rendered.
    #' @return A new `PivotCalculation` object.
    defineCalculation = function(calculationGroupName="default", calculationName=NULL, caption=NULL, visible=TRUE, displayOrder=NULL,
                         filters=NULL, format=NULL, fmtFuncArgs=NULL, dataName=NULL, type="summary",
                         valueName=NULL, summariseExpression=NULL, calculationExpression=NULL, calculationFunction=NULL, calcFuncArgs=NULL,
                         basedOn=NULL, noDataValue=NULL, noDataCaption=NULL,
                         headingBaseStyleName=NULL, headingStyleDeclarations=NULL, cellBaseStyleName=NULL, cellStyleDeclarations=NULL,
                         resetCells=TRUE) {
      timeStart <- proc.time()
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "defineCalculation", calculationGroupName, missing(calculationGroupName), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "defineCalculation", calculationName, missing(calculationName), allowMissing=FALSE, allowNull=FALSE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "defineCalculation", caption, missing(caption), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "defineCalculation", visible, missing(visible), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "defineCalculation", displayOrder, missing(displayOrder), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "defineCalculation", filters, missing(filters), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("PivotFilters", "PivotFilterOverrides"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "defineCalculation", format, missing(format), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("function", "list", "character"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "defineCalculation", fmtFuncArgs, missing(fmtFuncArgs), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "defineCalculation", dataName, missing(dataName), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "defineCalculation", type, missing(type), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("value", "summary", "calculation", "function"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "defineCalculation", valueName, missing(valueName), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "defineCalculation", summariseExpression, missing(summariseExpression), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "defineCalculation", calculationExpression, missing(calculationExpression), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "defineCalculation", calculationFunction, missing(calculationFunction), allowMissing=TRUE, allowNull=TRUE, allowedClasses="function")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "defineCalculation", calcFuncArgs, missing(calcFuncArgs), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "defineCalculation", basedOn, missing(basedOn), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "defineCalculation", noDataValue, missing(noDataValue), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer","numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "defineCalculation", noDataCaption, missing(noDataCaption), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "defineCalculation", headingBaseStyleName, missing(headingBaseStyleName), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "defineCalculation", headingStyleDeclarations, missing(headingStyleDeclarations), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list", allowedListElementClasses=c("character", "integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "defineCalculation", cellBaseStyleName, missing(cellBaseStyleName), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "defineCalculation", cellStyleDeclarations, missing(cellStyleDeclarations), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list", allowedListElementClasses=c("character", "integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "defineCalculation", resetCells, missing(resetCells), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
      }
      if(private$p_calculationsSet) stop("PivotTable$defineCalculation(): Calculations cannot be moved/added after either addColumnCalculationGroups() or addRowCalculationGroups() has been executed.", call. = FALSE)
      fstr <- NULL
      if(!is.null(filters)) fstr <- filters$asString()
      if((!is.null(headingStyleDeclarations))&&(length(headingStyleDeclarations)!=length(names(headingStyleDeclarations))))
        stop("PivotTable$defineCalculation(): One or more heading style declarations are missing a name.", call. = FALSE)
      if((!is.null(cellStyleDeclarations))&&(length(cellStyleDeclarations)!=length(names(cellStyleDeclarations))))
        stop("PivotTable$defineCalculation(): One or more cell style declarations are missing a name.", call. = FALSE)
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$defineCalculation", "Defining calculation...",
                   list(calculationGroupName=calculationGroupName, calculationName=calculationName, caption=caption,
                        visible=visible, displayOrder=displayOrder, filters=fstr, format=format, fmtFuncArgs=fmtFuncArgs,
                        dataName=dataName, type=type, valueName=valueName, summariseExpression=summariseExpression,
                        calculationExpression=calculationExpression, calculationFunction=calculationFunction, calcFuncArgs=calcFuncArgs,
                        basedOn=basedOn, noDataValue=noDataValue, noDataCaption=noDataCaption,
                        headingBaseStyleName=headingBaseStyleName, headingStyleDeclarations=headingStyleDeclarations,
                        cellBaseStyleName=cellBaseStyleName, cellStyleDeclarations=cellStyleDeclarations, resetCells=resetCells))
      if(resetCells) self$resetCells()
      calculationGroupExists <- private$p_calculationGroups$isExistingCalculationGroup(calculationGroupName)
      if(calculationGroupExists) {
        calculationGroup <- private$p_calculationGroups$getCalculationGroup(calculationGroupName)
      }
      else {
        calculationGroup <- private$p_calculationGroups$addCalculationGroup(calculationGroupName)
      }
      calculation <- calculationGroup$defineCalculation(calculationName=calculationName, caption=caption, visible=visible,
                         displayOrder=displayOrder, filters=filters, format=format, fmtFuncArgs=fmtFuncArgs, dataName=dataName,
                         type=type, valueName=valueName, summariseExpression=summariseExpression,
                         calculationExpression=calculationExpression, calculationFunction=calculationFunction, calcFuncArgs=calcFuncArgs,
                         basedOn=basedOn, noDataValue=noDataValue, noDataCaption=noDataCaption,
                         headingBaseStyleName=headingBaseStyleName, headingStyleDeclarations=headingStyleDeclarations,
                         cellBaseStyleName=cellBaseStyleName, cellStyleDeclarations=cellStyleDeclarations)
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$defineCalculation", "Defined calculation.")
      private$addTiming(paste0("defineCalculation(", calculationGroupName, ":", calculationName, ")"), timeStart)
      return(invisible(calculation))
    },

    #' @description
    #' Set calculations on existing data groups or add multiple new groups
    #' to the column data group hierarchy to represent calculations.
    #' @details
    #' If only one calculation is defined in the pivot table, then the calculation
    #' is set onto the existing column data groups (and no new groups are generated).
    #' If multiple calculations are defined, then a new level of data groups is
    #' added, e.g. if two calculations are defined, then two new data groups will
    #' be created under each existing leaf-level column data group.
    #' @param calculationGroupName The name of the calculation group to add
    #' into the data group hierarchy.
    #' @param atLevel The level number that specifies where to add the new
    #' groups.  Level 1 = on the first visible level of the hierarchy.
    #' `NULL` = create a new level at the bottom of the hierarchy for the new
    #' groups.
    #' @return A list of new `PivotDataGroup` objects that have been added.
    addColumnCalculationGroups = function(calculationGroupName="default", atLevel=NULL) { # atLevel=1 is the top level, (since 1 is the top level as visible to the user)
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnCalculationGroups", calculationGroupName, missing(calculationGroupName), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addColumnCalculationGroups", atLevel, missing(atLevel), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$addColumnCalculationGroups", "Adding column calculation groups...",
                   list(calculationGroupName=calculationGroupName, atLevel=atLevel))
      if(private$p_calculationsSet) stop("PivotTable$addColumnCalculationGroups(): Calculations cannot be moved/added after either addColumnCalculationGroups() or addRowCalculationGroups() has been executed.", call. = FALSE)
      self$resetCells()
      levelsBelow <- NULL
      if((!is.null(atLevel))&&(atLevel>0)) levelsBelow <- atLevel - 1
      grps <- private$p_columnGroup$addCalculationGroups(calculationGroupName=calculationGroupName, atLevel=levelsBelow)
      private$p_calculationsSet <- TRUE
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$addColumnCalculationGroups", "Added column calculation groups.")
      return(invisible(grps))
    },

    #' @description
    #' Set calculations on existing data groups or add multiple new groups
    #' to the row data group hierarchy to represent calculations.
    #' @details
    #' If only one calculation is defined in the pivot table, then the calculation
    #' is set onto the existing row data groups (and no new groups are generated).
    #' If multiple calculations are defined, then a new level of data groups is
    #' added, e.g. if two calculations are defined, then two new data groups will
    #' be created under each existing leaf-level row data group.
    #' @param calculationGroupName The name of the calculation group to add
    #' into the data group hierarchy.
    #' @param atLevel The level number that specifies where to add the new
    #' groups.  Level 1 = on the first visible level of the hierarchy.
    #' `NULL` = create a new level at the bottom of the hierarchy for the new
    #' groups.
    #' @param outlineBefore Default `FALSE` to disable the creation of outline header
    #' groups.  Specify either `TRUE` or a list of outline group settings to create
    #' outline header groups.  See the "Regular Layout" vignette for details.
    #' @param outlineAfter Default `FALSE` to disable the creation of outline footer
    #' groups.  Specify either `TRUE` or a list of outline group settings to create
    #' outline footer groups.  See the "Regular Layout" vignette for details.
    #' @return A list of new `PivotDataGroup` objects that have been added.
    addRowCalculationGroups = function(calculationGroupName="default", atLevel=NULL, outlineBefore=NULL, outlineAfter=NULL) { # atLevel=1 is the top level, (since 1 is the top level as visible to the user)
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowCalculationGroups", calculationGroupName, missing(calculationGroupName), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowCalculationGroups", atLevel, missing(atLevel), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowCalculationGroups", outlineBefore, missing(outlineBefore), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("logical", "list"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addRowCalculationGroups", outlineAfter, missing(outlineAfter), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("logical", "list"))
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$addRowCalculationGroups", "Adding row calculation groups...",
                   list(calculationGroupName=calculationGroupName, atLevel=atLevel, outlineBefore=outlineBefore, outlineAfter=outlineAfter))
      if(private$p_calculationsSet) stop("PivotTable$addRowCalculationGroups(): Calculations cannot be moved/added after either addColumnCalculationGroups() or addRowCalculationGroups() has been executed.", call. = FALSE)
      self$resetCells()
      levelsBelow <- NULL
      if((!is.null(atLevel))&&(atLevel>0)) levelsBelow <- atLevel - 1
      # helper function:  if outline==TRUE, then use the default outline setting if we have one
      getDefaultOutline <- function(outline, outlineDefault) {
        if((isTRUE(outline))&&(!is.null(outlineDefault))) return(invisible(outlineDefault))
        return(invisible(outline))
      }
      # add groups
      grps <- private$p_rowGroup$addCalculationGroups(calculationGroupName=calculationGroupName, atLevel=levelsBelow,
                                                      outlineBefore=getDefaultOutline(outlineBefore, self$getDefault3("outlineBefore")),
                                                      outlineAfter=getDefaultOutline(outlineAfter, self$getDefault3("outlineAfter")))
      private$p_calculationsSet <- TRUE
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$addRowCalculationGroups", "Added row calculation groups.")
      return(invisible(grps))
    },

    #' @description
    #' Add a new named style to the pivot table.
    #' @param styleName The name of the new style.
    #' @param declarations CSS style declarations in the form of a list, e.g.
    #' `list("font-weight"="bold", "color"="#0000FF")`
    #' @return The newly created `PivotStyle` object.
    addStyle = function(styleName=NULL, declarations=NULL) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addStyle", styleName, missing(styleName), allowMissing=FALSE, allowNull=FALSE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "addStyle", declarations, missing(declarations), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list", allowedListElementClasses="character")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$addStyle", "Adding style...", list(styleName=styleName))
      style <- private$p_styles$addStyle(styleName=styleName, declarations=declarations)
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$addStyle", "Added style.")
      return(invisible(style))
    },

    #' @description
    #' Create an inline style that can be used to override a base style.
    #' For general use cases, the `setStyling()` method provides a simpler
    #' and more direct way of styling specific parts of a pivot table.
    #' @details
    #' Inline styles are typically used to override the style of some specific
    #' cells in a pivot table.  Inline styles have no name.
    #' In HTML, they are rendered as 'style' attributes on specific table cells,
    #' where as named styles are linked to cells using the 'class' attribute.
    #' @param baseStyleName The name of an existing style to base the new style on.
    #' @param declarations CSS style declarations in the form of a list, e.g.
    #' `list("font-weight"="bold", "color"="#0000FF")`
    #' @return The newly created `PivotStyle` object.
    createInlineStyle = function(baseStyleName=NULL, declarations=NULL) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "createInlineStyle", declarations, missing(declarations), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list", allowedListElementClasses="character")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$createInlineStyle", "Creating inline style...")
      if(is.null(baseStyleName)) {
        style <- PivotStyle$new(parentPivot=self, styleName="", declarations=declarations)
      }
      else {
        baseStyle <- private$p_styles$getStyle(styleName=baseStyleName)
        style <- PivotStyle$new(parentPivot=self, styleName="", declarations=baseStyle$declarations)
        style$setPropertyValues(declarations=declarations)
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$createInlineStyle", "Created inline style.")
      return(invisible(style))
    },

    #' @description
    #' Apply styling to a set of data groups or cells in a pivot table.
    #' @details
    #' There are five ways to specify the part(s) of a pivot table to apply
    #' styling to:\cr
    #' (1) By specifying a list of data groups using the `groups` argument.\cr
    #' (2) By specifying a list of cells using the `cells` argument.\cr
    #' (3) By specifying a single cell using the `rFrom` and `cFrom` arguments.\cr
    #' (4) By specifying a rectangular cell range using the `rFrom`, `cFrom`,
    #' `rTo` and `cTo` arguments.\cr
    #' (5) By specifying a vector of rowNumbers and/or columnNumbers.  If both
    #' rowNumbers and columnNumbers are specified, then the cells at the
    #' intersection of the specified row numbers and column numbers are styled.\cr
    #' If both rFrom/rTo and rowNumbers are specified, then rFrom/rTo constrain
    #' the row numbers specified in rowNumbers.\cr
    #' If both cFrom/cTo and columnNumbers are specified, then cFrom/cTo constrain
    #' the column numbers specified in columnNumbers.\cr
    #' See the "Styling" and "Finding and Formatting" vignettes for more
    #' information and many examples.
    #' @param rFrom An integer row number that specifies the start row for the
    #' styling changes.
    #' @param cFrom An integer column number that specifies the start column for the
    #' styling changes.
    #' @param rTo An integer row number that specifies the end row for the styling
    #' changes.
    #' @param cTo An integer column number that specifies the end column for the
    #' styling changes.
    #' @param rowNumbers An integer vector that specifies the row numbers for the
    #' styling changes.
    #' @param columnNumbers An integer vector that specifies the column numbers for
    #' the styling changes.
    #' @param groups A list containing `PivotDataGroup` objects.
    #' @param cells A list containing `PivotCell` objects.
    #' @param baseStyleName The name of a style to apply.
    #' @param style A `PivotStyle` object to apply.
    #' @param declarations CSS style declarations to apply in the form of a list,
    #' e.g. `list("font-weight"="bold", "color"="#0000FF")`
    #' @param applyBorderToAdjacentCells TRUE to override the border in
    #' neighbouring cells, e.g. the left border of the current cell becomes the
    #' right border of the cell to the left.  Does not apply to row/column groups.
    #' @return No return value.
    setStyling = function(rFrom=NULL, cFrom=NULL, rTo=NULL, cTo=NULL, rowNumbers=NULL, columnNumbers=NULL,
                          groups=NULL, cells=NULL, baseStyleName=NULL, style=NULL, declarations=NULL, applyBorderToAdjacentCells=FALSE) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "setStyling", rFrom, missing(rFrom), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "setStyling", cFrom, missing(cFrom), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "setStyling", rTo, missing(rTo), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "setStyling", cTo, missing(cTo), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "setStyling", rowNumbers, missing(rowNumbers), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "setStyling", columnNumbers, missing(columnNumbers), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "setStyling", groups, missing(groups), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("list", "PivotDataGroup"), allowedListElementClasses="PivotDataGroup")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "setStyling", cells, missing(cells), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("list", "PivotCell"), allowedListElementClasses="PivotCell")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "setStyling", baseStyleName, missing(baseStyleName), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "setStyling", style, missing(style), allowMissing=TRUE, allowNull=TRUE, allowedClasses="PivotStyle")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "setStyling", declarations, missing(declarations), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list", allowedListElementClasses=c("character", "integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "setStyling", applyBorderToAdjacentCells, missing(applyBorderToAdjacentCells), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$setStyling", "Setting styling...")
      if(missing(baseStyleName)&&missing(style)&&missing(declarations)) { stop("PivotTable$setStyling():  Please specify at least one of baseStyleName, style or declarations.", call. = FALSE) }
      # style a group or list of groups
      if(!is.null(groups)) {
        if("PivotDataGroup" %in% class(groups)) {
          groups <- list(groups)
        }
        if(length(groups)>0) {
          for(i in 1:length(groups)) {
            grp <- groups[[i]]
            if(!is.null(grp)) {
              if(!missing(baseStyleName)) { grp$baseStyleName <- baseStyleName }
              if(!missing(style)) { grp$style <- ifelse(is.null(style), NULL, style$getCopy()) }
              if((!missing(declarations))&&(!is.null(declarations))) {
                if (is.null(grp$style)) { grp$style <- PivotStyle$new(parentPivot=self, declarations=declarations) }
                else { grp$style$setPropertyValues(declarations) }
                # applyBorderToAdjacentCells does not apply to groups, because groups are rendered as merged cells, and CSS cannot support border rendering for some merge cell combinations
                # e.g. consider a 2nd level row group (i.e. has a parent row group to the left).  The parent row group specifies a border - and because this is encountered first
                # when rendering in the browser it is not possible to override the border it specifies.
              }
            }
          }
        }
      }
      # style a cell or list of cells
      if(!is.null(cells)) {
        if("PivotCell" %in% class(cells)) {
          cells <- list(cells)
        }
        if(length(cells)>0) {
          for(i in 1:length(cells)) {
            cell <- cells[[i]]
            if(!is.null(cell)) {
              if(!missing(baseStyleName)) { cell$baseStyleName <- baseStyleName }
              if(!missing(style)) { cell$style <- ifelse(is.null(style), NULL, style$getCopy()) }
              if((!missing(declarations))&&(!is.null(declarations))) {
                if (is.null(cell$style)) { cell$style <- PivotStyle$new(parentPivot=self, declarations=declarations) }
                else { cell$style$setPropertyValues(declarations) }
                if (isTRUE(applyBorderToAdjacentCells)) { private$applyBorderToAdjacentCells(cell=cell, declarations=declarations) }
              }
            }
          }
        }
      }
      # styling cells by coordinates...
      styleCells <- FALSE
      rowCount <- self$rowCount
      columnCount <- self$columnCount
      ## switch to use the option legacy coordinates (this ignores the rowNumbers and columnNumbers parameters)
      if(isTRUE(private$p_compatibility$legacySetStylingRowColumnNumbers)) {
        if((!is.null(rFrom))&&(!is.null(cFrom))) {
          if(is.null(rTo)) rTo <- rFrom
          if(is.null(cTo)) cTo <- cFrom
          if(rTo<rFrom) { stop("PivotTable$setStyling():  rTo must be greater than or equal to rFrom.", call. = FALSE) }
          if(cTo<cFrom) { stop("PivotTable$setStyling():  cTo must be greater than or equal to cFrom.", call. = FALSE) }
          rowNumbers <- rFrom:rTo
          columnNumbers <- cFrom:cTo
          styleCells <- TRUE
        }
      }
      else {
        ## check if a single cell has been specified
        if((length(rowNumbers)==0)&&(length(columnNumbers)==0)&&
           (!is.null(rFrom))&&(!is.null(cFrom))&&(is.null(rTo))&&(is.null(cTo))) {
          rowNumbers <- rFrom
          columnNumbers <- cFrom
        }
        else
        {
          # specifying a range of cells
          if((length(rowNumbers)>0)||(!is.null(rFrom))||(!is.null(rTo))) {
            if(length(rowNumbers)==0) rowNumbers <- 1:max(rowCount, 1)
            if(!is.null(rFrom)) rowNumbers <- rowNumbers[rowNumbers >= min(rFrom)]
            if(!is.null(rTo)) rowNumbers <- rowNumbers[rowNumbers <= max(rTo)]
          }
          if((length(columnNumbers)>0)||(!is.null(cFrom))||(!is.null(cTo))) {
            if(length(columnNumbers)==0) columnNumbers <- 1:max(columnCount, 1)
            if(!is.null(cFrom)) columnNumbers <- columnNumbers[columnNumbers >= min(cFrom)]
            if(!is.null(cTo)) columnNumbers <- columnNumbers[columnNumbers <= max(cTo)]
          }
        }
        styleCells <- (length(rowNumbers)>0)||(length(columnNumbers)>0)
      }
      if(styleCells==TRUE) {
        if(!private$p_evaluated) stop("PivotTable$setStyling():  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
        if(is.null(private$p_cells)) stop("PivotTable$setStyling():  No cells exist in the pivot table.", call. = FALSE)
        # set defaults for other axes
        if((length(rowNumbers)==0)&&(length(columnNumbers)>0)) rowNumbers <- 1:rowCount
        if((length(rowNumbers)>0)&&(length(columnNumbers)==0)) columnNumbers <- 1:columnCount
        # silently remove invalid row/column numbers
        if(min(rowNumbers)<1) rowNumbers <- rowNumbers[rowNumbers >= 1]
        if(max(rowNumbers)>rowCount) rowNumbers <- rowNumbers[rowNumbers <= rowCount]
        if(min(columnNumbers)<1) columnNumbers <- columnNumbers[columnNumbers >= 1]
        if(max(columnNumbers)>columnCount) columnNumbers <- columnNumbers[columnNumbers <= columnCount]
        # style cells
        for(r in rowNumbers) {
          for(c in columnNumbers) {
            cell <- self$cells$getCell(r, c)
            if(!is.null(cell)) {
              if(!missing(baseStyleName)) { cell$baseStyleName <- baseStyleName }
              if(!missing(style)) { cell$style <- ifelse(is.null(style), NULL, style$getCopy()) }
              if((!missing(declarations))&&(!is.null(declarations))) {
                if (is.null(cell$style)) { cell$style <- PivotStyle$new(parentPivot=self, declarations=declarations) }
                else { cell$style$setPropertyValues(declarations) }
                if (isTRUE(applyBorderToAdjacentCells)) { private$applyBorderToAdjacentCells(cell=cell, declarations=declarations) }
              }
            }
          }
        }
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$setStyling", "Set styling.")
    },

    # mapType=value only allows fixed "from" values (either just a number or v==number): it maps discrete "from" values to discrete "to" values
    # mapType=range allows "from" as a number or as a range specified with "v": it matches values into "from" ranges, the "to" values are discrete
    # mapType=continuous only allows fixed "from" values (just a number or v==number): it rescales "from" numbers into continuous "to" values
    # allowed usage:
    # valueType=text, mapType=value:        0, "normal", 1, "bold", etc.
    # valueType=text, mapType=logic:        v==0, "normal", 1<v<=2, "bold", etc.
    # valueType=text, mapType=range:        0, "normal", 1, "bold", etc                styleLowerValues=FALSE, styleHigherValues=TRUE
    # valueType=number: supports the above, plus:
    # valueType=number, mapType=continuous: 0, 10, 1, 20, etc                          styleLowerValues=FALSE, styleHigherValues=TRUE
    # valueType=color:  supports the above, but the continuous option looks like:
    # valueType=color, mapType=continuous:  0, red, 1, yellow, etc                     styleLowerValues=FALSE, styleHigherValues=TRUE
    # Note in documentation that these methods are primarily for numerical data.  Parts may work for dates (e.g. mapType=value and range) ...
    # ... but other bits won't, e.g. v>=as.Date("2020-05-22"), or variables, e.g. v>=x where x is a local variable outside of the pivot table (but the name could collide with a local variable inside the pivot table function).

    #' @description
    #' Apply styling to pivot table cells based on the value of each cell.
    #' @details
    #' `mapStyling()` is typically used to conditionally apply styling to cells
    #' based on the value of each individual cell, e.g. cells with values less
    #' than a specified number could be coloured red.\cr
    #' mapType="logic" maps values matching specified logical criteria to
    #' specific "to" values.  The logical criteria can be any of the following
    #' forms (the first matching mapping is used):\cr
    #' (1) a specific value, e.g. 12.\cr
    #' (2) a specific value equality condition, e.g. "v==12", where v
    #' represents the cell value.\cr
    #' (3) a value range expression using the following abbreviated form:
    #' "value1<=v<value2", e.g. "10<=v<15".  Only "<" or "<=" can be used
    #' in these value range expressions.\cr
    #' (4) a standard R logical expression, e.g.
    #' "10<=v && v<15".\cr
    #' Basic R functions that test the value can also be
    #' used, e.g. is.na(v).\cr
    #' See the "Styling" and Finding and Formatting" vignettes for more
    #' information and many examples.
    #' @param styleProperty The name of the style property to set on the specified
    #' cells, e.g. background-color.
    #' @param cells A list containing `PivotCell` objects.
    #' @param valueType The type of style value to be set.  Must be one of:
    #' "text", "character", "number", "numeric", "color" or "colour".\cr
    #' "text" and "character" are equivalent.  "number" and "numeric" are equivalent.
    #' "color" and "colour" are equivalent.
    #' @param mapType The type of mapping to be performed.  The following mapping
    #' types are supported:\cr
    #' (1) "value" = a 1:1 mapping which maps each specified "from" value to the
    #' corresponding "to" value, e.g. 100 -> "green".\cr
    #' (2) "logic" = each from value is logical criteria.  See details.\cr
    #' (3) "range" = values between each pair of "from" values are mapped to the
    #' corresponding "to" value, e.g. values in the range 80-100 -> "green" (more
    #' specifically values greater than or equal to 80 and less than 100).\cr
    #' (4) "continuous" = rescales values between each pair of "from" values into
    #' the range of the corresponding pair of "to" values, e.g. if the "from" range
    #' is 80-100 and the corresponding "to" range is 0.8-1, then 90 -> 0.9.\cr
    #' "continuous" cannot be used with valueType="text"/"character".
    #' @param mappings The mappings to be applied, specified in one of the following
    #' three forms:\cr
    #' (1) a list containing pairs of values, e.g.
    #' `list(0, "red", 0.4, "yellow", 0.8, "green")`.\cr
    #' (2) a list containing "from" and "to" vectors/lists, e.g.
    #' `list(from=c(0, 0.4, 0.8), to=c("red", "yellow", "green"))`.\cr
    #' (3) a custom mapping function that will be invoked once per cell, e.g.
    #' `function(v, cell) { if(isTRUE(v>0.8)) return("green") }`.\cr
    #' Mappings must be specified in ascending order when valueType="range" or
    #' valueType="continuous".\cr
    #' If a custom mapping function is specified, then the valueType and mapType
    #' parameters are ignored.
    #' @param styleLowerValues A logical value, default `FALSE`, that specifies
    #' whether values less than the lowest specified "from" value should be styled
    #' using the style specified for the lowest "from" value.  Only applies when
    #' valueType="range" or valueType="continuous".
    #' @param styleHigherValues A logical value, default `TRUE`, that specifies
    #' whether values greater than the highest specified "from" value should be styled
    #' using the style specified for the highest "from" value.  Only applies when
    #' valueType="range" or valueType="continuous".
    #' @return No return value.
    mapStyling = function(styleProperty=NULL, cells=NULL, valueType="text", mapType="range", mappings=NULL, styleLowerValues=FALSE, styleHigherValues=TRUE) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "mapStyling", styleProperty, missing(styleProperty), allowMissing=FALSE, allowNull=FALSE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "mapStyling", cells, missing(cells), allowMissing=FALSE, allowNull=FALSE, allowedClasses=c("PivotCell", "list"), allowedListElementClasses="PivotCell")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "mapStyling", valueType, missing(valueType), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("character", "text", "numeric", "number", "color", "colour"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "mapStyling", mapType, missing(mapType), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("value", "range", "logic", "continuous"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "mapStyling", mappings, missing(mappings), allowMissing=TRUE, allowNull=FALSE, allowedClasses=c("character", "list", "function"), allowedListElementClasses=c("character", "integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "mapStyling", styleLowerValues, missing(styleLowerValues), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "mapStyling", styleHigherValues, missing(styleHigherValues), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$mapStyling", "Mapping styling...")
      # prep parameters
      if("PivotCell" %in% class(cells)) cells <- list(cells)
      if(mapType=="numeric") mapType <- "number"
      if(mapType=="character") mapType <- "text"
      if("character" %in% class(mappings)) mappings <- as.list(mappings)
      if(valueType=="colour") valueType <- "color"
      # special case of a mapping function - this ignores the valueType and mapType arguments
      if("function" %in% class(mappings)) {
        for(cell in cells) {
          value <- cell$rawValue
          mappedValue <- mappings(value, cell)
          if((length(mappedValue)>0)&&(!is.na(mappedValue))) {
            declarations <- list()
            declarations[[styleProperty]] <- mappedValue
            self$setStyling(cells=cell, declarations=declarations)
          }
        }
        if(private$p_traceEnabled==TRUE) self$trace("PivotTable$mapStyling", "Mapped styling.")
        return(invisible())
      }
      # general or special case?
      if((length(mappings)==2)&&(length(intersect(names(mappings), c("from", "to")))==2)) {
        # special case of a two element list (from, to)
        if(length(intersect(class(mappings$from), c("character", "integer", "numeric", "list")))==0) {
          stop("PivotTable$mapStyling():  The 'from' values must be either character, integer, numeric or a list.", call. = FALSE)
        }
        # build the maps list
        maps <- list()
        mapCount <- min(length(mappings$from), length(mappings$to))
        m <- 1
        mMax <- mapCount
        for(m in 1:mMax) {
          if("list" %in% class(mappings$from)) vre <- mappings$from[[m]]
          else vre <- mappings$from[m]
          if("list" %in% class(mappings$to)) value <- mappings$to[[m]]
          else value <- mappings$to[m]
          nextVre <- NULL
          nextValue <- NULL
          if((m+1)<=mMax){
            if("list" %in% class(mappings$from)) nextVre <- mappings$from[[m+1]]
            else nextVre <- mappings$from[m+1]
            if("list" %in% class(mappings$to)) nextValue <- mappings$to[[m+1]]
            else nextValue <- mappings$to[m+1]
          }
          maps[[length(maps)+1]] <- list(vre=vre, value=value, nextVre=nextVre, nextValue=nextValue)
        }
      }
      else {
        # general case of a longer list of mappings
        # check some mappings, return if none
        if(length(mappings)<2) {
          if(private$p_traceEnabled==TRUE) self$trace("PivotTable$mapStyling", "Mapped styling.")
          return(invisible())
        }
        # process the mappings into value pairs
        # every map has two values: vre (value range expression) = the "from" value, value = the "to" value
        # "from" here means the raw value (or range of raw values) that we are mapping from, i.e. the input of the mapping
        # "to" here means the result value, i.e. the output of the mapping
        # mapType=range and mapType=continuous also have: rangeStart, rangeEnd, where vre->rangeStart and nextVre->rangeEnd
        # valueType=color with mapType=continuous also has:
        maps <- list()
        mapCount <- length(mappings)
        m <- 1
        mMax <- mapCount
        while(m<mMax) {
          vre <- mappings[[m]]
          value <- NULL
          if((m+1)<=length(mappings)) value <- mappings[[m+1]]
          nextVre <- NULL
          if((m+2)<=length(mappings)) nextVre <- mappings[[m+2]]
          nextValue <- NULL
          if((m+3)<=length(mappings)) nextValue <- mappings[[m+3]]
          maps[[length(maps)+1]] <- list(vre=vre, value=value, nextVre=nextVre, nextValue=nextValue)
          # message("vre=", vre, " value=", value, " nextVre=", nextVre, " nextValue=", nextValue)
          m <- m+2
        }
      }
      # check/prepare maps
      if(mapType=="value") {
        for(m in 1:length(maps)) {
          map <- maps[[m]]
          if((is.null(map$vre))||(length(map$vre)==0)) stop("PivotTable$mapStyling():  The 'from' value for a mapping cannot be null or blank.", call. = FALSE)
          if(vreIsSingleValue(map$vre)) {
            # write the single value back into the mapping to minimise repeated parsing
            maps[[m]]$vre <- vreGetSingleValue(map$vre)
          }
          else {
            stop(paste0("PivotTable$mapStyling():  The 'from' value for a mapping must be a single numerical value - invalid value ", map$vre), call. = FALSE)
          }
          if((is.null(map$value))||(length(map$value)==0)) stop("PivotTable$mapStyling():  The 'to' value for a mapping cannot be null or blank.", call. = FALSE)
        }
      }
      else if(mapType=="logic") {
        for(m in 1:length(maps)) {
          map <- maps[[m]]
          if((is.null(map$vre))||(length(map$vre)==0)) stop("PivotTable$mapStyling():  The 'from' criteria for a mapping cannot be null or blank.", call. = FALSE)
          if((is.null(map$value))||(length(map$value)==0)) stop("PivotTable$mapStyling():  The 'to' value for a mapping cannot be null or blank.", call. = FALSE)
          testResult <- vreIsMatch(map$vre, 0, testOnly=TRUE)
        }
      }
      else if(mapType=="range") {
        for(m in 1:length(maps)) {
          map <- maps[[m]]
          # basic map checks
          if((is.null(map$vre))||(length(map$vre)==0)) stop("PivotTable$mapStyling():  The 'from' value for a mapping cannot be null or blank.", call. = FALSE)
          if(!vreIsSingleValue(map$vre)) stop("PivotTable$mapStyling():  Only single-value 'from' values can be be used with a range mapping.", call. = FALSE)
          if(((is.null(map$value))||(length(map$value)==0))&&(m<length(maps))) {
            # the last value of a range mapping can be null, e.g. mappings=list(0, "red", 1000, "orange", 15000), note there is no colour for 15000
            stop("PivotTable$mapStyling():  The 'to' value for a mapping cannot be null or blank (except for the last specified 'from' value).", call. = FALSE)
          }
          # store the numerical range as part of the mapping so they don't need to be parsed repeatedly
          map$rangeStart <- vreGetSingleValue(map$vre)
          if((!is.null(map$nextVre))&&(vreIsSingleValue(map$nextVre))) map$rangeEnd <- vreGetSingleValue(map$nextVre)
          # check the "to" values
          if(valueType=="number") {
            if(!is.numeric(map$value)) stop(paste0("PivotTable$mapStyling():  The 'to' value '", map$value, "' must be numeric since valueType=number/numeric has been specified."), call. = FALSE)
          }
          else if(valueType=="color") {
            testColor <- parseColor(map$value)
            if(is.null(testColor)) stop(paste0("PivotTable$mapStyling():  The 'to' value '", map$value, "' must be a valid color/colour since valueType=color has been specified."), call. = FALSE)
          }
          # message(paste0("vre=", map$vre, " nextVre=", map$nextVre, " rangeStart=", map$rangeStart, " rangeEnd=", map$rangeEnd, " value=", map$value, " nextValue=", map$nextValue))
          maps[[m]] <- map
        }
        # check the order of the mappings
        fx <- function(x) { x$rangeStart }
        if(!isTRUE(all.equal(1:length(maps),order(sapply(maps, fx))))) {
          stop("PivotTable$mapStyling(): The 'from' values for range mappings must be specified in ascending order (lowest to highest).", call. = FALSE)
        }
      }
      else if(mapType=="continuous") {
        if(valueType=="text") stop("PivotTable$mapStyling():  Continuous mappings are not supported with text values.", call. = FALSE)
        for(m in 1:length(maps)) {
          map <- maps[[m]]
          # basic map checks
          if((is.null(map$vre))||(length(map$vre)==0)) stop("PivotTable$mapStyling():  The 'from' value for a mapping cannot be null or blank.", call. = FALSE)
          if(!vreIsSingleValue(map$vre)) stop("PivotTable$mapStyling():  Only single-value 'from' values can be be used with a continuous mapping.", call. = FALSE)
          if((is.null(map$value))||(length(map$value)==0)) stop("PivotTable$mapStyling():  The 'to' value for a mapping cannot be null or blank.", call. = FALSE)
          # store the numerical range as part of the mapping so they don't need to be parsed repeatedly
          map$rangeStart <- vreGetSingleValue(map$vre)
          if((!is.null(map$nextVre))&&(vreIsSingleValue(map$nextVre))) map$rangeEnd <- vreGetSingleValue(map$nextVre)
          # check the "to" values
          if(valueType=="number") {
            if(!is.numeric(map$value)) stop(paste0("PivotTable$mapStyling():  The 'to' value '", map$value, "' must be numeric since valueType=number/numeric has been specified."), call. = FALSE)
          }
          else if(valueType=="color") {
            map$startColorHex <- parseColor(map$value)
            if(is.null(map$startColorHex)) stop(paste0("PivotTable$mapStyling():  The 'to' value '", map$value, "' must be a valid color/colour since valueType=color/colour has been specified."), call. = FALSE)
            map$startColorList <- vreHexToClr(map$startColorHex)
            if(length(map$nextValue)>0) {
              map$endColorHex <- parseColor(map$nextValue)
              if(is.null(map$endColorHex)) stop(paste0("PivotTable$mapStyling():  The 'to' value '", map$nextValue, "' must be a valid color/colour since valueType=color/colour has been specified."), call. = FALSE)
              map$endColorList <- vreHexToClr(map$endColorHex)
            }
          }
          # message(paste0("vre=", map$vre, " nextVre=", map$nextVre, " rangeStart=", map$rangeStart, " rangeEnd=", map$rangeEnd, " value=", map$value, " nextValue=", map$nextValue))
          maps[[m]] <- map
        }
        # check the order of the mappings
        fx <- function(x) { x$rangeStart }
        if(!isTRUE(all.equal(1:length(maps),order(sapply(maps, fx))))) {
          stop("PivotTable$mapStyling(): The 'from' values for continuous mappings must be specified in ascending order (lowest to highest).", call. = FALSE)
        }
      }
      # map values
      mMax <- length(maps)
      if(mapType=="value") {
        for(cell in cells) {
          value <- cell$rawValue
          for(map in maps) {
            if(vreIsEqual(map$vre, value)) {
              declarations <- list()
              declarations[[styleProperty]] <- map$value
              self$setStyling(cells=cell, declarations=declarations)
              break # jump to next cell
            }
          }
        }
      }
      else if(mapType=="logic") {
        for(cell in cells) {
          value <- cell$rawValue
          for(map in maps) {
            if(vreIsMatch(map$vre, value)) {
              declarations <- list()
              declarations[[styleProperty]] <- map$value
              self$setStyling(cells=cell, declarations=declarations)
              break # jump to next cell
            }
          }
        }
      }
      else if(mapType=="range") {
        for(cell in cells) {
          cellStyleSet <- FALSE
          value <- cell$rawValue
          for(map in maps) {
            if(length(map$rangeStart)==0) next
            if(length(map$rangeEnd)==0) next
            if(length(map$value)==0) next
            if(isTRUE(map$rangeStart <= value && value < map$rangeEnd)) {
              declarations <- list()
              declarations[[styleProperty]] <- map$value
              self$setStyling(cells=cell, declarations=declarations)
              cellStyleSet <- TRUE
              break # jump to next cell
            }
          }
          # if this cell has been processed, jump to the next cell
          if(cellStyleSet==TRUE) next
          # none of the mappings have matched...
          # ...style the same as the first mapping?
          map <- maps[[1]]
          if(styleLowerValues && (length(map$rangeStart)>0) && isTRUE(value < map$rangeStart) && (length(map$value)>0)) {
            declarations <- list()
            declarations[[styleProperty]] <- map$value
            self$setStyling(cells=cell, declarations=declarations)
          }
          # ...style the same as the last mapping?
          map <- maps[[mMax]]
          if(styleHigherValues && (length(map$rangeStart)>0) && isTRUE(value > map$rangeStart) && (length(map$value)>0)) {
            declarations <- list()
            declarations[[styleProperty]] <- map$value
            self$setStyling(cells=cell, declarations=declarations)
          }
        }
      }
      else if(mapType=="continuous") {
        for(cell in cells) {
          cellStyleSet <- FALSE
          value <- cell$rawValue
          for(map in maps) {
            if(length(map$rangeStart)==0) next
            if(length(map$rangeEnd)==0) next
            if(length(map$value)==0) next
            if(length(map$nextValue)==0) next
            if(isTRUE(map$rangeStart <= value && value < map$rangeEnd)) {
              if(valueType=="number") {
                value <- vreScaleNumber(map$value, map$nextValue, map$rangeStart, map$rangeEnd, value)
                cellStyleSet <- TRUE
              }
              else if(valueType=="color") {
                # message(paste0("startColorHex=", map$startColorHex, " endColorHex=", map$endColorHex, " rangeStart=", map$rangeStart, " rangeEnd=", map$rangeEnd, " value=", value))
                value <- vreScale2Colours(map$startColorList, map$endColorList, map$rangeStart, map$rangeEnd, value)
                cellStyleSet <- TRUE
              }
              if((length(value)>0)&&(!is.na(value))) {
                declarations <- list()
                declarations[[styleProperty]] <- value
                self$setStyling(cells=cell, declarations=declarations)
                break # jump to next cell
              }
            }
          }
          # if this cell has been processed, jump to the next cell
          if(cellStyleSet==TRUE) next
          # none of the mappings have matched...
          # ...style the same as the first mapping?
          map <- maps[[1]]
          if(styleLowerValues && (length(map$rangeStart)>0) && isTRUE(value < map$rangeStart) && (length(map$value)>0)) {
            declarations <- list()
            declarations[[styleProperty]] <- map$startColorHex
            self$setStyling(cells=cell, declarations=declarations)
          }
          # ...style the same as the last mapping?
          map <- maps[[mMax]]
          if(styleHigherValues && (length(map$rangeStart)>0) && isTRUE(value > map$rangeStart) && (length(map$value)>0)) {
            declarations <- list()
            declarations[[styleProperty]] <- map$startColorHex
            self$setStyling(cells=cell, declarations=declarations)
          }
        }
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$mapStyling", "Mapped styling.")
      return(invisible())
    },

    #' @description
    #' Generate the cells that will form the body of the pivot table.
    #' @details
    #' This method rarely needs to be called explicitly, since other methods will
    #' invoke it if needed.
    #' @return No return value.
    generateCellStructure = function() {
      timeStart <- proc.time()
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$generateCellStructure", "Generating cell structure...")
      # clear any existing PivotCells
      private$p_cells$reset()
      # clear rowColumn numbers on both axes
      rowGrps <- private$p_rowGroup$getDescendantGroups(descendants=NULL, includeCurrentGroup=TRUE)
      for(i in 1:length(rowGrps)) {
        rowGrps[[i]]$rowColumnNumber <- NULL
      }
      columnGrps <- private$p_columnGroup$getDescendantGroups(descendants=NULL, includeCurrentGroup=TRUE)
      for(i in 1:length(columnGrps)) {
        columnGrps[[i]]$rowColumnNumber <- NULL
      }
      # set the calculations on columns, if not present
      if((!private$p_calculationsSet)&&(is.null(private$p_calculationsPosition))&&(!is.null(private$p_calculationGroups))&&
         (!is.null(private$p_calculationGroups$defaultGroup))&&(private$p_calculationGroups$defaultGroup$visibleCount>0)) {
        self$addColumnCalculationGroups()
      }
      # get the leaf levels on both axes
      rowGrps <- private$p_rowGroup$getLeafGroups(leafGroups=NULL)
      columnGrps <- private$p_columnGroup$getLeafGroups(leafGroups=NULL)
      rowCount <- length(rowGrps)
      columnCount <- length(columnGrps)
      # calculate filters and calculations for each heading group
      # net filters are calculated top-down
      # calculations are selected bottom-up (first one encountered is used)
      # rows...
      rowFilters <- list()
      rowCalculationGroupNames <- list()
      rowCalculationNames <- list()
      rowIsEmptyFlags <- list()
      for(i in 1:rowCount) {
        # set the rowColumnNumber on the leaf cell
        rowGrps[[i]]$rowColumnNumber <- as.integer(i)
        # get the ancestor groups for this group, starting with the current object
        ancestors <- rowGrps[[i]]$getAncestorGroups(includeCurrentGroup=TRUE)
        # construct the isEmpty flag using OR
        rowIsEmptyFlag <- FALSE
        # construct the parent filter settings using "intersect" filter logic
        rowColFilters <- PivotFilters$new(self)
        for(j in length(ancestors):1) {
          acs <- ancestors[[j]]
          if(!is.null(acs$isEmpty)) {
            rowIsEmptyFlag <- rowIsEmptyFlag||acs$isEmpty
          }
          filters <- acs$filters
          if(is.null(filters)) next
          if(filters$count==0) next
          for(k in 1:length(filters$filters)) {
            filter <- filters$filters[[k]]
            rowColFilters$setFilter(filter, action="intersect")
          }
        }
        rowIsEmptyFlags[[i]] <- rowIsEmptyFlag
        rowFilters[[i]] <- rowColFilters
        # find the calculation
        for(j in 1:length(ancestors)) {
          acs <- ancestors[[j]]
          if(is.null(acs$calculationGroupName)) next
          if(is.null(acs$calculationName)) next
          rowCalculationGroupNames[[i]] <- acs$calculationGroupName
          rowCalculationNames[[i]] <- acs$calculationName
          break
        }
      }
      # calculate the net filters at each position on the columns
      columnFilters <- list()
      columnCalculationGroupNames <- list()
      columnCalculationNames <- list()
      columnIsEmptyFlags <- list()
      for(i in 1:columnCount) {
        # set the rowColumnNumber on the leaf cell
        columnGrps[[i]]$rowColumnNumber <- as.integer(i)
        # get the ancestor groups for this group, starting with the current object
        ancestors <- columnGrps[[i]]$getAncestorGroups(includeCurrentGroup=TRUE)
        # construct the isEmpty flag using OR
        columnIsEmptyFlag <- FALSE
        # construct the parent filter settings using "intersect" filter logic
        rowColFilters <- PivotFilters$new(self)
        for(j in length(ancestors):1) {
          acs <- ancestors[[j]]
          if(!is.null(acs$isEmpty)) {
            columnIsEmptyFlag <- columnIsEmptyFlag||acs$isEmpty
          }
          filters <- acs$filters
          if(is.null(filters)) next
          if(filters$count==0) next
          for(k in 1:length(filters$filters)) {
            filter <- filters$filters[[k]]
            rowColFilters$setFilter(filter, action="intersect")
          }
        }
        columnIsEmptyFlags[[i]] <- columnIsEmptyFlag
        columnFilters[[i]] <- rowColFilters
        # find the calculation
        for(j in 1:length(ancestors)) {
          acs <- ancestors[[j]]
          if(is.null(acs$calculationGroupName)) next
          if(is.null(acs$calculationName)) next
          columnCalculationGroupNames[[i]] <- acs$calculationGroupName
          columnCalculationNames[[i]] <- acs$calculationName
          break
        }
      }
      # get the calculation position
      calculationsPosition <- private$p_calculationsPosition
      if(is.null(calculationsPosition)) { calculationsPosition <- "column" }
      if(!(calculationsPosition %in% c("row", "column")))
        stop("PivotTable$generateCellStructure(): calculationsPosition must be either row or column", call. = FALSE)
      # build a calculation lookup
      calcGrpsLookup <- list()
      if(private$p_calculationGroups$count>0) {
        for(cg in 1:private$p_calculationGroups$count) {
          calcGrp <- private$p_calculationGroups$item(cg)
          cgCalcs <- list()
          if(calcGrp$count>0) {
            for(cn in 1:calcGrp$count) {
              calcn <- calcGrp$item(cn)
              cgCalcs[[calcn$calculationName]]<-calcn
            }
          }
          calcGrpsLookup[[calcGrp$calculationGroupName]] <- cgCalcs
        }
      }
      # create and size the new PivotCells
      private$p_cells$setGroups(rowGroups=rowGrps, columnGroups=columnGrps)
      if(rowCount>0) {
        for(r in 1:rowCount) {
          if(columnCount>0) {
            for(c in 1:columnCount) {
              # determine if this cell is empty
              isEmpty <- rowIsEmptyFlags[[r]]||columnIsEmptyFlags[[c]]
              # calculate the net filters
              if(is.null(rowFilters[[r]])) {
                if(!is.null(columnFilters[[c]])) { rowColFilters <- columnFilters[[c]]$getCopy() }
              }
              else {
                rowColFilters <- rowFilters[[r]]$getCopy()
                if (!is.null(columnFilters[[c]])) { rowColFilters$setFilters(columnFilters[[c]], action="intersect") }
              }
              # find the calculation
              calcGrpNme <- NULL
              calcNme <- NULL
              if(calculationsPosition=="row") {
                if(r <= length(rowCalculationGroupNames)) calcGrpNme <- rowCalculationGroupNames[[r]]
                else calcGrpNme <- NULL
                if(r <= length(rowCalculationNames)) calcNme <- rowCalculationNames[[r]]
                else calcNme<- NULL
              }
              else if(calculationsPosition=="column") {
                if(c <= length(columnCalculationGroupNames)) calcGrpNme <- columnCalculationGroupNames[[c]]
                else calcGrpNme<- NULL
                if(c <= length(columnCalculationNames)) calcNme <- columnCalculationNames[[c]]
                else calcNme<- NULL
              }
              # create the cell
              cell <- PivotCell$new(self, rowNumber=as.integer(r), columnNumber=as.integer(c), isEmpty=isEmpty,
                                    calculationName=calcNme, calculationGroupName=calcGrpNme,
                                    rowColFilters=rowColFilters, rowFilters=rowFilters[[r]], columnFilters=columnFilters[[c]],
                                    rowLeafGroup=rowGrps[[r]], columnLeafGroup=columnGrps[[c]])
              # set the style for the cell
              # note:  the logic for inherited styles from the data groups is taken care of in the renderers
              #        i.e. the cell only stores the additional/overriding styles for the cell (not all of the inherited styles)
              if((!is.null(calcGrpNme))&&(!is.null(calcNme))) {
                calcn <- calcGrpsLookup[[calcGrpNme]][[calcNme]]
                if(!is.null(calcn)) {
                  if(!is.null(calcn$cellBaseStyleName)) cell$baseStyleName <- calcn$cellBaseStyleName
                  if(!is.null(calcn$cellStyleDeclarations))
                    cell$style <- self$createInlineStyle(baseStyleName=calcn$cellBaseStyleName, declarations=calcn$cellStyleDeclarations)
                }
              }
              # add the cell to the pivot table
              private$p_cells$setCell(r=r, c=c, cell=cell)
            }
          }
        }
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$generateCellStructure", "Generated cell structure.")
      private$addTiming("generateCellStructure", timeStart)
      return(invisible(private$cells))
    },

    #' @description
    #' Clear the cells of the pivot table.
    #' @details
    #' The cells are reset automatically when structural changes are made to the
    #' pivot table, so this method rarely needs to be called explicitly.
    #' @return No return value.
    resetCells = function() {
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$resetCells", "Resetting cells...")
      if(private$p_evaluated==TRUE){
        timeStart <- proc.time()
        private$p_cells$reset()
        private$p_evaluated <- FALSE
        private$p_latexRenderer$resetVisibleRange()
        private$p_fixedWidthSized <- FALSE
        private$addTiming("resetCells", timeStart)
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$resetCells", "Reset cells.")
      return(invisible())
    },

    #' @description
    #' Calculate the cell values in the body of the pivot table.
    #' @details
    #' This method rarely needs to be called explicitly, since other methods will
    #' invoke it if needed.
    #' @return No return value.
    evaluateCells = function() {
      timeStartT <- proc.time()
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$evaluateCells", "Evaluating cell values...")
      if(is.null(private$p_cells)) stop("PivotTable$evaluateCells():  No cells exist to calculate.", call. = FALSE)
      rowCount <- private$p_cells$rowCount
      columnCount <- private$p_cells$columnCount
      calculator <- PivotCalculator$new(self)
      timeStart1 <- proc.time()
      for(r in 1:rowCount) {
        for(c in 1:columnCount) {
          cell <- private$p_cells$getCell(r, c)
          calculator$setWorkingData(cell)
        }
      }
      private$addTiming("evaluateCells:setWorkingData", timeStart1)
      if(private$p_evaluationMode=="batch") {
        timeStart1 <- proc.time()
        calculator$generateBatchesForCellEvaluation()
        private$addTiming("evaluateCells:generateBatchesForCellEvaluation", timeStart1)
        timeStart1 <- proc.time()
        calculator$evaluateBatches()
        private$addTiming("evaluateCells:evaluateBatches", timeStart1)
        private$p_lastCellBatchInfo <- calculator$batchInfo
      }
      timeStart1 <- proc.time()
      for(r in 1:rowCount) {
        for(c in 1:columnCount) {
          cell <- private$p_cells$getCell(r, c)
          # ignore empty cells
          if(cell$isEmpty) next
          # calculate cell
          calculator$evaluateCell(cell)
        }
      }
      private$addTiming("evaluateCells:evaluateCell", timeStart1)
      private$p_evaluated <- TRUE
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$evaluateCells", "Evaluated cell values.")
      private$addTiming("evaluateCells:total", timeStartT)
      return(invisible())
    },

    #' @description
    #' Calculate the cell values in the body of the pivot table.
    #' @details
    #' This generally only needs to be called explicitly if specific pivot cells
    #' need to be further processed (e.g. formatted) before the pivot table is
    #' rendered.\cr
    #' This method is a wrapper for calling `normaliseColumnGroups()`,
    #' `normaliseRowGroups()`, `generateCellStructure()` and `evaluateCells()`
    #' in sequence.
    #' @return No return value.
    evaluatePivot = function() {
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$evaluatePivot", "Evaluating pivot table...")
      self$normaliseColumnGroups()
      self$normaliseRowGroups()
      self$generateCellStructure()
      self$evaluateCells()
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$evaluatePivot", "Evaluated pivot table.")
      return(invisible())
    },

    #' @description
    #' Find row data groups that match specified criteria.
    #' @param matchMode Either "simple" (default) or "combinations".\cr
    #' "simple" is used when matching only one variable-value, multiple
    #' variable-value combinations are effectively logical "OR".\cr
    #' "combinations" is used when matching for combinations of variable
    #' values, multiple variable-value combinations are effectively
    #' logical "AND".  A child group is viewed as having the variable-value
    #' filters of itself and it's parent/ancestors, e.g.\cr
    #' `list("TrainCategory"="Express Passenger", "PowerType"="DMU")`,
    #' would return the "DMU" data group underneath "Express Passenger".\cr
    #' See the "Finding and Formatting" vignette for graphical examples.
    #' @param variableNames A character vector specifying the name/names of the
    #' variables to find.  This is useful generally only in pivot tables with
    #' irregular layouts, since in regular pivot tables every cell is related
    #' to every variable.
    #' @param variableValues A list specifying the variable names and values to find,
    #' e.g. `variableValues=list("PowerType"=c("DMU", "HST"))`.\cr
    #' Specify "**" as the variable value to match totals for the specified variable.\cr
    #' Specify "!*" as the variable value to match non-totals for the specified variable.\cr
    #' NB: The totals/non-totals criteria above won’t work when visual totals are used.\cr
    #' @param totals A word that specifies how totals are matched (overrides the finer
    #' settings above) - must be one of "include" (default), "exclude" or "only".
    #' @param calculationNames A character vector specifying the name/names of the
    #' calculations to find.
    #' @param atLevels An integer vector constraining the levels in the hierarchy to
    #' search.
    #' @param minChildCount Match only data groups with this minimum number of children.
    #' @param maxChildCount Match only data groups with this maximum number of children.
    #' @param emptyGroups A word that specifies how empty groups are matched -
    #' must be one of "include", "exclude" (default) or "only".
    #' @param outlineGroups A word that specifies how outline cells are matched -
    #' must be one of "include", "exclude" (default) or "only".
    #' @param outlineLinkedGroupExists `TRUE` to match only groups where the related
    #' outline child group still exists.  `FALSE` to match only groups where the related
    #' outline child group no longer exists.
    #' @param includeDescendantGroups Default `FALSE`.  Specify true to also return
    #' all descendants of data groups that match the specified criteria.
    #' @param rowNumbers An integer vector specifying row numbers that constrains
    #' the data groups to be found.
    #' @param cells A `PivotCell` object or a list of `PivotCell` objects to specify
    #' one or more cells that must intersect the data groups.
    #' @return A list of data groups matching the specified criteria.
    findRowDataGroups = function(matchMode="simple", variableNames=NULL, variableValues=NULL,
                                 totals="include", calculationNames=NULL,
                                 atLevels=NULL, minChildCount=NULL, maxChildCount=NULL,
                                 emptyGroups="exclude",
                                 outlineGroups="exclude", outlineLinkedGroupExists=NULL,
                                 includeDescendantGroups=FALSE,
                                 rowNumbers=NULL, cells=NULL) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findRowDataGroups", matchMode, missing(matchMode), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("simple", "combinations"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findRowDataGroups", variableNames, missing(variableNames), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findRowDataGroups", variableValues, missing(variableValues), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list", listElementsMustBeAtomic=TRUE)
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findRowDataGroups", totals, missing(totals), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("include", "exclude", "only"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findRowDataGroups", calculationNames, missing(calculationNames), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findRowDataGroups", atLevels, missing(atLevels), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer","numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findRowDataGroups", minChildCount, missing(minChildCount), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer","numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findRowDataGroups", maxChildCount, missing(maxChildCount), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer","numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findRowDataGroups", emptyGroups, missing(emptyGroups), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("include", "exclude", "only"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findRowDataGroups", outlineGroups, missing(outlineGroups), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("include", "exclude", "only"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findRowDataGroups", outlineLinkedGroupExists, missing(outlineLinkedGroupExists), allowMissing=TRUE, allowNull=TRUE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findRowDataGroups", includeDescendantGroups, missing(includeDescendantGroups), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findRowDataGroups", rowNumbers, missing(rowNumbers), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findRowDataGroups", cells, missing(cells), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("PivotCell", "list"), allowedListElementClasses="PivotCell")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$findRowDataGroups", "Finding row data groups...")
      # find groups
      grps1 <- private$p_rowGroup$findDataGroups(matchMode=matchMode, variableNames=variableNames, variableValues=variableValues, totals=totals,
                                                calculationNames=calculationNames,
                                                atLevels=atLevels, minChildCount=minChildCount, maxChildCount=maxChildCount,
                                                emptyGroups=emptyGroups,
                                                outlineGroups=outlineGroups, outlineLinkedGroupExists=outlineLinkedGroupExists,
                                                includeDescendantGroups=includeDescendantGroups, includeCurrentGroup=FALSE)
      # additional constraints: this logic is not embedded in PivotDataGroup since it needs the row numbers
      if("PivotCell" %in% class(cells)) cells <- list(cells)
      grps2 <- list()
      if((length(rowNumbers)>0)||(length(cells)>0)) {
        if(!private$p_evaluated) stop("PivotTable$findRowDataGroups():  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
        if(is.null(private$p_cells)) stop("PivotTable$findRowDataGroups():  No cells exist to examine.", call. = FALSE)
        if(length(cells)>0) {
          fx <- function(x) { return(x$rowNumber) }
          cellRowNumbers <- unique(sapply(cells, fx))
          rowNumbers <- union(rowNumbers, cellRowNumbers)
        }
        if(length(rowNumbers)>0) {
          for(grp in grps1) {
            grpRowNumbers <- self$findGroupRowNumbers(group=grp)
            if(length(intersect(rowNumbers, grpRowNumbers))>0) {
              grps2[[length(grps2)+1]] <- grp
            }
          }
        }
      }
      else {
        grps2 <- grps1
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$findRowDataGroups", "Found row data groups.")
      return(invisible(grps2))
    },

    #' @description
    #' Find column data groups that match specified criteria.
    #' @param matchMode Either "simple" (default) or "combinations".\cr
    #' "simple" is used when matching only one variable-value - multiple
    #' variable-value combinations are effectively logical "OR".\cr
    #' "combinations" is used when matching for combinations of variable
    #' values - multiple variable-value combinations are effectively
    #' logical "AND".  A child group is viewed as having the variable-value
    #' filters of itself and it's parent/ancestors, e.g.\cr
    #' `list("TrainCategory"="Express Passenger", "PowerType"="DMU")`,
    #' would return the "DMU" data group underneath "Express Passenger".\cr
    #' See the "Finding and Formatting" vignette for graphical examples.
    #' @param variableNames A character vector specifying the name/names of the
    #' variables to find.  This is useful generally only in pivot tables with
    #' irregular layouts, since in regular pivot tables every cell is related
    #' to every variable.
    #' @param variableValues A list specifying the variable names and values to find,
    #' e.g. `variableValues=list("PowerType"=c("DMU", "HST"))`.\cr
    #' Specify "**" as the variable value to match totals for the specified variable.\cr
    #' Specify "!*" as the variable value to match non-totals for the specified variable.\cr
    #' NB: The totals/non-totals criteria above won’t work when visual totals are used.
    #' @param totals A word that specifies how totals are matched (overrides the finer
    #' settings above) - must be one of "include" (default), "exclude" or "only".
    #' @param calculationNames A character vector specifying the name/names of the
    #' calculations to find.
    #' @param atLevels An integer vector constraining the levels in the hierarchy to
    #' search.
    #' @param minChildCount Match only data groups with this minimum number of children.
    #' @param maxChildCount Match only data groups with this maximum number of children.
    #' @param emptyGroups A word that specifies how empty groups are matched -
    #' must be one of "include", "exclude" (default) or "only".
    #' @param includeDescendantGroups Default `FALSE`.  Specify true to also return
    #' all descendants of data groups that match the specified criteria.
    #' @param columnNumbers An integer vector specifying column numbers that constrains
    #' the data groups to be found.
    #' @param cells A `PivotCell` object or a list of `PivotCell` objects to specify
    #' one or more cells that must intersect the data groups.
    #' @return A list of data groups matching the specified criteria.
    findColumnDataGroups = function(matchMode="simple", variableNames=NULL, variableValues=NULL,
                                    totals="include", calculationNames=NULL,
                                    atLevels=NULL, minChildCount=NULL, maxChildCount=NULL,
                                    emptyGroups="exclude", includeDescendantGroups=FALSE,
                                    columnNumbers=NULL, cells=NULL) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findColumnDataGroups", matchMode, missing(matchMode), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("simple", "combinations"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findColumnDataGroups", variableNames, missing(variableNames), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findColumnDataGroups", variableValues, missing(variableValues), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list", listElementsMustBeAtomic=TRUE)
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findColumnDataGroups", totals, missing(totals), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("include", "exclude", "only"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findColumnDataGroups", calculationNames, missing(calculationNames), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findColumnDataGroups", atLevels, missing(atLevels), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer","numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findColumnDataGroups", minChildCount, missing(minChildCount), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer","numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findColumnDataGroups", maxChildCount, missing(maxChildCount), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer","numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findColumnDataGroups", emptyGroups, missing(emptyGroups), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("include", "exclude", "only"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findColumnDataGroups", includeDescendantGroups, missing(includeDescendantGroups), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findColumnDataGroups", columnNumbers, missing(columnNumbers), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findColumnDataGroups", cells, missing(cells), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("PivotCell", "list"), allowedListElementClasses="PivotCell")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$findColumnDataGroups", "Finding column data groups...")
      # find groups
      grps1 <- private$p_columnGroup$findDataGroups(matchMode=matchMode, variableNames=variableNames, variableValues=variableValues, totals=totals,
                                                   calculationNames=calculationNames,
                                                   atLevels=atLevels, minChildCount=minChildCount, maxChildCount=maxChildCount,
                                                   emptyGroups=emptyGroups, outlineGroups="exclude",
                                                   includeDescendantGroups=includeDescendantGroups, includeCurrentGroup=FALSE)
      # additional constraints: this logic is not embedded in PivotDataGroup since it needs the column numbers
      if("PivotCell" %in% class(cells)) cells <- list(cells)
      grps2 <- list()
      if((length(columnNumbers)>0)||(length(cells)>0)) {
        if(!private$p_evaluated) stop("PivotTable$findColumnDataGroups():  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
        if(is.null(private$p_cells)) stop("PivotTable$findColumnDataGroups():  No cells exist to examine.", call. = FALSE)
        if(length(cells)>0) {
          fx <- function(x) { return(x$columnNumber) }
          cellColNumbers <- unique(sapply(cells, fx))
          columnNumbers <- union(columnNumbers, cellColNumbers)
        }
        if(length(columnNumbers)>0) {
          for(grp in grps1) {
            grpColNumbers <- self$findGroupColumnNumbers(group=grp)
            if(length(intersect(columnNumbers, grpColNumbers))>0) {
              grps2[[length(grps2)+1]] <- grp
            }
          }
        }
      }
      else {
        grps2 <- grps1
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$findColumnDataGroups", "Found column data groups.")
      return(invisible(grps2))
    },

    #' @description
    #' Retrieve row numbers for rows where all cells are empty.
    #' @details
    #' `NULL` cell values are always regarded as empty.
    #' @param NAasEmpty `TRUE` (default) specifies that `NA` is treated as empty.
    #' @param zeroAsEmpty `TRUE` specifies that zero is treated as empty,
    #' default `FALSE`.
    #' @param zeroTolerance The tolerance for zero comparisons, default 0.000001.
    #' @param includeOutlineRows `TRUE` to also examine outline rows, default `FALSE`.
    #' @return An integer vector of row numbers.
    getEmptyRows = function(NAasEmpty=TRUE, zeroAsEmpty=FALSE, zeroTolerance=0.000001, includeOutlineRows=FALSE) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getEmptyRows", NAasEmpty, missing(NAasEmpty), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getEmptyRows", zeroAsEmpty, missing(zeroAsEmpty), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getEmptyRows", zeroTolerance, missing(zeroTolerance), allowMissing=TRUE, allowNull=FALSE, allowedClasses=c("integer","numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getEmptyRows", includeOutlineRows, missing(includeOutlineRows), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getEmptyRows", "Getting empty rows...")
      if(!private$p_evaluated) stop("PivotTable$getEmptyRows():  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
      if(is.null(private$p_cells)) stop("PivotTable$getEmptyRows():  No cells exist to examine.", call. = FALSE)
      emptyRowNumbers <- vector("integer", 0)
      rowCount <- private$p_cells$rowCount
      columnCount <- private$p_cells$columnCount
      for(r in 1:rowCount) {
        if(includeOutlineRows==FALSE) {
          if(private$p_cells$rowGroups[[r]]$isOutline) next
        }
        allCellsNull <- TRUE
        for (c in 1:columnCount) {
          cell <- private$p_cells$getCell(r, c)
          isNULL <- FALSE
          isNULL <- isNULL || is.null(cell$rawValue)
          isNULL <- isNULL || ((NAasEmpty==TRUE)&&(is.na(cell$rawValue)))
          isNULL <- isNULL || ((zeroAsEmpty==TRUE)&&(abs(cell$rawValue)<zeroTolerance))
          if(isNULL==FALSE) {
            allCellsNull <- FALSE
            break
          }
        }
        if(allCellsNull==TRUE) {
          emptyRowNumbers[length(emptyRowNumbers)+1] <- r
        }
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getEmptyRows", "Got empty rows.")
      return(invisible(emptyRowNumbers))
    },

    #' @description
    #' Retrieve column numbers for columns where all cells are empty.
    #' @details
    #' `NULL` cell values are always regarded as empty.
    #' @param NAasEmpty `TRUE` (default) specifies that `NA` is treated as empty.
    #' @param zeroAsEmpty `TRUE` specifies that zero is treated as empty,
    #' default `FALSE`.
    #' @param zeroTolerance The tolerance for zero comparisons, default 0.000001.
    #' @return An integer vector of column numbers.
    getEmptyColumns = function(NAasEmpty=TRUE, zeroAsEmpty=FALSE, zeroTolerance=0.000001) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getEmptyColumns", NAasEmpty, missing(NAasEmpty), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getEmptyColumns", zeroAsEmpty, missing(zeroAsEmpty), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getEmptyRows", zeroTolerance, missing(zeroTolerance), allowMissing=TRUE, allowNull=FALSE, allowedClasses=c("integer","numeric"))
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getEmptyColumns", "Getting empty columns...")
      if(!private$p_evaluated) stop("PivotTable$getEmptyColumns():  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
      if(is.null(private$p_cells)) stop("PivotTable$getEmptyColumns():  No cells exist to examine.", call. = FALSE)
      emptyColumnNumbers <- vector("integer", 0)
      rowCount <- private$p_cells$rowCount
      columnCount <- private$p_cells$columnCount
      for(c in 1:columnCount) {
        allCellsNull <- TRUE
        for (r in 1:rowCount) {
          cell <- private$p_cells$getCell(r, c)
          isNULL <- FALSE
          isNULL <- isNULL || is.null(cell$rawValue)
          isNULL <- isNULL || ((NAasEmpty==TRUE)&&(is.na(cell$rawValue)))
          isNULL <- isNULL || ((zeroAsEmpty==TRUE)&&(abs(cell$rawValue)<zeroTolerance))
          if(isNULL==FALSE) {
            allCellsNull <- FALSE
            break
          }
        }
        if(allCellsNull==TRUE) {
          emptyColumnNumbers[length(emptyColumnNumbers)+1] <- c
        }
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getEmptyColumns", "Got empty columns.")
      return(invisible(emptyColumnNumbers))
    },

    #' @description
    #' Get the cell at the specified row and column coordinates in the pivot table.
    #' @details
    #' The row and column numbers refer only to the cells in the body of the pivot
    #' table, i.e. row and column headings are excluded, e.g. row 1 is the first
    #' row of cells underneath the column headings.
    #' @param r Row number of the cell to retrieve.
    #' @param c Column number of the cell to retrieve.
    #' @return A `PivotCell` object representing the cell.
    getCell = function(r=NULL, c=NULL) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getCell", r, missing(r), allowMissing=FALSE, allowNull=FALSE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getCell", c, missing(c), allowMissing=FALSE, allowNull=FALSE, allowedClasses=c("integer", "numeric"))
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getCell", "Getting cell...")
      if(is.null(private$p_cells)) stop("PivotTable$getCell():  No cells exist to retrieve.", call. = FALSE)
      if(length(r)>1) r <- r[1]
      if(length(c)>1) c <- c[1]
      cell <- private$p_cells$getCell(r=r, c=c)
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getCell", "Got cell.")
      return(invisible(cell))
    },

    #' @description
    #' Retrieve cells by a combination of row and/or column numbers.
    #' See the "Finding and Formatting" vignette for graphical examples.
    #' @details
    #' When `specifyCellsAsList=TRUE` (the default):\cr
    #' Get one or more rows by specifying the row numbers as a vector as
    #' the rowNumbers argument and leaving the columnNumbers argument set
    #' to the default value of `NULL`, or\cr
    #' Get one or more columns by specifying the column numbers as a vector
    #' as the columnNumbers argument and leaving the rowNumbers argument
    #' set to the default value of `NULL`, or\cr
    #' Get one or more individual cells by specifying the cellCoordinates
    #' argument as a list of vectors of length 2, where each element in the
    #' list is the row and column number of one cell,\cr
    #' e.g. `list(c(1, 2), c(3, 4))` specifies two cells, the first located
    #' at row 1, column 2 and the second located at row 3, column 4.\cr
    #' When `specifyCellsAsList=FALSE`:\cr
    #' Get one or more rows by specifying the row numbers as a vector as the
    #' rowNumbers argument and leaving the columnNumbers argument set to the
    #' default value of `NULL`, or\cr
    #' Get one or more columns by specifying the column numbers as a vector
    #' as the columnNumbers argument and leaving the rowNumbers argument set
    #' to the default value of `NULL`, or\cr
    #' Get one or more cells by specifying the row and column numbers as vectors
    #' for the rowNumbers and columnNumbers arguments, or\cr
    #' a mixture of the above, where for entire rows/columns the element in the
    #' other vector is set to `NA`, e.g. to retrieve whole rows, specify the row
    #' numbers as the rowNumbers but set the corresponding elements in the
    #' columnNumbers vector to `NA`.
    #' @param specifyCellsAsList Specify how cells are retrieved.
    #' Default `TRUE`. More information is provided in the details section.
    #' @param rowNumbers A vector of row numbers that specify the rows or
    #' cells to retrieve.
    #' @param columnNumbers A vector of column numbers that specify the columns
    #' or cells to retrieve.
    #' @param cellCoordinates A list of two-element vectors that specify the
    #' coordinates of cells to retrieve.  Ignored when `specifyCellsAsList=FALSE`.
    #' @param excludeEmptyCells Default `FALSE`.  Specify `TRUE` to exclude empty
    #' cells.
    #' @param groups A `PivotDataGroup` object or a list of `PivotDataGroup`
    #' objects on either the rows or columns axes.  The cells to be retrieved
    #' must be related to at least one of these groups.
    #' @param rowGroups A `PivotDataGroup` object or a list of `PivotDataGroup`
    #' objects on the rows axis.  The cells to be retrieved must be related to
    #' at least one of these row groups.  If both `rowGroups` and `columnGroups`
    #' are specified, then the cells to be retrieved must be related to at least
    #' one of the specified row groups and one of the specified column groups.
    #' @param columnGroups A `PivotDataGroup` object or a list of `PivotDataGroup`
    #' objects on the columns axis.  The cells to be retrieved must be related to
    #' at least one of these column groups.  If both `rowGroups` and `columnGroups`
    #' are specified, then the cells to be retrieved must be related to at least
    #' one of the specified row groups and one of the specified column groups.
    #' @param matchMode Either "simple" (default) or "combinations":\cr
    #' "simple" specifies that row and column arguments are considered separately
    #' (logical OR), e.g. rowNumbers=1 and columnNumbers=2 will match all cells in
    #' row 1 and all cells in column 2.\cr
    #' "combinations" specifies that row and column arguments are considered together
    #' (logical AND), e.g. rowNumbers=1 and columnNumbers=2 will match only the
    #' cell single at location (1, 2).\cr
    #' Arguments `rowNumbers`, `columnNumbers`, `rowGroups` and `columnGroups` are
    #' affected by the match mode.  All other arguments are not.
    #' @return A list of `PivotCell` objects.
    getCells = function(specifyCellsAsList=TRUE, rowNumbers=NULL, columnNumbers=NULL, cellCoordinates=NULL, excludeEmptyCells=FALSE,
                        groups=NULL, rowGroups=NULL, columnGroups=NULL, matchMode="simple") {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getCells", specifyCellsAsList, missing(specifyCellsAsList), allowMissing=TRUE, allowNull=TRUE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getCells", rowNumbers, missing(rowNumbers), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getCells", columnNumbers, missing(columnNumbers), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getCells", cellCoordinates, missing(cellCoordinates), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list", allowedListElementClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getCells", excludeEmptyCells, missing(excludeEmptyCells), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getCells", groups, missing(groups), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("PivotDataGroup", "list"), allowedListElementClasses="PivotDataGroup")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getCells", rowGroups, missing(rowGroups), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("PivotDataGroup", "list"), allowedListElementClasses="PivotDataGroup")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getCells", columnGroups, missing(columnGroups), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("PivotDataGroup", "list"), allowedListElementClasses="PivotDataGroup")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getCells", matchMode, missing(matchMode), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("simple", "combinations"))
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getCells", "Getting cells...")
      if(!private$p_evaluated) stop("PivotTable$getCells():  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
      if(is.null(private$p_cells)) stop("PivotTable$getCells():  No cells exist to retrieve.", call. = FALSE)
      cells <- private$p_cells$getCells(specifyCellsAsList=specifyCellsAsList, rowNumbers=rowNumbers, columnNumber=columnNumbers,
                                        cellCoordinates=cellCoordinates, excludeEmptyCells=excludeEmptyCells,
                                        groups=groups, rowGroups=rowGroups, columnGroups=columnGroups,
                                        matchMode=matchMode)
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getCells", "Got cells.")
      return(invisible(cells))
    },

    #' @description
    #' Find cells matching specified criteria.
    #' See the "Finding and Formatting" vignette for graphical examples.
    #' @details
    #' The valueRanges parameter can be any of the following
    #' forms:\cr
    #' (1) a specific value, e.g. 12.\cr
    #' (2) a specific value equality condition, e.g. "v==12", where v
    #' represents the cell value.\cr
    #' (3) a value range expression using the following abbreviated form:
    #' "value1<=v<value2", e.g. "10<=v<15".  Only "<" or "<=" can be used
    #' in these value range expressions.\cr
    #' (4) a standard R logical expression, e.g.
    #' "10<=v && v<15".\cr
    #' Basic R functions that test the value can also be
    #' used, e.g. is.na(v).\cr
    #' @param variableNames A character vector specifying the name/names of the
    #' variables to find.  This is useful generally only in pivot tables with
    #' irregular layouts, since in regular pivot tables every cell is related
    #' to every variable.
    #' @param variableValues A list specifying the variable names and values to find,
    #' e.g. `variableValues=list("PowerType"=c("DMU", "HST"))`.\cr
    #' Specify "**" as the variable value to match totals for the specified variable.\cr
    #' Specify "!*" as the variable value to match non-totals for the specified variable.\cr
    #' NB: The totals/non-totals criteria above won’t work when visual totals are used.
    #' @param totals A word that specifies how totals are matched (overrides the finer
    #' settings above) - must be one of "include" (default), "exclude" or "only".
    #' @param calculationNames A character vector specifying the name/names of the
    #' calculations to find.
    #' @param minValue A numerical value specifying a minimum value threshold.
    #' @param maxValue A numerical value specifying a maximum value threshold.
    #' @param exactValues A vector or list specifying a set of allowed values.
    #' @param valueRanges A vector specifying one or more value range expressions which
    #' the cell values must match.  If multiple value range expressions are specified,
    #' then the cell value must match any of one the specified expressions.  See details.
    #' @param includeNull specify TRUE to include `NULL` in the matched cells,
    #' FALSE to exclude `NULL` values.
    #' @param includeNA specify TRUE to include `NA` in the matched cells,
    #' FALSE to exclude `NA` values.
    #' @param emptyCells A word that specifies how empty cells are matched -
    #' must be one of "include" (default), "exclude" or "only".
    #' @param outlineCells A word that specifies how outline cells are matched -
    #' must be one of "include", "exclude" (default) or "only".
    #' @param rowNumbers A vector of row numbers that specify the rows or
    #' cells to constrain the search.
    #' @param columnNumbers A vector of column numbers that specify the columns
    #' or cells to constrain the search.
    #' @param cellCoordinates A list of two-element vectors that specify the
    #' coordinates of cells to constrain the search.
    #' @param groups A `PivotDataGroup` object or a list of `PivotDataGroup`
    #' objects on either the rows or columns axes.  The cells to be searched
    #' must be related to at least one of these groups.
    #' @param rowGroups A `PivotDataGroup` object or a list of `PivotDataGroup`
    #' objects on the rows axis.  The cells to be searched must be related to
    #' at least one of these row groups.  If both `rowGroups` and `columnGroups`
    #' are specified, then the cells to be searched must be related to at least
    #' one of the specified row groups and one of the specified column groups.
    #' @param columnGroups A `PivotDataGroup` object or a list of `PivotDataGroup`
    #' objects on the columns axis.  The cells to be searched must be related to
    #' at least one of these column groups.  If both `rowGroups` and `columnGroups`
    #' are specified, then the cells to be searched must be related to at least
    #' one of the specified row groups and one of the specified column groups.
    #' @param rowColumnMatchMode Either "simple" (default) or "combinations":\cr
    #' "simple" specifies that row and column arguments are considered separately
    #' (logical OR), e.g. rowNumbers=1 and columnNumbers=2 will match all cells in
    #' row 1 and all cells in column 2.\cr
    #' "combinations" specifies that row and column arguments are considered together
    #' (logical AND), e.g. rowNumbers=1 and columnNumbers=2 will match only the
    #' cell single at location (1, 2).\cr
    #' Arguments `rowNumbers`, `columnNumbers`, `rowGroups` and `columnGroups` are
    #' affected by the match mode.  All other arguments are not.
    #' @param cells A `PivotCell` object or a list of `PivotCell`
    #' objects to constrain the scope of the search.
    #' @param lowN Find the first N cells (ascending order, lowest values first).
    #' @param highN Find the last N cells (descending order, highest values first).
    #' @return A list of `PivotCell` objects.
    findCells = function(variableNames=NULL, variableValues=NULL, totals="include", calculationNames=NULL,
                         minValue=NULL, maxValue=NULL, exactValues=NULL, valueRanges=NULL, includeNull=TRUE, includeNA=TRUE,
                         emptyCells="include", outlineCells="exclude",
                         # additional arguments to constrain cells matched
                         rowNumbers=NULL, columnNumbers=NULL, cellCoordinates=NULL,
                         groups=NULL, rowGroups=NULL, columnGroups=NULL,
                         rowColumnMatchMode="simple", cells=NULL,
                         lowN=NULL, highN=NULL) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findCells", variableNames, missing(variableNames), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findCells", variableValues, missing(variableValues), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list", listElementsMustBeAtomic=TRUE)
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findCells", totals, missing(totals), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("include", "exclude", "only"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findCells", calculationNames, missing(calculationNames), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findCells", minValue, missing(minValue), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findCells", maxValue, missing(maxValue), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findCells", valueRanges, missing(valueRanges), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findCells", exactValues, missing(exactValues), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer","numeric", "character", "logical", "date", "Date", "POSIXct", "list"), listElementsMustBeAtomic=TRUE)
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findCells", includeNull, missing(includeNull), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findCells", includeNA, missing(includeNA), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findCells", emptyCells, missing(emptyCells), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("include", "exclude", "only"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findCells", outlineCells, missing(outlineCells), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("include", "exclude", "only"))
        # additional arguments to constrain cells matched
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findCells", rowNumbers, missing(rowNumbers), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findCells", columnNumbers, missing(columnNumbers), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findCells", cellCoordinates, missing(cellCoordinates), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list", allowedListElementClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findCells", groups, missing(groups), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("PivotDataGroup", "list"), allowedListElementClasses="PivotDataGroup")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findCells", rowGroups, missing(rowGroups), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("PivotDataGroup", "list"), allowedListElementClasses="PivotDataGroup")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findCells", columnGroups, missing(columnGroups), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("PivotDataGroup", "list"), allowedListElementClasses="PivotDataGroup")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findCells", cells, missing(cells), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("PivotCell", "list"), allowedListElementClasses="PivotCell")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findCells", rowColumnMatchMode, missing(rowColumnMatchMode), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character", allowedValues=c("simple", "combinations"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotCells", "findCells", lowN, missing(lowN), allowMissing=TRUE, allowNull=TRUE, minValue=0, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotCells", "findCells", highN, missing(highN), allowMissing=TRUE, allowNull=TRUE, minValue=0, allowedClasses=c("integer", "numeric"))
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$findCells", "Finding cells...")
      if(!private$p_evaluated) stop("PivotTable$findCells():  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
      if(is.null(private$p_cells)) stop("PivotTable$findCells():  No cells exist to retrieve.", call. = FALSE)
      cells <- private$p_cells$findCells(variableNames=variableNames, variableValues=variableValues, totals=totals, calculationNames=calculationNames,
                                         minValue=minValue, maxValue=maxValue, exactValues=exactValues, valueRanges=valueRanges,
                                         includeNull=includeNull, includeNA=includeNA,
                                         emptyCells=emptyCells, outlineCells=outlineCells,
                                         # additional arguments to constrain cells matched
                                         rowNumbers=rowNumbers, columnNumbers=columnNumbers, cellCoordinates=cellCoordinates,
                                         groups=groups, rowGroups=rowGroups, columnGroups=columnGroups,
                                         rowColumnMatchMode=rowColumnMatchMode, cells=cells, lowN=lowN, highN=highN)
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$findCells", "Found cells.")
      return(invisible(cells))
    },

    #' @description
    #' Find the column numbers associated with a specific data group or
    #' groups.
    #' @param group A `PivotDataGroup` in the column data groups (i.e. a
    #' column heading) or a list of column data groups.
    #' @param collapse A logical value specifying whether the return value should be
    #' simplified.  See details.
    #' @details
    #' If `group` is a list:  If `collapse` is `FALSE`, then a list of vectors is
    #' returned, if `collapse` is `TRUE`, then a single combined vector is returned.
    #' @return Either a vector of column numbers related to the single specified group
    #' or a list of vectors containing column numbers related to the specified groups.
    findGroupColumnNumbers = function(group=NULL, collapse=FALSE) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findGroupColumnNumbers", group, missing(group), allowMissing=FALSE, allowNull=FALSE, allowedClasses=c("PivotDataGroup", "list"), allowedListElementClasses="PivotDataGroup")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findGroupColumnNumbers", collapse, missing(collapse), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
      }
      # multiple groups
      if("list" %in% class(group)) {
        fx <- function(x) { return(self$findGroupColumnNumbers(group=x)) }
        if(isTRUE(collapse)) return(invisible(unique(unlist(lapply(group, fx)))))
        else return(invisible(lapply(group, fx)))
      }
      # single group
      if(private$p_traceEnabled==TRUE) private$p_parentPivot$trace("PivotTable$findGroupColumnNumbers", "Finding group column numbers...")
      if(!private$p_evaluated) stop("PivotTable$findGroupColumnNumbers():  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
      if(is.null(private$p_cells)) stop("PivotTable$findGroupColumnNumbers():  No cells exist to retrieve.", call. = FALSE)
      matchingColumnNumbers <- private$p_cells$findGroupColumnNumbers(group=group)
      if(private$p_traceEnabled==TRUE) private$p_parentPivot$trace("PivotTable$findGroupColumnNumbers", "Found group column numbers.")
      return(invisible(matchingColumnNumbers))
    },

    #' @description
    #' Find the row numbers associated with a specific data group or groups.
    #' @param group A `PivotDataGroup` in the row data groups (i.e. a
    #' row heading) or a list of row data groups.
    #' @param collapse A logical value specifying whether the return value should be
    #' simplified.  See details.
    #' @details
    #' If `group` is a list:  If `collapse` is `FALSE`, then a list of vectors is
    #' returned, if `collapse` is `TRUE`, then a single combined vector is returned.
    #' @return Either a vector of row numbers related to the single specified group
    #' or a list of vectors containing row numbers related to the specified groups.
    findGroupRowNumbers = function(group=NULL, collapse=FALSE) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findGroupRowNumbers", group, missing(group), allowMissing=FALSE, allowNull=FALSE, allowedClasses=c("PivotDataGroup", "list"), allowedListElementClasses="PivotDataGroup")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "findGroupRowNumbers", collapse, missing(collapse), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
      }
      # multiple groups
      if("list" %in% class(group)) {
        fx <- function(x) { return(self$findGroupRowNumbers(group=x)) }
        if(isTRUE(collapse)) return(invisible(unique(unlist(lapply(group, fx)))))
        else return(invisible(lapply(group, fx)))
      }
      # single group
      if(private$p_traceEnabled==TRUE) private$p_parentPivot$trace("PivotTable$findGroupRowNumbers", "Finding group row numbers...")
      if(!private$p_evaluated) stop("PivotTable$findGroupRowNumbers():  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
      if(is.null(private$p_cells)) stop("PivotTable$findGroupRowNumbers():  No cells exist to retrieve.", call. = FALSE)
      matchingColumnNumbers <- private$p_cells$findGroupRowNumbers(group=group)
      if(private$p_traceEnabled==TRUE) private$p_parentPivot$trace("PivotTable$findGroupRowNumbers", "Found group row numbers.")
      return(invisible(matchingColumnNumbers))
    },

    #' @description
    #' Remove a column from the pivot table.
    #' @details
    #' This method removes both the related column group and cells.
    #' @param c The column number.  The first column is column 1, excluding the
    #' column(s) associated with row-headings.
    #' @return No return value.
    removeColumn = function(c=NULL) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "removeColumn", c, missing(c), allowMissing=FALSE, allowNull=FALSE, allowedClasses=c("integer", "numeric"))
      }
      if(private$p_traceEnabled==TRUE) private$p_parentPivot$trace("PivotTable$removeColumn", "Removing column...")
      if(!private$p_evaluated) stop("PivotTable$removeColumn():  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
      if(is.null(private$p_cells)) stop("PivotTable$removeColumn():  No cells exist to retrieve.", call. = FALSE)
      private$p_cells$removeColumn(c)
      if(private$p_traceEnabled==TRUE) private$p_parentPivot$trace("PivotTable$removeColumn", "Removed column.")
    },

    #' @description
    #' Remove multiple column from the pivot table.
    #' @details
    #' This method removes both the related column groups and cells.
    #' @param columnNumbers The column numbers.  The first column is column 1, excluding the
    #' column(s) associated with row-headings.
    #' @return No return value.
    removeColumns = function(columnNumbers=NULL) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "removeColumns", columnNumbers, missing(columnNumbers), allowMissing=FALSE, allowNull=FALSE, allowedClasses=c("integer", "numeric"))
      }
      if(private$p_traceEnabled==TRUE) private$p_parentPivot$trace("PivotTable$removeColumns", "Removing columns...")
      if(!private$p_evaluated) stop("PivotTable$removeColumns():  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
      if(is.null(private$p_cells)) stop("PivotTable$removeColumns():  No cells exist to retrieve.", call. = FALSE)
      private$p_cells$removeColumns(columnNumbers)
      if(private$p_traceEnabled==TRUE) private$p_parentPivot$trace("PivotTable$removeColumns", "Removed columns.")
    },

    #' @description
    #' Remove columns where all cells are empty.
    #' @details
    #' `NULL` cell values are always regarded as empty.
    #' @param NAasEmpty `TRUE` (default) specifies that `NA` is treated as empty.
    #' @param zeroAsEmpty `TRUE` specifies that zero is treated as empty,
    #' default `FALSE`.
    #' @param zeroTolerance The tolerance for zero comparisons, default 0.000001.
    #' @return No return value.
    removeEmptyColumns = function(NAasEmpty=TRUE, zeroAsEmpty=FALSE, zeroTolerance=0.000001) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "removeEmptyColumns", NAasEmpty, missing(NAasEmpty), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "removeEmptyColumns", zeroAsEmpty, missing(zeroAsEmpty), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "removeEmptyColumns", zeroTolerance, missing(zeroTolerance), allowMissing=TRUE, allowNull=FALSE, allowedClasses=c("integer", "numeric"))
      }
      if(private$p_traceEnabled==TRUE) private$p_parentPivot$trace("PivotTable$removeEmptyColumns", "Removing empty columns...")
      if(!private$p_evaluated) stop("PivotTable$removeEmptyColumns():  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
      if(is.null(private$p_cells)) stop("PivotTable$removeEmptyColumns():  No cells exist to retrieve.", call. = FALSE)
      columnNumbers <- self$getEmptyColumns(NAasEmpty=NAasEmpty, zeroAsEmpty=zeroAsEmpty, zeroTolerance=zeroTolerance)
      if((!is.null(columnNumbers))&&(length(columnNumbers)>0)) private$p_cells$removeColumns(columnNumbers)
      if(private$p_traceEnabled==TRUE) private$p_parentPivot$trace("PivotTable$removeEmptyColumns", "Removed empty columns.")
    },

    #' @description
    #' Remove a row from the pivot table.
    #' @details
    #' This method removes both the related row group and cells.
    #' @param r The row number.  The first row is row 1, excluding the
    #' row(s) associated with column-headings.
    #' @return No return value.
    removeRow = function(r=NULL) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "removeRow", r, missing(r), allowMissing=FALSE, allowNull=FALSE, allowedClasses=c("integer", "numeric"))
      }
      if(private$p_traceEnabled==TRUE) private$p_parentPivot$trace("PivotTable$removeRow", "Removing row...")
      if(!private$p_evaluated) stop("PivotTable$removeRow():  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
      if(is.null(private$p_cells)) stop("PivotTable$removeRow():  No cells exist to retrieve.", call. = FALSE)
      private$p_cells$removeRow(r)
      if(private$p_traceEnabled==TRUE) private$p_parentPivot$trace("PivotTable$removeRow", "Removed row.")
    },

    #' @description
    #' Remove multiple rows from the pivot table.
    #' @details
    #' This method removes both the related row groups and cells.
    #' @param rowNumbers The row numbers.  The first row is row 1, excluding the
    #' rows(s) associated with column-headings.
    #' @return No return value.
    removeRows = function(rowNumbers=NULL) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "removeRows", rowNumbers, missing(rowNumbers), allowMissing=FALSE, allowNull=FALSE, allowedClasses=c("integer", "numeric"))
      }
      if(private$p_traceEnabled==TRUE) private$p_parentPivot$trace("PivotTable$removeRows", "Removing rows...")
      if(!private$p_evaluated) stop("PivotTable$removeRows():  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
      if(is.null(private$p_cells)) stop("PivotTable$removeRows():  No cells exist to retrieve.", call. = FALSE)
      private$p_cells$removeRows(rowNumbers)
      if(private$p_traceEnabled==TRUE) private$p_parentPivot$trace("PivotTable$removeRows", "Removed rows.")
    },

    #' @description
    #' Remove rows where all cells are empty.
    #' @details
    #' `NULL` cell values are always regarded as empty.
    #' @param NAasEmpty `TRUE` (default) specifies that `NA` is treated as empty.
    #' @param zeroAsEmpty `TRUE` specifies that zero is treated as empty,
    #' default `FALSE`.
    #' @param zeroTolerance The tolerance for zero comparisons, default 0.000001.
    #' @param includeOutlineRows `TRUE` to also remove empty outline rows,
    #' default `FALSE`.
    #' @return No return value.
    removeEmptyRows = function(NAasEmpty=TRUE, zeroAsEmpty=FALSE, zeroTolerance=0.000001, includeOutlineRows=FALSE) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "removeEmptyRows", NAasEmpty, missing(NAasEmpty), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "removeEmptyRows", zeroAsEmpty, missing(zeroAsEmpty), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "removeEmptyRows", zeroTolerance, missing(zeroTolerance), allowMissing=TRUE, allowNull=FALSE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "removeEmptyRows", includeOutlineRows, missing(includeOutlineRows), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
      }
      if(private$p_traceEnabled==TRUE) private$p_parentPivot$trace("PivotTable$removeEmptyRows", "Removing empty rows...")
      if(!private$p_evaluated) stop("PivotTable$removeEmptyRows():  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
      if(is.null(private$p_cells)) stop("PivotTable$removeEmptyRows():  No cells exist to retrieve.", call. = FALSE)
      rowNumbers <- self$getEmptyRows(NAasEmpty=NAasEmpty, zeroAsEmpty=zeroAsEmpty, zeroTolerance=zeroTolerance, includeOutlineRows=includeOutlineRows)
      if((!is.null(rowNumbers))&&(length(rowNumbers)>0)) private$p_cells$removeRows(rowNumbers)
      if(private$p_traceEnabled==TRUE) private$p_parentPivot$trace("PivotTable$removeEmptyRows", "Removed empty rows.")
    },

    #' @description
    #' Outputs a plain text representation of the pivot table to the console or
    #' returns a character representation of the pivot table.
    #' @param asCharacter `FALSE`(default) outputs to the console, specify `TRUE`
    #' to instead return a character value (does not output to console).
    #' @param showRowGroupHeaders `TRUE` to include the row group headers in the
    #' output, default `FALSE`.
    #' @return Plain text representation of the pivot table.
    print = function(asCharacter=FALSE, showRowGroupHeaders=FALSE) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "print", asCharacter, missing(asCharacter), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "print", showRowGroupHeaders, missing(showRowGroupHeaders), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$print", "Printing matrix...")
      # if not yet evaluated, need to normalise the row/column groups, otherwise errors occur for pivots of more than one level on row/columns
      if(!private$p_evaluated) {
        self$normaliseColumnGroups()
        self$normaliseRowGroups()
      }
      lineIndex <- 0
      if(asCharacter==TRUE) returnLines <- vector("list", self$rowCount + private$p_columnGroup$getLevelCount())
      else returnLines <- NULL
      # constant
      columnPadding <- 2 # characters
      # clearing rendered flags
      private$clearIsRenderedFlags()
      # get the leaf groups
      columnLeafGroups <- private$p_columnGroup$getLeafGroups()
      if(!is.null(columnLeafGroups)) {
        if(length(columnLeafGroups)>0)
          for(c in 1:length(columnLeafGroups))
            columnLeafGroups[[c]]$fixedWidthSize <- 0
      }
      # get the data widths
      columnWidths <- NULL
      if(private$p_evaluated) {
        if(!is.null(private$p_cells)) {
          columnWidths <- private$p_cells$getColumnWidths()
          if(length(columnWidths)!=length(columnLeafGroups)) stop("PivotTable$print():  Column count - column widths mismatch!", call. = FALSE)
          for(c in 1:length(columnWidths)) {
            columnLeafGroups[[c]]$fixedWidthSize <- columnWidths[[c]] + columnPadding
          }
        }
      }
      # work up from the leaf groups setting the data group widths
      columnLevelCount <- private$p_columnGroup$getLevelCount()
      if(columnLevelCount>0) {
        for(l in columnLevelCount:1) {
          levelGroups <- private$p_columnGroup$getLevelGroups(l)
          for(c in 1:length(levelGroups)) {
            grp <- levelGroups[[c]]
            if(is.null(grp$caption)) captionWidth <- columnPadding
            else captionWidth <- nchar(grp$caption) + columnPadding
            if(length(grp$childGroups)==0) {
              # at the leaf level
              # group width is the wider of the caption width of data width
              if(captionWidth > grp$fixedWidthSize) grp$fixedWidthSize <- captionWidth
            }
            else if(length(grp$childGroups)>0) {
              # not at the leaf level (somewhere above)
              # group width is either the caption width or the sum of the child widths - whichever is larger
              totalChildWidth <- 0
              for(cg in 1:length(grp$childGroups)) {
                cgrp <- grp$childGroups[[cg]]
                totalChildWidth <- totalChildWidth + cgrp$fixedWidthSize
              }
              if(totalChildWidth>=captionWidth) grp$fixedWidthSize <- totalChildWidth
              else {
                # pad the child widths to match the caption width
                grp$fixedWidthSize <- captionWidth
                while (totalChildWidth < captionWidth) {
                  for(cg in 1:length(grp$childGroups)) {
                    cgrp <- grp$childGroups[[cg]]
                    cgrp$fixedWidthSize <- cgrp$fixedWidthSize + 1
                    totalChildWidth <- totalChildWidth + 1
                    if(totalChildWidth==captionWidth) break
                  }
                }
              }
            }
          }
        }
      }
      # calculate the widths of the row headings
      rowLevelCount <- private$p_rowGroup$getLevelCount()
      rowLevelWidths <- vector("integer", rowLevelCount)
      if(rowLevelCount>0) {
        for(l in 1:rowLevelCount) {
          maxWidth <- 0
          # row group header
          if(showRowGroupHeaders==TRUE) {
            if(!is.null(private$p_rowGrpHeaders)) {
              if(l<=length(private$p_rowGrpHeaders)) {
                if(!is.null(private$p_rowGrpHeaders[[l]])) {
                  maxWidth <- max(maxWidth, nchar(private$p_rowGrpHeaders[[l]]))
                }
              }
            }
          }
          # row groups
          levelGroups <- private$p_rowGroup$getLevelGroups(l)
          for(r in 1:length(levelGroups)) {
            grp <- levelGroups[[r]]
            maxWidth <- max(maxWidth, nchar(grp$caption))
          }
          rowLevelWidths[l] <- maxWidth + columnPadding
        }
      }
      # quick functions
      repStr <- function(string, times) {
        return(paste(rep(string, times), collapse = ""))
      }
      padStr <- function(str, len, side){
        if(is.null(str)) return(repStr(" ", len))
        thisLength <- nchar(str)
        padLength <- len - thisLength
        if(side=="left") return(paste0(repStr(" ", padLength), str))
        else return(paste0(str, repStr(" ", padLength)))
      }
      # print the output string
      if(columnLevelCount>0) {
        # column headings
        for(cl in 1:columnLevelCount) {
          currentLine <- NULL
          # empty cells at top left or row group headings
          if(rowLevelCount==0) currentLine <- paste0(currentLine, "  ")
          else {
            for(rl in 1:rowLevelCount) {
              if((showRowGroupHeaders==TRUE)&&(!is.null(private$p_rowGrpHeaders))&&
                 (rl<=length(private$p_rowGrpHeaders))&&(!is.null(private$p_rowGrpHeaders[[rl]]))) {
                currentLine <- paste0(currentLine, private$p_rowGrpHeaders[[rl]], repStr(" ", rowLevelWidths[rl]-nchar(private$p_rowGrpHeaders[[rl]])))
              }
              else {
                currentLine <- paste0(currentLine, repStr(" ", rowLevelWidths[rl]))
              }
            }
          }
          # column headings at this level
          levelGroups <- private$p_columnGroup$getLevelGroups(cl)
          for(cg in 1:length(levelGroups)) {
            cgrp <- levelGroups[[cg]]
            if(is.null(cgrp$caption)) currentLine <- paste0(currentLine, repStr(" ", cgrp$fixedWidthSize))
            else if(length(cgrp$caption)==0) currentLine <- paste0(currentLine, repStr(" ", cgrp$fixedWidthSize))
            else if(is.na(cgrp$caption)) currentLine <- paste0(currentLine, repStr(" ", cgrp$fixedWidthSize))
            else currentLine <- paste0(currentLine, cgrp$caption, repStr(" ", cgrp$fixedWidthSize - nchar(cgrp$caption)))
          }
          # print this line
          if(asCharacter==TRUE) {
            lineIndex <- lineIndex + 1
            returnLines[[lineIndex]] <- currentLine
          }
          else cat(paste0(currentLine, "\n"))
        }
      }
      # row headings and data
      rowLeafGroups <- private$p_rowGroup$getLeafGroups()
      columnWidths <- sapply(columnLeafGroups, function(grp) { return(grp$fixedWidthSize) })
      for(r in 1:length(rowLeafGroups)) {
        currentLine <- NULL
        # row headings
        rgrp <- rowLeafGroups[[r]]
        rancs <- rgrp$getAncestorGroups(includeCurrentGroup=TRUE)
        if(rowLevelCount != (length(rancs)-1)) stop("PivotTable$print():  Row level count - leaf group ancestor count mismatch!", call. = FALSE)
        if(rowLevelCount==0) {
          currentLine <- paste0(currentLine, "  ")
        }
        else if(rowLevelCount>0) {
          for(rl in 1:rowLevelCount) {
            rg <- rancs[[rowLevelCount-rl+1]]
            if(rg$isRendered==TRUE) {
              currentLine <- paste0(currentLine, repStr(" ", rowLevelWidths[rl]))
            }
            else {
              if(is.null(rg$caption)) currentLine <- paste0(currentLine, repStr(" ", rowLevelWidths[rl]))
              else if(length(rg$caption)==0) currentLine <- paste0(currentLine, repStr(" ", rowLevelWidths[rl]))
              else if(is.na(rg$caption)) currentLine <- paste0(currentLine, repStr(" ", rowLevelWidths[rl]))
              else currentLine <- paste0(currentLine, rg$caption, repStr(" ", rowLevelWidths[rl] - nchar(rg$caption)))
              rg$isRendered <- TRUE
            }
          }
        }
        # data
        if(private$p_evaluated) {
          if(!is.null(private$p_cells)) {
            for(c in 1:self$columnCount) {
              cell <- private$p_cells$getCell(r, c)
              if(is.null(cell$formattedValue)) currentLine <- paste0(currentLine, repStr(" ", columnWidths[c]))
              else if(length(cell$formattedValue)==0) currentLine <- paste0(currentLine, repStr(" ", columnWidths[c]))
              else if(is.na(cell$formattedValue)) currentLine <- paste0(currentLine, repStr(" ", columnWidths[c]))
              else currentLine <- paste0(currentLine, repStr(" ", columnWidths[c] - 2 - nchar(cell$formattedValue)), cell$formattedValue, "  ")
            }
          }
        }
        # print this line
        if(asCharacter==TRUE) {
          lineIndex <- lineIndex + 1
          returnLines[[lineIndex]] <- currentLine
        }
        else cat(paste0(currentLine, "\n"))
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$print", "Printed matrix.")
      return(invisible(paste0(returnLines, sep="", collapse="\n")))
    },

    #' @description
    #' Convert the pivot table to a matrix, where the data group headings
    #' are included in the body of the matrix.  This method tends to produce
    #' a character matrix.
    #' @details
    #' The newer `asDataMatrix()` tends to produce more a useful matrix.
    #' See the "Outputs" vignette for a comparison of outputs.
    #' @param includeHeaders `TRUE` (default) to include the headings in the
    #' body of the matrix.  Specifying `FALSE` omits the headings.
    #' @param repeatHeaders `FALSE` (default) only outputs the first occurrence of each
    #' header. Specify `TRUE` to repeat the headings.
    #' @param rawValue `FALSE` (default) outputs the formatted (character) values.
    #' Specify `TRUE` to output the raw cell values.
    #' @param showRowGroupHeaders `TRUE` to include the row group headers in the
    #' matrix, default `FALSE`.
    #' @return A matrix.
    asMatrix = function(includeHeaders=TRUE, repeatHeaders=FALSE, rawValue=FALSE, showRowGroupHeaders=FALSE) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "asMatrix", includeHeaders, missing(includeHeaders), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "asMatrix", rawValue, missing(rawValue), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "asMatrix", showRowGroupHeaders, missing(showRowGroupHeaders), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$asMatrix", "Getting pivot table as a matrix...",
                    list(includeHeaders=includeHeaders, repeatHeaders=repeatHeaders, rawValue=rawValue))
      if(!private$p_evaluated) stop("PivotTable$asMatrix():  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
      if(is.null(private$p_cells)) stop("PivotTable$asMatrix():  No cells exist to retrieve.", call. = FALSE)
      if(includeHeaders==FALSE) {
        return(private$p_cells$asMatrix(rawValue=rawValue))
      }
      if(repeatHeaders==FALSE) {
        private$clearIsRenderedFlags()
      }
      # size the matrix
      rowHeaderLevelCount <- private$p_rowGroup$getLevelCount()
      columnHeaderLevelCount <- private$p_columnGroup$getLevelCount()
      rowCount <- private$p_cells$rowCount
      columnCount <- private$p_cells$columnCount
      m <- matrix(data=NA, nrow=columnHeaderLevelCount+rowCount, ncol=rowHeaderLevelCount+columnCount)
      # set the root cells
      for(r in 1:columnHeaderLevelCount) {
        for(c in 1:rowHeaderLevelCount) {
          if((r==1)&&(showRowGroupHeaders==TRUE)&&(!is.null(private$p_rowGrpHeaders))&&
             (c<=length(private$p_rowGrpHeaders))&&(!is.null(private$p_rowGrpHeaders[[c]]))) {
            m[r, c] <- private$p_rowGrpHeaders[[c]]
          }
          else {
            m[r, c] <- ""
          }
        }
      }
      # set the column headers
      colHeaderLeafGroups <- private$p_columnGroup$getLeafGroups()
      for(c in 1:length(colHeaderLeafGroups)) {
        leafGroup <- colHeaderLeafGroups[[c]]
        grps <- leafGroup$getAncestorGroups(includeCurrentGroup=TRUE)
        for(r in (length(grps)-1):1) {
          grp <- grps[[length(grps) - r]]
          if((repeatHeaders==FALSE) && (grp$isRendered==TRUE)) {
            m[r, c + rowHeaderLevelCount] <- ""
            next
          }
          m[r, c + rowHeaderLevelCount] <- grp$caption
          grp$isRendered <- TRUE
        }
      }
      # set the row headers
      rowHeaderLeafGroups <- private$p_rowGroup$getLeafGroups()
      for(r in 1:length(rowHeaderLeafGroups)) {
        leafGroup <- rowHeaderLeafGroups[[r]]
        grps <- leafGroup$getAncestorGroups(includeCurrentGroup=TRUE)
        if((!is.null(grps))&&(length(grps)>1)) {
          for(c in (length(grps)-1):1) {
            grp <- grps[[length(grps) - c]]
            if((repeatHeaders==FALSE) && (grp$isRendered==TRUE)) {
              m[r + columnHeaderLevelCount, c] <- ""
              next
            }
            m[r + columnHeaderLevelCount, c] <- grp$caption
            grp$isRendered <- TRUE
          }
        }
      }
      # set the cell values
      for(r in 1:rowCount) {
        for(c in 1:columnCount) {
          cell <- private$p_cells$getCell(r, c)
          if(rawValue==TRUE) {
            v <- cell$rawValue
            if(!(("integer" %in% class(v))||("numeric" %in% class(v)))) v <- NA
          }
          else v <- cell$formattedValue
          if(is.null(v)) m[columnHeaderLevelCount + r, rowHeaderLevelCount + c] <- ""
          else if(length(v)==0) m[columnHeaderLevelCount + r, rowHeaderLevelCount + c] <- ""
          else if(is.na(v)) m[columnHeaderLevelCount + r, rowHeaderLevelCount + c] <- ""
          else m[columnHeaderLevelCount + r, rowHeaderLevelCount + c] <- v
        }
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$asMatrix", "Got pivot table as a matrix.")
      return(m)
    },

    #' @description
    #' Convert the pivot table to a matrix, where the data group headings
    #' are included as row/column headings in the matrix.
    #' This method tends to produce a numeric matrix.
    #' @details
    #' Where there are multiple levels in a data group hierarchy, the captions are
    #' concatenated to form the row/column headings in the matrix.
    #' See the "Outputs" vignette for a comparison of outputs.
    #' @param includeHeaders `TRUE` (default) to include the headings in the matrix.
    #' Specifying `FALSE` omits the headings.
    #' @param rawValue `TRUE` (default) outputs the raw cell values.
    #' Specify `FALSE` to output the formatted (character) values.
    #' @param separator Specifies the character value used to concatenate data
    #' group captions where multiple levels exist in the data group hierarchy.
    #' @return A matrix.
    asDataMatrix = function(includeHeaders=TRUE, rawValue=TRUE, separator=" ") {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "asDataMatrix", includeHeaders, missing(includeHeaders), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "asDataMatrix", rawValue, missing(rawValue), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "asDataMatrix", separator, missing(separator), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$asDataMatrix", "Getting pivot table as a data matrix...",
                                                  list(rawValue=rawValue, separator=separator))
      if(!private$p_evaluated) stop("PivotTable$asDataMatrix():  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
      if(is.null(private$p_cells)) stop("PivotTable$asDataMatrix():  No cells exist to retrieve.", call. = FALSE)
      if(includeHeaders==FALSE) {
        return(private$p_cells$asMatrix(rawValue=rawValue))
      }
      # get the headings dimensions
      rowHeaderLevelCount <- private$p_rowGroup$getLevelCount()
      columnHeaderLevelCount <- private$p_columnGroup$getLevelCount()
      rowCount <- private$p_cells$rowCount
      columnCount <- private$p_cells$columnCount
      # column names
      colHeaderLeafGroups <- private$p_columnGroup$getLeafGroups()
      colNames <-  vector("list", length = columnCount)
      for(c in 1:columnCount) {
        colName <- ""
        grps <- colHeaderLeafGroups[[c]]$getAncestorGroups(includeCurrentGroup=TRUE)
        for(cl in 1:columnHeaderLevelCount) {
          if(cl==1) {
            colName <- grps[[columnHeaderLevelCount-cl+1]]$caption
          }
          else {
            colName <- paste0(colName, separator, grps[[columnHeaderLevelCount-cl+1]]$caption)
          }
        }
        colNames[[c]] <- colName
      }
      # row names
      rowHeaderLeafGroups <- private$p_rowGroup$getLeafGroups()
      rowNames <-  vector("list", length = rowCount)
      for(r in 1:rowCount) {
        rowName <- ""
        grps <- rowHeaderLeafGroups[[r]]$getAncestorGroups(includeCurrentGroup=TRUE)
        if((!is.null(grps))&&(length(grps)>1)) {
          for(rl in 1:rowHeaderLevelCount) {
            if(rl==1) {
              rowName <- grps[[rowHeaderLevelCount-rl+1]]$caption
            }
            else {
              rowName <- paste0(rowName, separator, grps[[rowHeaderLevelCount-rl+1]]$caption)
            }
          }
        }
        rowNames[[r]] <- rowName
      }
      # data
      m <- private$p_cells$asMatrix(rawValue=rawValue)
      # apply names
      colnames(m) <- make.unique(unlist(colNames))
      rownames(m) <- make.unique(unlist(rowNames))
      # done
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$asDataMatrix", "Got pivot table as a data matrix.")
      return(m)
    },

    #' @description
    #' Convert the pivot table to a data frame, combining multiple levels of
    #' headings with the specified separator and/or exporting the row groups
    #' as columns in the data frame.
    #' @details
    #' See the "Outputs" vignette for more details and examples
    #' @param separator Specifies the character value used to concatenate data
    #' group captions where multiple levels exist in the data group hierarchy.
    #' @param stringsAsFactors Specify `TRUE` to convert strings to factors,
    #' default is `default.stringsAsFactors()` for R < 4.1.0 and `FALSE`
    #' for R >= 4.1.0.
    #' @param forceNumeric Specify `TRUE` to force the conversion of cell values
    #' to a numeric value, default `FALSE`.
    #' @param rowGroupsAsColumns Specify `TRUE` to include the row groups as
    #' additional columns in the data frame.  Default `FALSE`.
    #' @return A data frame.
    asDataFrame = function(separator=" ", stringsAsFactors=NULL, forceNumeric=FALSE, rowGroupsAsColumns=FALSE) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "asDataFrame", separator, missing(separator), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "asDataFrame", stringsAsFactors, missing(stringsAsFactors), allowMissing=TRUE, allowNull=TRUE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "asDataFrame", forceNumeric, missing(forceNumeric), allowMissing=TRUE, allowNull=TRUE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "asDataFrame", rowGroupsAsColumns, missing(rowGroupsAsColumns), allowMissing=TRUE, allowNull=TRUE, allowedClasses="logical")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$asDataFrame", "Getting pivot table as a data frame...", list(separator=separator, stringsAsFactors=stringsAsFactors))
      if(!private$p_evaluated) stop("PivotTable$asDataFrame():  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
      if(is.null(private$p_cells)) stop("PivotTable$asDataFrame():  No cells exist to retrieve.", call. = FALSE)
      # stringsAsFactors default depends on the version of R...
      if(is.null(stringsAsFactors)) {
        if(getRversion() < "4.0.0") {
          # old version, retain existing behaviour with no warning
          stringsAsFactors <- default.stringsAsFactors()
        }
        else if (getRversion() < "4.1.0") {
          stringsAsFactors <- default.stringsAsFactors()
          # generate a warning if the default is TRUE as this will change to FALSE in future
          if(stringsAsFactors) {
            warning("PivotTable$asDataFrame(): In a future version of R, default.stringsAsFactors() will be deprecated and removed, at which time the 'stringsAsFactors' argument will default to FALSE.  Explictly set the 'stringsAsFactors' argument to remove this warning.")
          }
        }
        else {
          # default to FALSE for R 4.1.0 onwards
          stringsAsFactors <- FALSE
        }
      }
      # sizing
      rowHeaderLevelCount <- private$p_rowGroup$getLevelCount()
      columnHeaderLevelCount <- private$p_columnGroup$getLevelCount()
      rowCount <- private$p_cells$rowCount
      columnCount <- private$p_cells$columnCount
      rowHeaders <- list()
      columnHeaders <- list()
      # set the column headers
      if(rowGroupsAsColumns) {
        if(rowHeaderLevelCount > 0) {
          for(c in 1:rowHeaderLevelCount) {
            headerValue <- ""
            if((!is.null(private$p_rowGrpHeaders))&&(length(private$p_rowGrpHeaders)>=c)) {
              headerValue <- private$p_rowGrpHeaders[[c]]
            }
            else {
              headerValue <- paste0("RowGroup", c)
            }
            columnHeaders[[length(columnHeaders) + 1]] <- headerValue
          }
        }
      }
      colHeaderLeafGroups <- private$p_columnGroup$getLeafGroups()
      for(c in 1:length(colHeaderLeafGroups)) {
        leafGroup <- colHeaderLeafGroups[[c]]
        grps <- leafGroup$getAncestorGroups(includeCurrentGroup=TRUE)
        headerValue <- ""
        for(r in (length(grps)-1):1) {
          grp <- grps[[r]]
          if(nchar(headerValue) == 0) headerValue <- grp$caption
          else headerValue <- paste0(headerValue, separator, grp$caption)
        }
        columnHeaders[[length(columnHeaders) + 1]] <- headerValue
      }
      # set the row headers
      rowHeaderLeafGroups <- private$p_rowGroup$getLeafGroups()
      for(r in 1:length(rowHeaderLeafGroups)) {
        leafGroup <- rowHeaderLeafGroups[[r]]
        grps <- leafGroup$getAncestorGroups(includeCurrentGroup=TRUE)
        headerValue <- ""
        if((!is.null(grps))&&(length(grps)>1)) {
          for(c in (length(grps)-1):1) {
            grp <- grps[[c]]
            if(nchar(headerValue) == 0) headerValue <- grp$caption
            else headerValue <- paste0(headerValue, separator, grp$caption)
          }
        }
        rowHeaders[[length(rowHeaders) + 1]] <- headerValue
      }
      # get the value vectors to form the data frame
      dfColumns <- list()
      if(rowGroupsAsColumns) {
        if(rowHeaderLevelCount > 0) {
          for(c in 1:rowHeaderLevelCount) {
            rowNames <-  NA
            for(r in 1:rowCount) {
              grps <- rowHeaderLeafGroups[[r]]$getAncestorGroups(includeCurrentGroup=TRUE)
              if((!is.null(grps))&&(length(grps)>1)) {
                rowNames[r] <- grps[[rowHeaderLevelCount-c+1]]$caption
              }
            }
            dfColumns[[length(dfColumns) + 1]] <- rowNames
          }
        }
      }
      for(c in 1:columnCount) {
        columnValues <- NA
        for(r in 1:rowCount) {
          cell <- private$p_cells$getCell(r, c)
          v <- cell$rawValue
          if(forceNumeric) {
            if(!(("integer" %in% class(v))||("numeric" %in% class(v)))) v <- NA
          }
          if(is.null(v)) v <- NA
          else if(length(v) == 0) v <- NA
          columnValues[r] <- v
        }
        dfColumns[[length(dfColumns) + 1]] <- columnValues
      }
      df <- as.data.frame(dfColumns, stringsAsFactors=stringsAsFactors)
      colnames(df) <- make.unique(unlist(columnHeaders))
      rownames(df) <- make.unique(unlist(rowHeaders))
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$asDataFrame", "Got pivot table as a data frame.")
      return(df)
    },

    #' @description
    #' Convert the pivot table to tidy data frame, where each cell in the body of
    #' the pivot table becomes one row in the data frame.
    #' @details
    #' See the "Outputs" vignette for more details and examples
    #' @param includeGroupCaptions `TRUE` (default) to include the data group
    #' captions as columns in the data frame.
    #' @param includeGroupValues `TRUE` (default) to include the data group
    #' values as columns in the data frame.
    #' @param separator Specifies the character value used to concatenate
    #' filter values where multiple values exist in a filter.
    #' @param stringsAsFactors Specify `TRUE` to convert strings to factors,
    #' default is `default.stringsAsFactors()` for R < 4.1.0 and `FALSE`
    #' for R >= 4.1.0.
    #' @param excludeEmptyCells Specify `FALSE` to also include rows for
    #' empty cells in the data frame, default `TRUE`.
    #' @return A data frame.
    asTidyDataFrame = function(includeGroupCaptions=TRUE, includeGroupValues=TRUE, separator=" ", stringsAsFactors=NULL, excludeEmptyCells=TRUE) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "asTidyDataFrame", includeGroupCaptions, missing(includeGroupCaptions), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "asTidyDataFrame", includeGroupValues, missing(includeGroupValues), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "asTidyDataFrame", separator, missing(separator), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "asTidyDataFrame", stringsAsFactors, missing(stringsAsFactors), allowMissing=TRUE, allowNull=TRUE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "asTidyDataFrame", excludeEmptyCells, missing(excludeEmptyCells), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$asTidyDataFrame", "Getting pivot table as a tidy data frame...",
                   list(includeGroupCaptions=includeGroupCaptions, includeGroupValues=includeGroupValues, separator=separator, stringsAsFactors=stringsAsFactors))
      if(!private$p_evaluated) stop("PivotTable$asTidyDataFrame():  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
      if(is.null(private$p_cells)) stop("PivotTable$asTidyDataFrame():  No cells exist to retrieve.", call. = FALSE)
      # stringsAsFactors default depends on the version of R...
      if(is.null(stringsAsFactors)) {
        if(getRversion() < "4.0.0") {
          # old version, retain existing behaviour with no warning
          stringsAsFactors <- default.stringsAsFactors()
        }
        else if (getRversion() < "4.1.0") {
          stringsAsFactors <- default.stringsAsFactors()
          # generate a warning if the default is TRUE as this will change to FALSE in future
          if(stringsAsFactors) {
            warning("PivotTable$asTidyDataFrame(): In a future version of R, default.stringsAsFactors() will be deprecated and removed, at which time the 'stringsAsFactors' argument will default to FALSE.  Explictly set the 'stringsAsFactors' argument to remove this warning.")
          }
        }
        else {
          # default to FALSE for R 4.1.0 onwards
          stringsAsFactors <- FALSE
        }
      }
      # convert
      df <- list()
      vals <- list()
      # basic information
      df$rowNumber[1] <- NA
      df$columnNumber[1] <- NA
      df$isTotal[1] <- NA
      vals$calculationName[1] <- NA
      vals$calculationGroupName[1] <- NA
      vals$rawValue[1] <- NA
      vals$formattedValue[1] <- NA
      # other standard information
      if(includeGroupCaptions==TRUE) {
        # add in the level headings
        rowLevelCount <- private$p_rowGroup$getLevelCount()
        for(r in 1:rowLevelCount) {
          df[[paste0("RowLevel", sprintf("%02d", r))]][1] <- NA
        }
        columnLevelCount <- private$p_columnGroup$getLevelCount()
        for(c in 1:columnLevelCount) {
          df[[paste0("ColumnLevel", sprintf("%02d", c))]][1] <- NA
        }
      }
      # iterate the cells
      cellNumber <- 0
      if(private$p_cells$rowCount > 0) {
        for(r in 1:private$p_cells$rowCount) {
          if(private$p_cells$columnCount > 0) {
            for(c in 1:private$p_cells$columnCount) {
              # get the cell
              cell <- private$p_cells$getCell(r, c)
              if(is.null(cell)) next
              if(excludeEmptyCells && cell$isEmpty) next
              cellNumber <- cellNumber + 1
              # basic info
              df$rowNumber[cellNumber] <- r
              df$columnNumber[cellNumber] <- c
              df$isTotal[cellNumber] <- cell$isTotal
              vals$calculationName[cellNumber] <- cell$calculationName
              vals$calculationGroupName[cellNumber] <- cell$calculationGroupName
              if(!is.null(cell$rawValue)) {
                if(length(cell$rawValue)>0) {
                  if(!is.na(cell$rawValue)) vals$rawValue[cellNumber] <- cell$rawValue
                }
              }
              if(!is.null(cell$formattedValue)) {
                if(length(cell$formattedValue)>0) {
                  if(!is.na(cell$formattedValue)) vals$formattedValue[cellNumber] <- cell$formattedValue
                }
              }
              # captions
              if(includeGroupCaptions==TRUE) {
                # row heading captions
                rg <- cell$rowLeafGroup
                while(!is.null(rg)) {
                  levelNumber <- rg$levelNumber
                  if(levelNumber==0) break
                  df[[paste0("RowLevel", sprintf("%02d", levelNumber))]][cellNumber] <- rg$caption
                  rg <- rg$parentGroup
                }
                # column heading captions
                cg <- cell$columnLeafGroup
                while(!is.null(cg)) {
                  levelNumber <- cg$levelNumber
                  if(levelNumber==0) break
                  df[[paste0("ColumnLevel", sprintf("%02d", levelNumber))]][cellNumber] <- cg$caption
                  cg <- cg$parentGroup
                }
              }
              # filter values
              if(includeGroupValues==TRUE) {
                filters <- cell$rowColFilters
                if(!is.null(filters)) {
                  if(filters$count>0) {
                    for(i in 1:filters$count) {
                      filter <- filters$filters[[i]]
                      if(is.null(filter)) next
                      values <- filter$values
                      if(is.null(values)) values <- NA
                      if(length(values>1)) values <- paste(values, sep=separator, collapse=separator)
                      df[[filter$variableName]][cellNumber] <- values
                    }
                  }
                }
              }
            }
          }
        }
      }
      if(cellNumber==0) return(invisible())
      # append the values to the end of the data frame
      df$calculationName <- vals$calculationName
      df$calculationGroupName <- vals$calculationGroupName
      df$rawValue <- vals$rawValue
      df$formattedValue <- vals$formattedValue
      # check all of the to-be columns are of the same length and adjust if not
      maxLength <- 0
      for(i in 1:length(df)) {
        maxLength <- max(maxLength, length(df[[i]]))
      }
      for(i in 1:length(df)) {
        if(length(df[[i]]) < maxLength) df[[i]][maxLength] <- NA
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$asTidyDataFrame", "Got pivot table as a tidy data frame.")
      return(invisible(as.data.frame(df, stringsAsFactors=stringsAsFactors)))
    },

    #' @description
    #' Generate a list of the merged cell information arising from the data
    #' group hierarchies.
    #' This is an internal method used to support rendering the pivot table.
    #' @param axis Either "row" or "column".
    #' @return A list containing details of the merged cells.
    getMerges = function(axis=NULL) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getMerges", axis, missing(axis), allowMissing=FALSE, allowNull=FALSE, allowedClasses="character", allowedValues=c("row", "column"))
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getMerges", "Getting merges...", list(axis=axis))
      if(!private$p_evaluated) stop("PivotTable$getMerges():  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
      if(is.null(private$p_cells)) stop("PivotTable$getMerges():  No cells exist.", call. = FALSE)
      # get axis objects/info and allowed merges:  doNotMerge, dataGroupsOnly, cellsOnly, dataGroupsAndCellsAs1, dataGroupsAndCellsAs2
      if(axis=="column") {
        groups <- private$p_cells$columnGroups # leaf-level groups
        levelCount <- private$p_columnGroup$getLevelCount(includeCurrentLevel=FALSE)
        cellsAlongAxis <- private$p_cells$rowCount
        defaultMergeEmptySpace <- private$p_mergeEmptyRowSpace
        if(private$p_mergeEmptySpaceDirection=="row") {
          if(defaultMergeEmptySpace=="cellsOnly") defaultMergeEmptySpace <- "doNotMerge"
          if(defaultMergeEmptySpace=="dataGroupsAndCellsAs1") defaultMergeEmptySpace <- "dataGroupsOnly"
          if(defaultMergeEmptySpace=="dataGroupsAndCellsAs2") defaultMergeEmptySpace <- "dataGroupsOnly"
        }
      }
      else {
        groups <- private$p_cells$rowGroups
        levelCount <- private$p_rowGroup$getLevelCount(includeCurrentLevel=FALSE)
        cellsAlongAxis <- private$p_cells$columnCount
        defaultMergeEmptySpace <- private$p_mergeEmptyColumnSpace
        if(private$p_mergeEmptySpaceDirection=="column") {
          if(defaultMergeEmptySpace=="cellsOnly") defaultMergeEmptySpace <- "doNotMerge"
          if(defaultMergeEmptySpace=="dataGroupsAndCellsAs1") defaultMergeEmptySpace <- "dataGroupsOnly"
          if(defaultMergeEmptySpace=="dataGroupsAndCellsAs2") defaultMergeEmptySpace <- "dataGroupsOnly"
        }
      }
      # build the merge info
      mergeInfo <- list()
      for(i in 1:length(groups)) {
        grp <- groups[[i]]
        mergeEmptySpace <- grp$mergeEmptySpace
        if(is.null(mergeEmptySpace)) mergeEmptySpace <- defaultMergeEmptySpace
        # validate the mergeEmptySpace for this row
        if((!(grp$isEmpty))||(grp$isTotal)) {
          if(mergeEmptySpace=="cellsOnly") mergeEmptySpace <- "doNotMerge"
          else if(mergeEmptySpace %in% c("dataGroupsAndCellsAs1", "dataGroupsAndCellsAs2")) mergeEmptySpace <- "dataGroupsOnly"
        }
        # generate the merge info
        if(mergeEmptySpace=="doNotMerge") {
          mergeInfo[[i]] <- list(merge=FALSE, mergeGroups=FALSE, mergeCells=FALSE)
          next
        }
        ancgrps <- grp$getAncestorGroups(includeCurrentGroup=TRUE) # top-most parent is at bottom of returned list
        ancgrps <- rev(ancgrps) # top-most element is at top of list
        mergeFromLevel <- NULL
        mergeToLevel <- NULL
        for(l in 1:length(ancgrps)) {
          lgrp <- ancgrps[[l]]
          if(lgrp$isEmpty||lgrp$isOutline) {
            if(is.null(mergeFromLevel)) mergeFromLevel <- l - 1
            mergeToLevel <- l - 1
          }
          else {
            # reset back to null if descendant becomes non-empty again
            mergeFromLevel <- NULL
            mergeToLevel <- NULL
          }
        }
        isOutline <- FALSE
        if(isTRUE(grp$isOutline)) isOutline <- TRUE  #isTRUE() works with NULL
        if(is.null(mergeFromLevel)) {
          # no merge possible for this group
          mergeInfo[[i]] <- list(merge=FALSE, mergeGroups=FALSE, mergeCells=FALSE)
        }
        else if(mergeEmptySpace=="dataGroupsOnly") {
          if(mergeFromLevel==mergeToLevel) {
            mergeInfo[[i]] <- list(merge=FALSE, mergeGroups=FALSE, mergeCells=FALSE)
          }
          else {
            mergeInfo[[i]] <- list(merge=TRUE, mergeEmptySpace="dataGroupsOnly", isOutline=isOutline,
                                   mergeGroups=TRUE, mergeGroupsFromLevel=mergeFromLevel, mergeGroupSpan=mergeToLevel-mergeFromLevel+1,
                                   mergeCells=FALSE, skipCells=FALSE)
          }
        }
        else if(mergeEmptySpace=="cellsOnly") {
          if(cellsAlongAxis==1) {
            mergeInfo[[i]] <- list(merge=FALSE, mergeGroups=FALSE, mergeCells=FALSE)
          }
          else {
            mergeInfo[[i]] <- list(merge=TRUE, mergeEmptySpace="cellsOnly", isOutline=isOutline,
                                   mergeGroups=FALSE,
                                   mergeCells=TRUE, skipCells=FALSE, mergeCellSpan=cellsAlongAxis)
          }
        }
        else if(mergeEmptySpace=="dataGroupsAndCellsAs1") {
          mergeInfo[[i]] <- list(merge=TRUE, mergeEmptySpace="dataGroupsAndCellsAs1", isOutline=isOutline,
                                 mergeGroups=TRUE, mergeGroupsFromLevel=mergeFromLevel,
                                 mergeGroupSpan= mergeToLevel-mergeFromLevel+1+cellsAlongAxis,
                                 mergeCells=FALSE, skipCells=TRUE)
        }
        else if(mergeEmptySpace=="dataGroupsAndCellsAs2") {
          if((cellsAlongAxis==1)&&(mergeFromLevel==mergeToLevel)) {
            mergeInfo[[i]] <- list(merge=FALSE, mergeGroups=FALSE, mergeCells=FALSE)
          }
          else if(mergeFromLevel==mergeToLevel){
            mergeInfo[[i]] <- list(merge=TRUE, mergeEmptySpace="cellsOnly", isOutline=isOutline,
                                   mergeGroups=FALSE,
                                   mergeCells=TRUE, skipCells=FALSE, mergeCellSpan=cellsAlongAxis)
          }
          else if(cellsAlongAxis==1) {
            mergeInfo[[i]] <- list(merge=TRUE, mergeEmptySpace="dataGroupsOnly", isOutline=isOutline,
                                   mergeGroups=TRUE, mergeGroupsFromLevel=mergeFromLevel, mergeGroupSpan=mergeToLevel-mergeFromLevel+1,
                                   mergeCells=FALSE, skipCells=FALSE)
          }
          else {
            mergeInfo[[i]] <- list(merge=TRUE, mergeEmptySpace="dataGroupsAndCellsAs2", isOutline=isOutline,
                                   mergeGroups=TRUE, mergeGroupsFromLevel=mergeFromLevel, mergeGroupSpan=mergeToLevel-mergeFromLevel+1,
                                   mergeCells=TRUE, skipCells=FALSE, mergeCellSpan=cellsAlongAxis)
          }
        }
        # message(paste0("Merge: ", ifelse(axis=="column", "c", "r"), " ", i, " mg=", mergeInfo[[i]]$merge, " mges=", mergeInfo[[i]]$mergeEmptySpace,
        #                " outln=", mergeInfo[[i]]$isOutline, " mggrps=", mergeInfo[[i]]$mergeGroups, " mgrpsfl=", mergeInfo[[i]]$mergeGroupsFromLevel,
        #                " mgrpssp=", mergeInfo[[i]]$mergeGroupSpan, " mrgclls=", mergeInfo[[i]]$mergeCells, " skpclls=", mergeInfo[[i]]$skipCells, " mrgcllspn=", mergeInfo[[i]]$mergeCellSpan))
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getMerges", "Got merges.")
      return(invisible(mergeInfo))
    },

    #' @description
    #' Convert the pivot table to a `basictabler` table (from the `basictabler`
    #' R package) which allows further custom manipulation of the pivot table.
    #' @details
    #' See the "Outputs" vignette for more details and examples
    #' @param exportOptions A list of additional export options - see the
    #' "A1. Appendix" for details.
    #' @param compatibility A list containing compatibility options to force
    #' legacy behaviours in the resulting `basictabler` table.
    #' @param showRowGroupHeaders `TRUE` to include the row group headers in the
    #' matrix, default `FALSE`.
    #' @return A `basictabler` table.
    asBasicTable = function(exportOptions=NULL, compatibility=NULL, showRowGroupHeaders=FALSE) {
      timeStart <- proc.time()
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "asBasicTable", exportOptions, missing(exportOptions), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "asBasicTable", compatibility, missing(compatibility), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list", allowedListElementClasses=c("character", "integer", "numeric", "logical"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "asBasicTable", showRowGroupHeaders, missing(showRowGroupHeaders), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$asBasicTable", "Converting to basic table...")
      if(!private$p_evaluated) stop("PivotTable$getHtml():  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
      if(is.null(private$p_cells)) stop("PivotTable$getHtml():  No cells exist to render.", call. = FALSE)
      btbl <- convertPvtTblToBasicTbl(self, exportOptions, compatibility, showRowGroupHeaders=showRowGroupHeaders)
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$asBasicTable", "Converted to basic table.")
      private$addTiming("asBasicTable", timeStart)
      return(invisible(btbl))
    },

    #' @description
    #' Get the CSS declarations for the pivot table.
    #' @details
    #' See the "Outputs" vignette for more details and examples.
    #' @param styleNamePrefix A character variable specifying a prefix for all named
    #' CSS styles, to avoid style name collisions where multiple pivot tables exist.
    #' @return A character value containing the CSS style declaration.
    getCss = function(styleNamePrefix=NULL) {
      timeStart <- proc.time()
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getCss", styleNamePrefix, missing(styleNamePrefix), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getCss", "Getting Styles...")
      if(is.null(private$p_styles)) return("")
      if(length(private$p_styles$styles)==0) return("")
      styles <- ""
      for(s in 1:length(private$p_styles$styles)) {
        style <- private$p_styles$styles[[s]]
        if(is.null(style)) next
        styles <- paste0(styles, style$asNamedCSSStyle(styleNamePrefix=styleNamePrefix), "\r\n")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getCss", "Got Styles.")
      private$addTiming("getCss", timeStart)
      return(invisible(styles))
    },

    #' @description
    #' Generate a HTML representation of the pivot table, optionally including
    #' additional detail for debugging purposes.
    #' @details
    #' See the "Outputs" vignette for more details and examples.
    #' @param styleNamePrefix A character variable specifying a prefix for all named
    #' CSS styles, to avoid style name collisions where multiple pivot tables exist.
    #' @param includeHeaderValues Default `FALSE`, specify `TRUE` to render
    #' this debug information.
    #' @param includeRCFilters Default `FALSE`, specify `TRUE` to render
    #' this debug information.
    #' @param includeCalculationFilters Default `FALSE`, specify `TRUE` to render
    #' this debug information.
    #' @param includeWorkingData Default `FALSE`, specify `TRUE` to render
    #' this debug information.
    #' @param includeEvaluationFilters Default `FALSE`, specify `TRUE` to render
    #' this debug information.
    #' @param includeCalculationNames Default `FALSE`, specify `TRUE` to render
    #' this debug information.
    #' @param includeRawValue Default `FALSE`, specify `TRUE` to render
    #' this debug information.
    #' @param includeTotalInfo Default `FALSE`, specify `TRUE` to render
    #' this debug information.
    #' @param exportOptions A list of additional export options - see the
    #' "A1. Appendix" for details.
    #' @param showRowGroupHeaders Default `FALSE`, specify `TRUE` to render the row
    #' group headings.  See the "Data Groups" vignette for details.
    #' @return A list containing HTML tags from the `htmltools` package.
    #' Convert this to a character variable using `as.character()`.
    getHtml = function(styleNamePrefix=NULL, includeHeaderValues=FALSE, includeRCFilters=FALSE,
                       includeCalculationFilters=FALSE, includeWorkingData=FALSE, includeEvaluationFilters=FALSE,
                       includeCalculationNames=FALSE, includeRawValue=FALSE, includeTotalInfo=FALSE,
                       exportOptions=NULL, showRowGroupHeaders=FALSE) {
      timeStart <- proc.time()
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getHtml", styleNamePrefix, missing(styleNamePrefix), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getHtml", includeHeaderValues, missing(includeHeaderValues), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getHtml", includeRCFilters, missing(includeRCFilters), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getHtml", includeCalculationFilters, missing(includeCalculationFilters), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getHtml", includeWorkingData, missing(includeWorkingData), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getHtml", includeEvaluationFilters, missing(includeEvaluationFilters), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getHtml", includeCalculationNames, missing(includeCalculationNames), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getHtml", includeRawValue, missing(includeRawValue), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getHtml", includeTotalInfo, missing(includeTotalInfo), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getHtml", exportOptions, missing(exportOptions), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getHtml", showRowGroupHeaders, missing(showRowGroupHeaders), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getHtml", "Getting HTML...")
      if(!private$p_evaluated) stop("PivotTable$getHtml():  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
      if(is.null(private$p_cells)) stop("PivotTable$getHtml():  No cells exist to render.", call. = FALSE)
      htmlTable <- private$p_htmlRenderer$getTableHtml(styleNamePrefix=styleNamePrefix, includeHeaderValues=includeHeaderValues,
                                                   includeRCFilters=includeRCFilters, includeCalculationFilters=includeCalculationFilters,
                                                   includeWorkingData=includeWorkingData,
                                                   includeEvaluationFilters=includeEvaluationFilters, includeCalculationNames=includeCalculationNames,
                                                   includeRawValue=includeRawValue, includeTotalInfo=includeTotalInfo,
                                                   exportOptions=exportOptions, showRowGroupHeaders=showRowGroupHeaders)
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getHtml", "Got HTML.")
      private$addTiming("getHtml", timeStart)
      return(invisible(htmlTable))
    },

    #' @description
    #' Save a HTML representation of the pivot table to file, optionally including
    #' additional detail for debugging purposes.
    #' @details
    #' See the "Outputs" vignette for more details and examples.
    #' @param filePath The file to save the HTML to.
    #' @param fullPageHTML `TRUE` (default) includes basic HTML around the pivot
    #' table HTML so that the result file is a valid HTML file.
    #' @param styleNamePrefix A character variable specifying a prefix for all named
    #' CSS styles, to avoid style name collisions where multiple pivot tables exist.
    #' @param includeHeaderValues Default `FALSE`, specify `TRUE` to render
    #' this debug information.
    #' @param includeRCFilters Default `FALSE`, specify `TRUE` to render
    #' this debug information.
    #' @param includeCalculationFilters Default `FALSE`, specify `TRUE` to render
    #' this debug information.
    #' @param includeWorkingData Default `FALSE`, specify `TRUE` to render
    #' this debug information.
    #' @param includeEvaluationFilters Default `FALSE`, specify `TRUE` to render
    #' this debug information.
    #' @param includeCalculationNames Default `FALSE`, specify `TRUE` to render
    #' this debug information.
    #' @param includeRawValue Default `FALSE`, specify `TRUE` to render
    #' this debug information.
    #' @param includeTotalInfo Default `FALSE`, specify `TRUE` to render
    #' this debug information.
    #' @param exportOptions A list of additional export options - see the
    #' "A1. Appendix" for details.
    #' @param showRowGroupHeaders Default `FALSE`, specify `TRUE` to render the row
    #' group headings.  See the "Data Groups" vignette for details.
    #' @return No return value.
    saveHtml = function(filePath=NULL, fullPageHTML=TRUE, styleNamePrefix=NULL, includeHeaderValues=FALSE, includeRCFilters=FALSE,
                        includeCalculationFilters=FALSE, includeWorkingData=FALSE, includeEvaluationFilters=FALSE,
                        includeCalculationNames=FALSE, includeRawValue=FALSE, includeTotalInfo=FALSE,
                        exportOptions=NULL, showRowGroupHeaders=FALSE) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "saveHtml", filePath, missing(filePath), allowMissing=FALSE, allowNull=FALSE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "saveHtml", fullPageHTML, missing(fullPageHTML), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "saveHtml", styleNamePrefix, missing(styleNamePrefix), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "saveHtml", includeHeaderValues, missing(includeHeaderValues), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "saveHtml", includeRCFilters, missing(includeRCFilters), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "saveHtml", includeCalculationFilters, missing(includeCalculationFilters), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "saveHtml", includeWorkingData, missing(includeWorkingData), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "saveHtml", includeEvaluationFilters, missing(includeEvaluationFilters), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "saveHtml", includeCalculationNames, missing(includeCalculationNames), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "saveHtml", includeRawValue, missing(includeRawValue), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "saveHtml", includeTotalInfo, missing(includeTotalInfo), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "saveHtml", exportOptions, missing(exportOptions), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "saveHtml", showRowGroupHeaders, missing(showRowGroupHeaders), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$saveHtml", "Saving HTML...", list(filePath=filePath, fullPageHTML=fullPageHTML))
      if(!private$p_evaluated) stop("PivotTable$getHtml():  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
      # todo: enable rendering before cells are calculated so the structure of the pivot can be checked as it is being developed
      if(is.null(private$p_cells)) stop("PivotTable$saveHtml():  No cells exist to render.", call. = FALSE)
      htmlTable <- private$p_htmlRenderer$getTableHtml(styleNamePrefix=styleNamePrefix, includeHeaderValues=includeHeaderValues,
                                                   includeRCFilters=includeRCFilters, includeCalculationFilters=includeCalculationFilters,
                                                   includeWorkingData=includeWorkingData,
                                                   includeEvaluationFilters=includeEvaluationFilters, includeCalculationNames=includeCalculationNames,
                                                   includeRawValue=includeRawValue, includeTotalInfo=includeTotalInfo,
                                                   exportOptions=exportOptions, showRowGroupHeaders=showRowGroupHeaders)
      if (fullPageHTML==FALSE) {
        fileConn <- file(filePath)
        writeLines(as.character(htmlTable), fileConn)
        close(fileConn)
        if(private$p_traceEnabled==TRUE) self$trace("PivotTable$saveHtml", "Saved HTML.")
        return(invisible())
      }
      # basic css
      cssStr1 <- "<style>h1 { font: 2.5em arial; font-weight: bold; } p { font: 0.9em arial; }</style>"
      cssStr2 <- paste0("<style>", self$getCss(styleNamePrefix=styleNamePrefix), "</style>")
      #pgHtml <- htmltools::tags$html(htmltools::tags$head(htmltools::tags$title('R Pivot Table')), htmltools::HTML(cssStr),
      pgHtml <- htmltools::tags$html(htmltools::HTML("<head>"), htmltools::tags$title('R Pivot Table'), htmltools::HTML(cssStr1), htmltools::HTML(cssStr2), htmltools::HTML("</head>"),
                 htmltools::tags$body(
                   htmltools::h1("R Pivot Table"),
                   htmlTable,
                   htmltools::tags$br(),
                   htmltools::tags$p(paste0("Generated at ", format(Sys.time(), "%X on %a %d %b %Y")))
                 ))
      fileConn <- file(filePath)
      writeLines(as.character(pgHtml), fileConn)
      close(fileConn)
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$saveHtml", "Saved HTML.")
      return(invisible())
    },

    #' @description
    #' Render a HTML representation of the pivot table as an HTML widget,
    #' optionally including additional detail for debugging purposes.
    #' @details
    #' See the "Outputs" vignette for more details and examples.
    #' @param width The width of the widget.
    #' @param height The height of the widget.
    #' @param styleNamePrefix A character variable specifying a prefix for all named
    #' CSS styles, to avoid style name collisions where multiple pivot tables exist.
    #' @param includeHeaderValues Default `FALSE`, specify `TRUE` to render
    #' this debug information.
    #' @param includeRCFilters Default `FALSE`, specify `TRUE` to render
    #' this debug information.
    #' @param includeCalculationFilters Default `FALSE`, specify `TRUE` to render
    #' this debug information.
    #' @param includeWorkingData Default `FALSE`, specify `TRUE` to render
    #' this debug information.
    #' @param includeEvaluationFilters Default `FALSE`, specify `TRUE` to render
    #' this debug information.
    #' @param includeCalculationNames Default `FALSE`, specify `TRUE` to render
    #' this debug information.
    #' @param includeRawValue Default `FALSE`, specify `TRUE` to render
    #' this debug information.
    #' @param includeTotalInfo Default `FALSE`, specify `TRUE` to render
    #' this debug information.
    #' @param exportOptions A list of additional export options - see the
    #' "A1. Appendix" for details.
    #' @param showRowGroupHeaders Default `FALSE`, specify `TRUE` to render the row
    #' group headings.  See the "Data Groups" vignette for details.
    #' @return A HTML widget from the `htmlwidgets` package.
    renderPivot = function(width=NULL, height=NULL, styleNamePrefix=NULL, includeHeaderValues=FALSE, includeRCFilters=FALSE,
                           includeCalculationFilters=FALSE, includeWorkingData=FALSE, includeEvaluationFilters=FALSE,
                           includeCalculationNames=FALSE, includeRawValue=FALSE, includeTotalInfo=FALSE,
                           exportOptions=NULL, showRowGroupHeaders=FALSE) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "renderPivot", width, missing(width), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "renderPivot", height, missing(height), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "renderPivot", styleNamePrefix, missing(styleNamePrefix), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "renderPivot", includeHeaderValues, missing(includeHeaderValues), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "renderPivot", includeRCFilters, missing(includeRCFilters), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "renderPivot", includeCalculationFilters, missing(includeCalculationFilters), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "renderPivot", includeWorkingData, missing(includeWorkingData), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "renderPivot", includeEvaluationFilters, missing(includeEvaluationFilters), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "renderPivot", includeCalculationNames, missing(includeCalculationNames), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "renderPivot", includeRawValue, missing(includeRawValue), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "renderPivot", includeTotalInfo, missing(includeTotalInfo), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "renderPivot", exportOptions, missing(exportOptions), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "renderPivot", showRowGroupHeaders, missing(showRowGroupHeaders), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$renderPivot", "Rendering htmlwidget...", list(width=width, height=height, styleNamePrefix=styleNamePrefix,
                                                                             includeHeaderValues=includeHeaderValues,
                                                                             includeRCFilters=includeRCFilters, includeCalculationFilters=includeCalculationFilters,
                                                                             includeWorkingData=includeWorkingData,
                                                                             includeEvaluationFilters=includeEvaluationFilters, includeCalculationNames=includeCalculationNames,
                                                                             includeRawValue=includeRawValue, includeTotalInfo=includeTotalInfo,
                                                                             exportOptions=exportOptions, showRowGroupHeaders=showRowGroupHeaders))
      if(!private$p_evaluated) self$evaluatePivot()
      if(!private$p_evaluated) stop("PivotTable$getHtml():  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
      # pivottabler(self, width=width, height=height, includeRCFilters=includeRCFilters, includeCalculationFilters=includeCalculationFilters,
      #                 includeCalculationNames=includeCalculationNames, includeRawValue=includeRawValue)
      settings <- list() # may need this in the future
      widgetData <- list(
        tableCss = self$getCss(styleNamePrefix=styleNamePrefix),
        tableHtml = as.character(self$getHtml(styleNamePrefix=styleNamePrefix, includeHeaderValues=includeHeaderValues,
                                            includeRCFilters=includeRCFilters, includeCalculationFilters=includeCalculationFilters,
                                            includeWorkingData=includeWorkingData,
                                            includeEvaluationFilters=includeEvaluationFilters, includeCalculationNames=includeCalculationNames,
                                            includeRawValue=includeRawValue, includeTotalInfo=includeTotalInfo,
                                            exportOptions=exportOptions, showRowGroupHeaders=showRowGroupHeaders)),
        settings = settings
      )
      # viewer.fill=TRUE and browser.fill=TRUE sound like they would be good things, but they seem to prevent
      # any scroll bars being shown when the HTML tables are larger than the RStudio Viewer or the web browser window size
      sp = htmlwidgets::sizingPolicy(
        viewer.padding=10, viewer.fill=FALSE, viewer.suppress=FALSE,
        browser.padding=10, browser.fill=FALSE,
        knitr.defaultWidth="auto", knitr.defaultHeight="auto", knitr.figure = FALSE
      )
      w <- htmlwidgets::createWidget("pivottabler", widgetData, width=width, height=height, sizingPolicy=sp)
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$renderPivot", "Rendered htmlwidget.")
      return(w)
    },

    #' @description
    #' Generate a Latex representation of the pivot table.
    #' @param caption The caption to appear above the table.
    #' @param label The label to use when referring to the table elsewhere in
    #' the document
    #' @param fromRow The row number to render from.
    #' @param toRow The row number to render to.
    #' @param fromColumn The column number to render from.
    #' @param toColumn The column number to render to.
    #' @param boldHeadings Default `FALSE`, specify `TRUE` to render headings
    #' in bold.
    #' @param italicHeadings Default `FALSE`, specify `TRUE` to render headings
    #' in italic.
    #' @param exportOptions A list of additional export options - see the
    #' "A1. Appendix" for details.
    #' @return A character variable containing the Latex representation of
    #' the pivot table.
    getLatex = function(caption=NULL, label=NULL, fromRow=NULL, toRow=NULL, fromColumn=NULL, toColumn=NULL,
                        boldHeadings=FALSE, italicHeadings=FALSE, exportOptions=NULL) {
      timeStart <- proc.time()
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getLatex", caption, missing(caption), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getLatex", label, missing(label), allowMissing=TRUE, allowNull=TRUE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getLatex", fromRow, missing(fromRow), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getLatex", toRow, missing(toRow), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getLatex", fromColumn, missing(fromColumn), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getLatex", toColumn, missing(toColumn), allowMissing=TRUE, allowNull=TRUE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getLatex", boldHeadings, missing(boldHeadings), allowMissing=TRUE, allowNull=TRUE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getLatex", italicHeadings, missing(italicHeadings), allowMissing=TRUE, allowNull=TRUE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "getLatex", exportOptions, missing(exportOptions), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list")
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getLatex", "Getting Latex...", list(caption=caption, label=label,
                                                                                        fromRow=fromRow, toRow=toRow, fromColumn=fromColumn, toColumn=toColumn,
                                                                                        boldHeadings=boldHeadings, italicHeadings=italicHeadings, exportOptions=exportOptions))
      if(!private$p_evaluated) self$evaluatePivot()
      if(!private$p_evaluated) stop("PivotTable$getLatex():  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
      if(is.null(private$p_cells)) stop("PivotTable$getLatex():  No cells exist to render.", call. = FALSE)
      private$p_latexRenderer$setVisibleRange(fromRow=fromRow, toRow=toRow, fromColumn=fromColumn, toColumn=toColumn)
      ltx <- private$p_latexRenderer$getTableLatex(caption=caption, label=label, boldHeadings=boldHeadings, italicHeadings=italicHeadings, exportOptions=exportOptions)
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$getLatex", "Got Latex.")
      private$addTiming("getLatex", timeStart)
      return(ltx)
    },

    #' @description
    #' Write the pivot table into the specified workbook and worksheet at
    #' the specified row-column location.
    #' @param wb A `Workbook` object representing the Excel file being written
    #' to.
    #' @param wsName A character value specifying the name of the worksheet to
    #' write to.
    #' @param topRowNumber An integer value specifying the row number in the
    #' Excel worksheet to write the pivot table.
    #' @param leftMostColumnNumber An integer value specifying the column number
    #' in the Excel worksheet to write the pivot table.
    #' @param outputHeadingsAs Must be one of "rawValue",
    #' "formattedValueAsText" (default) or "formattedValueAsNumber" to specify
    #' how data groups are written into the Excel sheet.
    #' @param outputValuesAs Must be one of "rawValue" (default),
    #' "formattedValueAsText" or "formattedValueAsNumber" to specify
    #' how cell values are written into the Excel sheet.
    #' @param applyStyles Default `TRUE` to write styling information to the cell.
    #' @param mapStylesFromCSS Default `TRUE` to automatically convert CSS style
    #' declarations to their Excel equivalents.
    #' @param exportOptions A list of additional export options - see the
    #' "A1. Appendix" for details.
    #' @param showRowGroupHeaders Default `FALSE`, specify `TRUE` to write row
    #' group headers.
    #' @return No return value.
    writeToExcelWorksheet = function(wb=NULL, wsName=NULL, topRowNumber=NULL, leftMostColumnNumber=NULL, outputHeadingsAs="formattedValueAsText",
                                     outputValuesAs="rawValue", applyStyles=TRUE, mapStylesFromCSS=TRUE, exportOptions=NULL, showRowGroupHeaders=FALSE) {
      if(private$p_argumentCheckMode > 0) {
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "writeToExcelWorksheet", wb, missing(wb), allowMissing=TRUE, allowNull=TRUE, allowedClasses="Workbook")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "writeToExcelWorksheet", wsName, missing(wsName), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "writeToExcelWorksheet", topRowNumber, missing(topRowNumber), allowMissing=TRUE, allowNull=FALSE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "writeToExcelWorksheet", leftMostColumnNumber, missing(leftMostColumnNumber), allowMissing=TRUE, allowNull=FALSE, allowedClasses=c("integer", "numeric"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "writeToExcelWorksheet", outputHeadingsAs, missing(outputHeadingsAs), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("rawValue", "formattedValueAsText", "formattedValueAsNumber"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "writeToExcelWorksheet", outputValuesAs, missing(outputValuesAs), allowMissing=TRUE, allowNull=FALSE, allowedClasses="character", allowedValues=c("rawValue", "formattedValueAsText", "formattedValueAsNumber"))
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "writeToExcelWorksheet", applyStyles, missing(applyStyles), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "writeToExcelWorksheet", mapStylesFromCSS, missing(mapStylesFromCSS), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "writeToExcelWorksheet", exportOptions, missing(exportOptions), allowMissing=TRUE, allowNull=TRUE, allowedClasses="list")
        checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "writeToExcelWorksheet", showRowGroupHeaders, missing(showRowGroupHeaders), allowMissing=TRUE, allowNull=FALSE, allowedClasses="logical")
      }
      if (!requireNamespace("openxlsx", quietly = TRUE)) {
        stop("PivotTable$writeToExcelWorksheet():  The openxlsx package is needed to write the pivot table to an Excel file.  Please install it.", call. = FALSE)
      }
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$writeToExcelWorksheet", "Writing to worksheet...")
      private$p_openxlsxRenderer$writeToWorksheet(wb=wb, wsName=wsName, topRowNumber=topRowNumber,
                                                  leftMostColumnNumber=leftMostColumnNumber,
                                                  outputHeadingsAs=outputHeadingsAs, outputValuesAs=outputValuesAs,
                                                  applyStyles=applyStyles, mapStylesFromCSS=mapStylesFromCSS,
                                                  exportOptions=exportOptions, showRowGroupHeaders=showRowGroupHeaders)
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$writeToExcelWorksheet", "Written to worksheet.")
    },

    #' @description
    #' Capture a call for tracing purposes.
    #' This is an internal method.
    #' @param methodName The name of the method being invoked.
    #' @param desc Short description of method call.
    #' @param detailList A list containing detail such as parameter values.
    #' @return No return value.
    trace = function(methodName, desc, detailList=NULL) {
      if(!private$p_traceEnabled) return()
      stackdepth <- length(sys.calls())
      repStr <- function(string, times) {
        return(paste(rep(string, times), collapse = ""))
      }
      indent <- repStr(" ", (stackdepth - 1) *2)
      msg <- paste0(indent, methodName, ":  ", desc)
      if(length(detailList)>0) {
        nms <- names(detailList)
        msg <- paste0(msg, ": ")
        for(i in 1:length(detailList)) {
          sep <- ""
          if(i > 1) { sep <- ", " }
          dtl <- NULL
          if("function" %in% class(detailList[[i]])) {
            dtl <- deparse(detailList[[i]])
          }
          else dtl <- detailList[[i]]
          msg <- paste0(msg, sep, nms[i], "=", dtl)
        }
      }
      if(is.null(private$p_traceFile)) { message(msg) }
      else { cat(msg, file=private$p_traceFile, sep="\r\n", append=TRUE)}
    },

    #' @description
    #' Output batch information to the console.
    #' @return No return value.
    showBatchInfo = function() {
      message(self$batchInfo)
    },

    #' @description
    #' Return the contents of the pivot table as a list for debugging.
    #' @return A list of various object properties.
    asList = function() {
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$asList", "Getting list...")
      lst <- list(
        type = "pivotTable",
        dataFrames = private$p_data$asList(),
        rowGroup = private$p_rowGroup$asList(),
        columnGroup = private$p_columnGroup$asList(),
        calculationsPosition = private$p_calculationsPosition,
        calculationGroups = private$p_calculationGroups$asList()
      )
      if(!is.null(private$p_cells)) lst$cells <- private$p_cells$asList()
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$asList", "Got list.")
      return(lst)
    },

    #' @description
    #' Return the contents of the pivot table as JSON for debugging.
    #' @return A JSON representation of various object properties.
    asJSON = function() {
      if (!requireNamespace("jsonlite", quietly = TRUE)) {
        stop("The jsonlite package is needed to convert to JSON.  Please install the jsonlite package.", call. = FALSE)
      }
      jsonliteversion <- utils::packageDescription("jsonlite")$Version
      if(numeric_version(jsonliteversion) < numeric_version("1.1")) {
        stop("Version 1.1 or above of the jsonlite package is needed to convert to JSON.  Please install an updated version of the jsonlite package.", call. = FALSE)
      }
      return(jsonlite::toJSON(self$asList()))
    },

    #' @description
    #' Use the `listviewer` package to view the pivot table as JSON for debugging.
    #' @return No return value.
    viewJSON = function() {
      if (!requireNamespace("listviewer", quietly = TRUE)) {
        stop("PivotTable$asJSON():  The listviewer package is needed to view the internal structure of the PivotTable as JSON.  Please install it.", call. = FALSE)
      }
      listviewer::jsonedit(self$asList(), mode="code")
    },

    #' @description
    #' Clean-up the pivot table.
    #' @return No return value.
    finalize = function() {
      if(!is.null(private$p_traceFile)) close(private$p_traceFile)
    }
  ),
  active = list(

    #' @field argumentCheckMode The level of argument checking to perform.
    #' One of "auto", "none", "minimal", "basic", "balanced" (default)
    #' or "full".
    argumentCheckMode = function(value) { return(private$p_argumentCheckMode) },

    #' @field compatibility A list containing compatibility options to force
    #' legacy behaviours.  See the NEWS file for details.
    compatibility = function(value) { return(private$p_compatibility) },

    #' @field traceEnabled Default `FALSE`.  Specify `TRUE` to generate a trace
    #' for debugging purposes.
    traceEnabled = function(value){
      if(missing(value)) return(invisible(private$p_traceEnabled))
      else {
        if(is.logical(value)) private$p_traceEnabled <- value
        else stop("PivotTable$traceEnabled: value must be logical (TRUE, FALSE, T, F)", call. = FALSE)
        return(invisible())
      }
    },

    #' @field processingLibrary The package to use when processing data.
    #' Must be one of "auto" (which today is dplyr), "dplyr" or "data.table".
    processingLibrary = function(value) { return(private$p_processingLibrary) },

    #' @field data A `PivotData` object containing the data frames added to the
    #' pivot table.
    data = function(value) { return(private$p_data) },

    #' @field rowGroup The hidden root `PivotDataGroup` at the top of the row
    #' data groups hierarchy.  The children of this group form the first level
    #' of visible row data groups.
    rowGroup = function(value) { return(invisible(private$p_rowGroup ))},

    #' @field columnGroup The hidden root `PivotDataGroup` at the top of the
    #' column data groups hierarchy.  The children of this group form the first
    #' level of visible column data groups.
    columnGroup = function(value) { return(invisible(private$p_columnGroup ))},

    #' @field rowGroupLevelCount The number of visible levels in the row data
    #' group hierarchy.
    rowGroupLevelCount = function(value) { return(invisible(private$p_rowGroup$getLevelCount())) },

    #' @field columnGroupLevelCount The number of visible levels in the column
    #' data group hierarchy.
    columnGroupLevelCount = function(value) { return(invisible(private$p_columnGroup$getLevelCount())) },

    #' @field topColumnGroups A list containing the first level of column data
    #' groups.
    topColumnGroups = function(value) {
      return(private$p_columnGroup$childGroups)
    },

    #' @field leafColumnGroups  A list containing the bottom level of column
    #' data groups.
    leafColumnGroups = function(value) {
      leafGroups = list()
      grps <- private$p_columnGroup$getLeafGroups(leafGroups)
      return(invisible(grps))
    },

    #' @field allColumnGroups  A list containing all of the column data groups.
    allColumnGroups = function(value) {
      grps <- private$p_columnGroup$getDescendantGroups()
      return(invisible(grps))
    },

    #' @field topRowGroups A list containing the first level of row data
    #' groups.
    topRowGroups = function(value) {
      return(private$p_rowGroup$childGroups)
    },

    #' @field leafRowGroups A list containing the bottom level of row data
    #' groups.
    leafRowGroups = function(value) {
      leafGroups = list()
      grps <- private$p_rowGroup$getLeafGroups(leafGroups)
      return(invisible(grps))
    },

    #' @field allRowGroups  A list containing all of the row data groups.
    allRowGroups = function(value) {
      grps <- private$p_rowGroup$getDescendantGroups()
      return(invisible(grps))
    },

    #' @field rowGrpHeaders A list containing the row group headers.
    rowGrpHeaders = function() { return(invisible(private$p_rowGrpHeaders ))},

    #' @field calculationGroups A list containing the calculation groups in
    #' the pivot table.
    calculationGroups = function(value) { return(invisible(private$p_calculationGroups)) },

    #' @field calculationsPosition Either "row" or "column" describing which axis
    #' the calculations are rendered.
    calculationsPosition = function(value) {
      if(missing(value)) { return(invisible(private$p_calculationsPosition)) }
      else {
        if(is.null(private$p_calculationsPosition)) { private$p_calculationsPosition <- value }
        else {
          if(!(value %in% c("row", "column")))
            stop("PivotTable$calculationsPosition(): calculationsPosition must be either row or column", call. = FALSE)
          if(private$p_calculationsPosition != value)
            stop(paste0("PivotTable$calculationsPosition():  Calculations position already set to be '",
                        private$p_calculationsPosition, "' and cannot be changed."), call. = FALSE)
          return(invisible())
        }
      }
    },

    #' @field evaluationMode Either "batch" (default) or "sequential" (legacy).
    evaluationMode = function(value) { return(invisible(private$p_evaluationMode)) },

    #' @field batchInfo Diagnostic information describing the batches used in the
    #' last pivot table evaluation.
    batchInfo = function(value) { return(invisible(private$p_lastCellBatchInfo)) },

    #' @field cells A `PivotCells` object that contains all of the cells in the pivot
    #' table.
    cells = function(value) { return(invisible(private$p_cells)) },

    #' @field allCells A list of all of the cells in the pivot table, where each element
    #' in the list is a 'PivotCell' object.
    allCells = function(value) {
      if(!private$p_evaluated) stop("PivotTable$allCells:  Pivot table has not been evaluated.  Call evaluatePivot() to evaluate the pivot table.", call. = FALSE)
      if(is.null(private$p_cells)) stop("PivotTable$allCells:  No cells exist.", call. = FALSE)
      return(invisible(private$p_cells$all))
    },

    #' @field rowCount The number of rows in the pivot table, excluding headings.
    rowCount = function(value) { return(invisible(private$p_cells$rowCount)) },

    #' @field columnCount The number of columns in the pivot table, excluding headings.
    columnCount = function(value) { return(invisible(private$p_cells$columnCount)) },

    #' @field fixedWidthSized The total width of the pivot table in characters if
    #' the pivot table were to be rendered as plain text, e.g. to the console.
    fixedWidthSized = function(value) {
      if(missing(value)) return(invisible(private$p_fixedWidthSized))
      else {
        if(is.logical(value)) private$p_fixedWidthSized <- value
        else stop("PivotTable$fixedWidthSized: value must be logical (TRUE, FALSE, T, F)", call. = FALSE)
        return(invisible())
      }
    },

    #' @field asCharacter A plain text representation of the pivot table.
    asCharacter = function() { return(self$print(asCharacter=TRUE)) },

    #' @field theme The name of the theme used to style the pivot table.
    #' If setting this property, either a theme name can be used, or
    #' a list can be used (which specifies a simple theme) or a
    #' `PivotStyles` object can be used.
    #' See the "Styling" vignette for details and examples.
    theme = function(value) {
      if(missing(value)) {
        if(is.null(private$p_styles)) return(invisible(NULL))
        else return(invisible(private$p_styles$theme))
      }
      else {
        if(private$p_argumentCheckMode > 0) {
          checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "theme", value, missing(value), allowMissing=FALSE, allowNull=FALSE, allowedClasses=c("character", "list", "PivotStyles"), allowedListElementClasses="character")
        }
        if("character" %in% class(value)) private$p_styles <- getTheme(parentPivot=self, themeName=value)
        else if("list" %in% class(value)) private$p_styles <- getSimpleColoredTheme(parentPivot=self, theme=value)
        else if("PivotStyles" %in% class(value)) private$p_styles <- value
        return(invisible())
      }
    },

    #' @field styles A `PivotStyles` object that contains the styles
    #' applied to the pivot table.
    styles = function(value) {
      if(missing(value)) return(invisible(private$p_styles))
      else {
        if(private$p_argumentCheckMode > 0) {
          checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "styles", value, missing(value), allowMissing=FALSE, allowNull=FALSE, allowedClasses="PivotStyles")
        }
        private$p_styles <- value
        return(invisible())
      }
    },

    #' @field allowExternalStyles Default `FALSE`, which means the `PivotStyles`
    #' object checks that style names specified for styling the different
    #' parts of the pivot table must exist in the styles collection.  If they do
    #' not an error will occur.  Specify `TRUE` to disable this check, e.g. if
    #' the style definitions are not managed by `pivottabler` but instead
    #' in an external system.
    allowExternalStyles = function(value) {
      if(missing(value)) {
        if(is.null(private$p_styles)) return(invisible(NULL))
        else return(private$p_styles$allowExternalStyles)
      }
      else {
        if(private$p_argumentCheckMode > 0) {
          checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "allowExternalStyles", value, missing(value), allowMissing=FALSE, allowNull=FALSE, allowedClasses="logical")
        }
        private$p_styles$allowExternalStyles <- value
        return(invisible())
      }
    },

    #' @field mergeEmptyRowSpace A character value describing how empty
    #' space is merged.  Allowed values:  "doNotMerge", "dataGroupsOnly",
    #' "cellsOnly", "dataGroupsAndCellsAs1", "dataGroupsAndCellsAs2".
    mergeEmptyRowSpace = function(value) {
      if(missing(value)) return(invisible(private$p_mergeEmptyRowSpace))
      else {
        if(private$p_argumentCheckMode > 0) {
          checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "mergeEmptyRowSpace", value, missing(value), allowMissing=FALSE, allowNull=FALSE, allowedClasses="character", allowedValues=c("doNotMerge", "dataGroupsOnly", "cellsOnly", "dataGroupsAndCellsAs1", "dataGroupsAndCellsAs2"))
        }
        private$p_mergeEmptyRowSpace <- value
        return(invisible())
      }
    },

    #' @field mergeEmptyColumnSpace A character value describing how empty
    #' space is merged.  Allowed values:  "doNotMerge", "dataGroupsOnly",
    #' "cellsOnly", "dataGroupsAndCellsAs1", "dataGroupsAndCellsAs2".
    mergeEmptyColumnSpace = function(value) {
      if(missing(value)) return(invisible(private$p_mergeEmptyColumnSpace))
      else {
        if(private$p_argumentCheckMode > 0) {
          checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "mergeEmptyColumnSpace", value, missing(value), allowMissing=FALSE, allowNull=FALSE, allowedClasses="character", allowedValues=c("doNotMerge", "dataGroupsOnly", "cellsOnly", "dataGroupsAndCellsAs1", "dataGroupsAndCellsAs2"))
        }
        private$p_mergeEmptyColumnSpace <- value
        return(invisible())
      }
    },

    #' @field mergeEmptySpaceDirection A character value describing how empty
    #' space is merged.  Allowed values:  "row" or "column"
    mergeEmptySpaceDirection = function(value) {
      if(missing(value)) return(invisible(private$p_mergeEmptySpaceDirection))
      else {
        if(private$p_argumentCheckMode > 0) {
          checkArgument(private$p_argumentCheckMode, TRUE, "PivotTable", "mergeEmptySpaceDirection", value, missing(value), allowMissing=FALSE, allowNull=FALSE, allowedClasses="character", allowedValues=c("row", "column"))
        }
        private$p_mergeEmptySpaceDirection <- value
        return(invisible())
      }
    },

    #' @field allTimings Get a data frame containing timing details
    #' of pivot table operations.
    allTimings = function(value) {
      descriptions <- sapply(private$p_timings, function(x) { return(ifelse(is.null(x$desc), NA, x$desc)) })
      user <- sapply(private$p_timings, function(x) { return(ifelse(is.null(x$time["user.self"]), NA, x$time["user.self"])) })
      system <- sapply(private$p_timings, function(x) { return(ifelse(is.null(x$time["sys.self"]), NA, x$time["sys.self"])) })
      elapsed <- sapply(private$p_timings, function(x) { return(ifelse(is.null(x$time["elapsed"]), NA, x$time["elapsed"])) })
      return(data.frame(action=descriptions, user=user, system=system, elapsed=elapsed))
    },

    #' @field significantTimings Get a data frame containing timing details
    #' of significant pivot table operations (i.e. where elapsed>0.1).
    significantTimings = function(value) {
      df <- self$allTimings
      df <- df[df$elapsed>0.1, ]
      return(df)
    }
  ),
  private = list(
    p_argumentCheckMode = 4,
    p_traceEnabled = FALSE,
    p_processingLibrary = NULL,
    p_defaults = NULL,
    p_lastInstanceId = NULL,
    p_data = NULL,
    p_styles = NULL,
    p_rowGroup = NULL,
    p_columnGroup = NULL,
    p_rowGrpHeaders = NULL,
    p_calculationsPosition = NULL,
    p_calculationGroups = NULL,
    p_calculationsSet = FALSE,
    p_evaluationMode = "batch",
    p_evaluated = FALSE,
    p_cells = NULL,
    p_lastCellBatchInfo = NULL,
    p_fixedWidthSized = FALSE,
    p_mergeEmptyRowSpace = "dataGroupsAndCellsAs2",
    p_mergeEmptyColumnSpace = "dataGroupsAndCellsAs2",
    p_mergeEmptySpaceDirection = "row",
    p_htmlRenderer = NULL,
    p_latexRenderer = NULL,
    p_openxlsxRenderer = NULL,
    p_compatibility = NULL,
    p_traceFile = NULL,
    p_timings = NULL,
    clearIsRenderedFlags = function() {
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$clearIsRenderedFlags", "Clearing isRendered flags...")
      rowGroups <- self$rowGroup$getDescendantGroups(includeCurrentGroup=TRUE)
      lapply(rowGroups, function(grp) { grp$isRendered <- FALSE })
      columnGroups <- self$columnGroup$getDescendantGroups(includeCurrentGroup=TRUE)
      lapply(columnGroups, function(grp) { grp$isRendered <- FALSE })
      if(private$p_traceEnabled==TRUE) self$trace("PivotTable$clearIsRenderedFlags", "Cleared isRendered flags...")
      return(invisible())
    },
    # simple mechanism to track the activities/time taken to construct the pivot table
    # should only be tracking top-level actions (i.e. methods on the pivot table) not functions which return a value (as these could be called multiple times)
    addTiming = function(descr, timeStart) {
      timeEnd <- proc.time()
      private$p_timings[[length(private$p_timings)+1]] <- list(descr=descr, time=timeEnd-timeStart)
    },
    applyBorderToAdjacentCells = function(cell, declarations) {
      # get border properties
      border <- declarations$border
      borderTop <- declarations[["border-top"]]
      borderRight <- declarations[["border-right"]]
      borderBottom <- declarations[["border-bottom"]]
      borderLeft <- declarations[["border-left"]]
      xlBorder <- declarations[["xl-border"]]
      xlBorderTop <- declarations[["xl-border-top"]]
      xlBorderRight <- declarations[["xl-border-right"]]
      xlBorderBottom <- declarations[["xl-border-bottom"]]
      xlBorderLeft <- declarations[["xl-border-left"]]
      r <- cell$rowNumber
      c <- cell$columnNumber
      # bottom border of cell above
      if (r > 1) {
        cl <- private$p_cells$getCell(r=r-1, c=c)
        if (!is.null(border)) { self$setStyling(cells=cl, declarations=list("border-bottom"=border)) }
        if (!is.null(borderTop)) { self$setStyling(cells=cl, declarations=list("border-bottom"=borderTop)) }
        if (!is.null(xlBorder)) { self$setStyling(cells=cl, declarations=list("xl-border-bottom"=xlBorder)) }
        if (!is.null(xlBorderTop)) { self$setStyling(cells=cl, declarations=list("xl-border-bottom"=xlBorderTop)) }
      }
      # left border of cell to the right
      if (c < private$p_cells$columnCount) {
        cl <- private$p_cells$getCell(r=r, c=c+1)
        if (!is.null(border)) { self$setStyling(cells=cl, declarations=list("border-left"=border)) }
        if (!is.null(borderRight)) { self$setStyling(cells=cl, declarations=list("border-left"=borderRight)) }
        if (!is.null(xlBorder)) { self$setStyling(cells=cl, declarations=list("xl-border-left"=xlBorder)) }
        if (!is.null(xlBorderRight)) { self$setStyling(cells=cl, declarations=list("xl-border-left"=xlBorderRight)) }
      }
      # top border of the cell below
      if (r < private$p_cells$rowCount) {
        cl <- private$p_cells$getCell(r=r+1, c=c)
        if (!is.null(border)) { self$setStyling(cells=cl, declarations=list("border-top"=border)) }
        if (!is.null(borderBottom)) { self$setStyling(cells=cl, declarations=list("border-top"=borderBottom)) }
        if (!is.null(xlBorder)) { self$setStyling(cells=cl, declarations=list("xl-border-top"=xlBorder)) }
        if (!is.null(xlBorderBottom)) { self$setStyling(cells=cl, declarations=list("xl-border-top"=xlBorderBottom)) }
      }
      # right border of cell to the left
      if (c > 1) {
        cl <- private$p_cells$getCell(r=r, c=c-1)
        if (!is.null(border)) { self$setStyling(cells=cl, declarations=list("border-right"=border)) }
        if (!is.null(borderLeft)) { self$setStyling(cells=cl, declarations=list("border-right"=borderLeft)) }
        if (!is.null(xlBorder)) { self$setStyling(cells=cl, declarations=list("xl-border-right"=xlBorder)) }
        if (!is.null(xlBorderLeft)) { self$setStyling(cells=cl, declarations=list("xl-border-right"=xlBorderLeft)) }
      }
    }
  )
)
cbailiss/pivottabler documentation built on Oct. 14, 2023, 9:38 a.m.