best practices for avoiding roundoff gotchas in date manipulation

Viewed 560

I am doing some date/time manipulation and experiencing explicable, but unpleasant, round-tripping problems when converting date -> time -> date . I have temporarily overcome this problem by rounding at appropriate points, but I wonder if there are best practices for date handling that would be cleaner. I'm using a mix of base-R and lubridate functions.

tl;dr is there a good, simple way to convert from decimal date (YYYY.fff) to the Date class (and back) without going through POSIXt and incurring round-off (and potentially time-zone) complications??

Start with a few days from 1918, as separate year/month/day columns (not a critical part of my problem, but it's where my pipeline happens to start):

library(lubridate)
dd <- data.frame(year=1918,month=9,day=1:12)

Convert year/month/day -> date -> time:

dd <- transform(dd,
                time=decimal_date(make_date(year, month, day)))

The successive differences in the resulting time vector are not exactly 1 because of roundoff: this is understandable but leads to problems down the road.

table(diff(dd$time)*365)
## 0.999999999985448  1.00000000006844 
##                 9                 2 

Now suppose I convert back to a date: the dates are slightly before or after midnight (off by <1 second in either direction):

d2 <- lubridate::date_decimal(dd$time)
#  [1] "1918-09-01 00:00:00 UTC" "1918-09-02 00:00:00 UTC"
#  [3] "1918-09-03 00:00:00 UTC" "1918-09-03 23:59:59 UTC"
#  [5] "1918-09-04 23:59:59 UTC" "1918-09-05 23:59:59 UTC"
#  [7] "1918-09-07 00:00:00 UTC" "1918-09-08 00:00:00 UTC"
#  [9] "1918-09-09 00:00:00 UTC" "1918-09-09 23:59:59 UTC"
# [11] "1918-09-10 23:59:59 UTC" "1918-09-12 00:00:00 UTC"

If I now want dates (rather than POSIXct objects) I can use as.Date(), but to my dismay as.Date() truncates rather than rounding ...

tt <- as.Date(d2)
## [1] "1918-09-01" "1918-09-02" "1918-09-03" "1918-09-03" "1918-09-04"
## [6] "1918-09-05" "1918-09-07" "1918-09-08" "1918-09-09" "1918-09-09"
##[11] "1918-09-10" "1918-09-12"

So the differences are now 0/1/2 days:

table(diff(tt))
# 0 1 2 
# 2 7 2 

I can fix this by rounding first:

table(diff(as.Date(round(d2))))
## 1 
## 11

but I wonder if there is a better way (e.g. keeping POSIXct out of my pipeline and staying with dates ...

As suggested by this R-help desk article from 2004 by Grothendieck and Petzoldt:

When considering which class to use, always choose the least complex class that will support the application. That is, use Date if possible, otherwise use chron and otherwise use the POSIX classes. Such a strategy will greatly reduce the potential for error and increase the reliability of your application.

The extensive table in this article shows how to translate among Date, chron, and POSIXct, but doesn't include decimal time as one of the candidates ...

2 Answers
Related