diff --git a/NEWS.md b/NEWS.md index daa815151..938057679 100644 --- a/NEWS.md +++ b/NEWS.md @@ -32,6 +32,8 @@ 6. `yearqtr()` and `yearmon()` now gain an optional format specifier [#7694](https://github.com/Rdatatable/data.table/issues/7694). 'numeric' is the default, which preserves the original behavior, but 'character' formats `yearqtr()` as YYYYQ# (e.g. 2025Q2) and `yearmon()` as YYYYM## (e.g. 2025M02, 2025M10). Thanks to @jan-swissre for the report and @LunaticSage218 for the implementation. +7. `print.data.table()` gains a `show.ncols` argument and `datatable.show.ncols` option to print the number of columns (labeled as `ncol:`) in the header, [#6663](https://github.com/Rdatatable/data.table/issues/6663). When `trunc.cols=TRUE`, this information is merged into the truncation message at the top, and the redundant footer message is suppressed. Thanks to @KyleHaynes for the suggestion and @venom1204 for the implementation. + ### BUG FIXES 1. `fread()` with `skip=0` and `(header=TRUE|FALSE)` no longer skips the first row when it has fewer fields than subsequent rows, [#7463](https://github.com/Rdatatable/data.table/issues/7463). Thanks @emayerhofer for the report and @ben-schwen for the fix. diff --git a/R/onLoad.R b/R/onLoad.R index 46a9826fe..ec71252b7 100644 --- a/R/onLoad.R +++ b/R/onLoad.R @@ -90,6 +90,7 @@ datatable.print.keys=TRUE, # for print.data.table datatable.print.trunc.cols=FALSE, # for print.data.table datatable.show.indices=FALSE, # for print.data.table + datatable.show.ncols=FALSE, # for print.data.table datatable.allow.cartesian=FALSE, # datatable. datatable.join.many=TRUE, # mergelist, [.data.table #4383 #914 datatable.dfdispatchwarn=TRUE, # not a function argument diff --git a/R/print.data.table.R b/R/print.data.table.R index e602f80d6..797f89d4a 100644 --- a/R/print.data.table.R +++ b/R/print.data.table.R @@ -8,6 +8,7 @@ print.data.table = function(x, topn=getOption("datatable.print.topn"), print.keys=getOption("datatable.print.keys"), trunc.cols=getOption("datatable.print.trunc.cols"), show.indices=getOption("datatable.show.indices"), + show.ncols=getOption("datatable.show.ncols", FALSE), quote=FALSE, na.print=NULL, timezone=FALSE, ...) { @@ -43,6 +44,7 @@ print.data.table = function(x, topn=getOption("datatable.print.topn"), if (!is.numeric(topn)) topn = 5L topnmiss = missing(topn) topn = max(as.integer(topn),1L) + show_trunc_message = isTRUE(trunc.cols) if (print.keys) { if (!is.null(ky <- key(x))) catf("Key: <%s>\n", toString(ky)) @@ -52,6 +54,9 @@ print.data.table = function(x, topn=getOption("datatable.print.topn"), paste0("<", ixs, ">", collapse = ", ") )) } + if (show.ncols && !isTRUE(trunc.cols) && !any(dim(x)==0L)) { + trunc_cols_message(character(0), NULL, FALSE, "none", ncol=ncol(x)) + } if (any(dim(x)==0L)) { x_class = if (is.data.table(x)) "data.table" else "data.frame" # a data.frame could be passed to print.data.table() directly, #3363 if (all(dim(x)==0L)) { @@ -121,8 +126,13 @@ print.data.table = function(x, topn=getOption("datatable.print.topn"), cons_width = getOption("width") cols_to_print = widths < cons_width not_printed = colnames(toprint)[!cols_to_print] + if (show.ncols) { + trunc_cols_message(not_printed, abbs, class, col.names, ncol=ncol(x)) + show_trunc_message = FALSE + } + if (!any(cols_to_print)) { - trunc_cols_message(not_printed, abbs, class, col.names) + if (show_trunc_message) trunc_cols_message(not_printed, abbs, class, col.names) return(invisible(x)) } # When nrow(toprint) = 1, attributes get lost in the subset, @@ -134,7 +144,7 @@ print.data.table = function(x, topn=getOption("datatable.print.topn"), if (col.names != "none") cut_colnames = identity cut_colnames(print(x, right=TRUE, quote=quote, na.print=na.print)) # prints names of variables not shown in the print - if (trunc.cols) trunc_cols_message(not_printed, abbs, class, col.names) + if (show_trunc_message) trunc_cols_message(not_printed, abbs, class, col.names) } if (printdots) { if (isFALSE(row.names)) { @@ -291,14 +301,28 @@ toprint_subset = function(x, cols_to_print) { } } # message for when trunc.cols=TRUE and some columns are not printed -trunc_cols_message = function(not_printed, abbs, class, col.names){ +trunc_cols_message = function(not_printed, abbs, class, col.names, ncol=NULL){ n = length(not_printed) - if (class && col.names != "none") classes = paste0(" ", tail(abbs, n)) else classes = "" - catf( - ngettext(n, "%d variable not shown: %s\n", "%d variables not shown: %s\n"), - n, brackify(paste0(not_printed, classes)), - domain=NA - ) + if (is.null(ncol)) { + if (n == 0L) return() + if (class && col.names != "none") classes = paste0(" ", tail(abbs, n)) else classes = "" + catf( + ngettext(n, "%d variable not shown: %s\n", "%d variables not shown: %s\n"), + n, brackify(paste0(not_printed, classes)), + domain=NA + ) + } else { + if (n > 0L) { + if (class && col.names != "none") classes = paste0(" ", tail(abbs, n)) else classes = "" + catf( + ngettext(n, "Number of columns: %d, of which %d is not shown: %s\n", "Number of columns: %d, of which %d are not shown: %s\n"), + ncol, n, brackify(paste0(not_printed, classes)), + domain=NA + ) + } else { + catf("Number of columns: %d\n", ncol) + } + } } # Maybe add a method for repr::repr_text. See https://github.com/Rdatatable/data.table/issues/933#issuecomment-220237965 diff --git a/inst/tests/tests.Rraw b/inst/tests/tests.Rraw index 3f201f975..2b2e85706 100644 --- a/inst/tests/tests.Rraw +++ b/inst/tests/tests.Rraw @@ -21683,3 +21683,13 @@ max_ppsize = local({ x = as.data.table(as.list(1:max_ppsize)) test(2376, rbindlist(list(x)), x) rm(x, max_ppsize) + +#6663 Option to print the number of columns +test(2377.1, capture.output(print(as.data.table(iris)))[1L], "Number of columns: 5", options=list(datatable.show.ncols=TRUE)) +test(2377.2, any(grepl("^Number of columns: 5$", capture.output(print(setkey(as.data.table(iris), Sepal.Length))))), TRUE, options=list(datatable.show.ncols=TRUE)) +test(2377.3, any(grepl("^[0-9]+ variables? not shown", capture.output(print(data.table(a=1:3, b=1:3, c=1:3, d=1:3, e=1:3), trunc.cols=TRUE)))), FALSE, options=list(width=20, datatable.show.ncols=TRUE)) +test(2377.4, capture.output(print(as.data.table(iris), trunc.cols=TRUE))[1L], "Number of columns: 5, of which 5 are not shown: [Sepal.Length, Sepal.Width, Petal.Length, Petal.Width, Species]", options=list(width=10, datatable.show.ncols=TRUE)) +test(2377.5, any(grepl("^Number of columns:", capture.output(print(as.data.table(iris))))), FALSE, options=list(datatable.show.ncols=FALSE)) +test(2377.6, capture.output(print(as.data.table(iris), trunc.cols=TRUE, class=TRUE))[1L], "Number of columns: 5, of which 5 are not shown: [Sepal.Length , Sepal.Width , Petal.Length , Petal.Width , Species ]", options=list(width=10, datatable.show.ncols=TRUE)) +test(2377.7, { dt = data.table(matrix(1:200, nrow=25)); out = capture.output(print(dt, trunc.cols=TRUE, class=TRUE)); grepl("", out[length(out)]) }, TRUE, options=list(width=30, datatable.show.ncols=TRUE)) +test(2377.8, any(grepl("variable", capture.output(print(data.table(a=1), trunc.cols=TRUE)))), FALSE, options=list(width=80, datatable.show.ncols=FALSE)) diff --git a/man/data.table-options.Rd b/man/data.table-options.Rd index 439e88ef2..90ed4e5f0 100644 --- a/man/data.table-options.Rd +++ b/man/data.table-options.Rd @@ -33,6 +33,7 @@ \item{\code{datatable.print.keys}}{A logical, default \code{FALSE}. If \code{TRUE}, the table's keys are printed above the data.} \item{\code{datatable.show.indices}}{A logical, default \code{TRUE}. A synonym for \code{datatable.print.keys} for historical reasons.} + \item{\code{datatable.show.ncols}}{A logical, default \code{FALSE}. If \code{TRUE}, the number of columns is printed in the header.} \item{\code{datatable.print.trunc.cols}}{A logical, default \code{FALSE}. If \code{TRUE} and a table has more columns than fit on the screen, it truncates the middle columns.} \item{\code{datatable.prettyprint.char}}{An integer, default \code{100L}. The maximum number of diff --git a/man/print.data.table.Rd b/man/print.data.table.Rd index 442194b23..aa64b9c7f 100644 --- a/man/print.data.table.Rd +++ b/man/print.data.table.Rd @@ -25,6 +25,7 @@ print.keys=getOption("datatable.print.keys"), # default: TRUE trunc.cols=getOption("datatable.print.trunc.cols"), # default: FALSE show.indices=getOption("datatable.show.indices"), # default: FALSE + show.ncols=getOption("datatable.show.ncols", FALSE), # default: FALSE quote=FALSE, na.print=NULL, timezone=FALSE, \dots) @@ -44,6 +45,7 @@ \item{trunc.char}{The number of characters at which character columns and list-column summaries are truncated. If \code{NULL} (the default), it is dynamically calculated based on \code{getOption("width")}.} \item{class}{ If \code{TRUE}, the resulting output will include above each column its storage class (or a self-evident abbreviation thereof). When combined with \code{col.names="auto"} and tables >20 rows, classes will also appear at the bottom.} \item{row.names}{ If \code{TRUE}, row indices will be printed alongside \code{x}. } + \item{show.ncols}{ If \code{TRUE}, the number of columns is printed in the header. } \item{col.names}{ One of three flavours for controlling the display of column names in output. \code{"auto"} includes column names above the data, as well as below the table if \code{nrow(x) > 20} (when \code{class=TRUE}, column classes will also appear at the bottom). \code{"top"} excludes this lower register when applicable, and \code{"none"} suppresses column names altogether (as well as column classes if \code{class = TRUE}. } \item{print.keys}{ If \code{TRUE}, any \code{\link{key}} and/or \code{\link[=indices]{index}} currently assigned to \code{x} will be printed prior to the preview of the data. } \item{trunc.cols}{ If \code{TRUE}, only the columns that can be printed in the console without wrapping the columns to new lines will be printed (similar to \code{tibbles}). }