22

I have the following data:

transaction <- c(1,2,3);
date <- c("2010-01-31","2010-02-28","2010-03-31");
type <- c("debit", "debit", "credit");
amount <- c(-500, -1000.97, 12500.81);
oldbalance <- c(5000, 4500, 17000.81)
evolution <- data.frame(transaction, date, type, amount, oldbalance, row.names=transaction, stringsAsFactors=FALSE);
evolution <- transform(evolution, newbalance = oldbalance + amount);
evolution

Running

> library(xtable)
> xtable(evolution)

works fine. But if I add the line

evolution$date <- as.Date(evolution$date, "%Y-%m-%d");

to give

transaction <- c(1,2,3);
date <- c("2010-01-31","2010-02-28","2010-03-31");
type <- c("debit", "debit", "credit");
amount <- c(-500, -1000.97, 12500.81);
oldbalance <- c(5000, 4500, 17000.81)
evolution <- data.frame(transaction, date, type, amount, oldbalance, row.names=transaction, stringsAsFactors=FALSE);
evolution$date <- as.Date(evolution$date, "%Y-%m-%d");
evolution <- transform(evolution, newbalance = oldbalance + amount);
evolution

then running xtable gives

xtable(evolution) Error in Math.Date(x + ifelse(x == 0, 1, 0)) : abs not defined for Date objects

But it can be useful to use xtable in such a case to do some filtering of dates

evolution$date <- as.Date(evolution$date, "%Y-%m-%d")
startdate <-as.Date("2010-02-01");
enddate <-as.Date("2010-03-30");
newdate <-evolution[which (evolution$date >= startdate & evolution$date <= enddate),]
newdate


> newdate
  transaction       date  type   amount oldbalance newbalance
2           2 2010-02-28 debit -1000.97       4500    3499.03
> xtable(newdate)
Error in Math.Date(x + ifelse(x == 0, 1, 0)) :
  abs not defined for Date objects
BenMorel
  • 34,448
  • 50
  • 182
  • 322
yCalleecharan
  • 4,656
  • 11
  • 56
  • 86

4 Answers4

25

This is arguably a bug in xtable - you may want to report it to the maintainer.

A temporary work-around is to call as.character() on the classes that xtable misinterprets (apart from "Date" I can think of "POSIXt" but there may be others), e.g.:

xtable <- function(x, ...) {
   for (i in which(sapply(x, function(y) !all(is.na(match(c("POSIXt","Date"),class(y))))))) x[[i]] <- as.character(x[[i]])
   xtable::xtable(x, ...)
}
Simon Urbanek
  • 13,842
  • 45
  • 45
13

It does appear that xtable does not always play nicely with columns of class Date. (It does have zoo and ts methods, but those may not help if you have a single column of dates/times in a data frame, as coercion to zoo appears to alter the column names in the resulting table.) A few notes:

  1. The error is actually being thrown by print.xtable, (not xtable.data.frame), which is called by default in order to display the results of xtable in the console. So you'd find that if you stored the results of xtable in a variable, you'd get no error, but then when you tried to print it, the same error would pop up.

  2. Since you've wisely stored your dates in YYYY-MM-DD format, converting them to Date objects actually isn't necessary to use ordered selections, since they will sort properly as characters. So you could actually get away with simply keeping them as characters.

  3. In cases with more complex date/time objects you could do the subsetting first and then convert those columns to characters. Or create a wrapper for xtable.data.frame and add the lines at the beginning,

    dates <- sapply(x,FUN = function(x){class(x) == "Date"})
    x[,dates] <- as.character(x[,dates])
    

    checking for class Date, or whatever class you're dealing with.

  4. IMHO, xtable.data.frame should probably be checking for Dates, and possibly for other POSIX classes as well and converting them to strings as well. This may be a simple change, and may be worth contacting the package author about.

  5. Lastly, the semicolons as line terminators are not necessary. :) Habit from another language?

joran
  • 169,992
  • 32
  • 429
  • 468
5

As the maintainer of xtable I would like to state what I see as the true position regarding dates in xtable.

This is not really a bug, but the absence of a feature you might think is desirable.

The problem is that xtable only can deal with three different classes of columns: logical; character; and numeric. If you try to submit a table where the class of a column is Date, then it cannot deal with it. The relevant code is the set of xtable methods, the most important of which are xtable.data.frame and xtable.matrix.

The first part of the code for those methods deals with checking the class of the columns being submitted so they can be treated appropriately.

It would be possible to add code to allow columns of class Date as well, but I am not willing to do that.

Firstly, there is an easy work around (at least for straight R code, I can't say for Shiny applications), which is to change any Date column to be a character column:

Second, to allow columns of class Date, would require the addition of an argument to xtable and xtable methods (of which there are currently 31) as well as to xtableFtable and xtableList. That is fraught with problems because of the large number of reverse dependencies for xtable. (Haven't counted, but if you look at xtable on CRAN you will see a stack of depends, imports and suggests.) I am going to break some packages, maybe a lot of packages if I make that sort of change. Backward compatibility is a serious problem with xtable.

Why is an extra argument necessary? Because the end result of using xtable, or more to the point print.xtable, is a string of characters. How the columns of the data frame, matrix or other structure submitted to xtable are treated is determined by firstly how they are classified (logical, character, or numeric), then by the arguments align, digits and display which can all be vectors to allow for different treatment of different columns. So if dates were to be allowed, you would need an extra argument to specify how they would be formatted, because at some point they need to be converted to character to produce the final table output.

David Scott
  • 51
  • 1
  • 1
  • Hmm, okay. I'll just have to remember to treat my date/POSIXct variables every time... – Kim Mar 17 '20 at 16:52
0

Same answer as above, but replace sapply with vapply, slightly safer. Creates a new function xtable2 so you can compare the output. Don't quite understand @David Scott's reluctance to put this idea in xtable.

library(xtable)
xtable2 <- function(x, ...) {
   # get the names of variables that are dates by inheritance
   datevars <- colnames(x)[vapply(x, function(y) {
       inherits(y, c("Date", "POSIXt", "POSIXct"))
   }, logical(1))]
   for (i in datevars){
        x[ , i] <- as.character(x[, i])
   }
   xtable::xtable(x, ...)
}

example

> str(dat)
'data.frame':   200 obs. of  9 variables:
 $ x5   : num  0.686 0.227 -1.762 0.963 -0.863 ...
 $ x4   : num  1 3 3 4 4 4 4 5 6 1 ...
 $ x3   : Ord.factor w/ 3 levels "med"<"lo"<"hi": 3 2 2 2 3 3 2 1 3 3 ...
 $ x2   : chr  "d" "c" "b" "d" ...
 $ x1   : Factor w/ 5 levels "bobby","cindy",..: 3 2 4 2 3 5 2 2 5 5 ...
 $ x7   : Ord.factor w/ 5 levels "a"<"b"<"c"<"d"<..: 4 2 2 2 4 5 4 5 5 4 ...
 $ x6   : int  5 4 2 3 4 1 4 3 4 2 ...
 $ date1: Date, format: "2020-03-04" "1999-01-01" ...
 $ date2: POSIXct, format: "2020-03-04" "2005-04-04" ...
> xtable2(dat)
% latex table generated in R 4.0.3 by xtable 1.8-4 package
% Wed Dec  9 08:59:07 2020
\begin{table}[ht]
\centering
\begin{tabular}{rrrllllrll}
  \hline
 & x5 & x4 & x3 & x2 & x1 & x7 & x6 & date1 & date2 \\ 
  \hline
1 & 0.69 & 1.00 & hi & d & greg & d &   5 & 2020-03-04 & 2020-03-04 \\ 
  2 & 0.23 & 3.00 & lo & c & cindy & b &   4 & 1999-01-01 & 2005-04-04 \\ 
  3 & -1.76 & 3.00 & lo & b & marcia & b &   2 & 2020-03-04 & 2020-03-04 \\ 
  4 & 0.96 & 4.00 & lo & d & cindy & b &   3 & 2020-03-04 & 2020-03-04 \\ 
  5 & -0.86 & 4.00 & hi & d & greg & d &   4 & 2005-04-04 & 2005-04-04 \\ 
  6 & -0.30 & 4.00 & hi & b & peter & f &   1 & 2005-04-04 & 2020-03-04 \\ 
  7 & -1.39 & 4.00 & lo & c & cindy & d &   4 & 1999-01-01 & 2005-04-04 \\ 
  8 & -1.71 & 5.00 & med & f & cindy & f &   3 & 2005-04-04 & 2020-03-04 \\ 
   [snip]
   \hline
\end{tabular}
\end{table}

pauljohn32
  • 2,079
  • 21
  • 28