How to prevent ifelse() from turning Date objects into numeric objects

You may use data.table::fifelse (data.table >= 1.12.3) or dplyr::if_else.


data.table::fifelse

Unlike ifelse, fifelse preserves the type and class of the inputs.

library(data.table)
dates <- fifelse(dates == '2011-01-01', dates - 1, dates)
str(dates)
# Date[1:5], format: "2010-12-31" "2011-01-02" "2011-01-03" "2011-01-04" "2011-01-05"

dplyr::if_else

From dplyr 0.5.0 release notes:

[if_else] have stricter semantics that ifelse(): the true and false arguments must be the same type. This gives a less surprising return type, and preserves S3 vectors like dates" .

library(dplyr)
dates <- if_else(dates == '2011-01-01', dates - 1, dates)
str(dates)
# Date[1:5], format: "2010-12-31" "2011-01-02" "2011-01-03" "2011-01-04" "2011-01-05" 

It relates to the documented Value of ifelse:

A vector of the same length and attributes (including dimensions and "class") as test and data values from the values of yes or no. The mode of the answer will be coerced from logical to accommodate first any values taken from yes and then any values taken from no.

Boiled down to its implications, ifelse makes factors lose their levels and Dates lose their class and only their mode ("numeric") is restored. Try this instead:

dates[dates == '2011-01-01'] <- dates[dates == '2011-01-01'] - 1
str(dates)
# Date[1:5], format: "2010-12-31" "2011-01-02" "2011-01-03" "2011-01-04" "2011-01-05"

You could create a safe.ifelse:

safe.ifelse <- function(cond, yes, no){ class.y <- class(yes)
                                  X <- ifelse(cond, yes, no)
                                  class(X) <- class.y; return(X)}

safe.ifelse(dates == '2011-01-01', dates - 1, dates)
# [1] "2010-12-31" "2011-01-02" "2011-01-03" "2011-01-04" "2011-01-05"

A later note: I see that Hadley has built an if_else into the the magrittr/dplyr/tidyr complex of data-shaping packages.


DWin's explanation is spot on. I fiddled and fought with this for a while before I realized I could simply force the class after the ifelse statement:

dates <- as.Date(c('2011-01-01','2011-01-02','2011-01-03','2011-01-04','2011-01-05'))
dates <- ifelse(dates=='2011-01-01',dates-1,dates)
str(dates)
class(dates)<- "Date"
str(dates)

At first this felt a little "hackish" to me. But now I just think of it as a small price to pay for the performance returns that I get from ifelse(). Plus it's still a lot more concise than a loop.