interval
Description
An Interval is an S4 class in lubridate that represents a span of time anchored to specific start and end moments. Unlike a Duration (which measures elapsed seconds regardless of calendar context), an Interval is calendar-aware: it preserves information about the real-world dates it spans, which matters when units like months or days are involved.
Intervals are defined by two POSIXct endpoints. The direction matters: an interval runs from start to end. If start is after end, the interval has negative length.
Creation
Use interval() with two date-time arguments, or the %--% operator as syntactic sugar:
interval(start, end, tzone = attr(start, "tzone"))
# [1] Interval, change: 183 days, 0 hours, 0 mins, 0 secs
start <- ymd_hms("2010-01-01 00:00:00")
end <- ymd_hms("2010-06-01 00:00:00")
int <- interval(start, end)
# [1] 2010-01-01 00:00:00 UTC--2010-06-01 00:00:00 UTC
start %--% end
# [1] 2010-01-01 00:00:00 UTC--2010-06-01 00:00:00 UTC
Both start and end are coerced to POSIXct via force_tz(). The tzone argument sets the time zone of the resulting interval (defaults to the tzone attribute of start).
If start and end have different timezone attributes, both are converted to the timezone of start before the interval is constructed:
start <- ymd_hms("2010-01-01 00:00:00", tz = "America/New_York")
end <- ymd_hms("2010-01-02 00:00:00", tz = "Europe/London")
int <- interval(start, end)
# Both converted to America/New_York
# [1] 2010-01-01 00:00:00 EST--2010-01-02 05:00:00 EST
int_length(int)
# [1] 86400 # exactly 24 hours (5-hour NY/London difference at this point)
If start > end, the interval has negative length:
interval(ymd_hms("2010-06-01"), ymd_hms("2010-01-01"))
# [1] 2010-06-01 00:00:00 UTC--2010-01-01 00:00:00 UTC
# negative length: -183 days
Since lubridate 1.7.2, intervals can be parsed directly from ISO 8601 interval notation:
as.interval("2007-03-01T13:00:00Z/2008-05-11T15:30:00Z")
# [1] 2007-03-01 13:00:00 UTC--2008-05-11 15:30:00 UTC
Operators
The %--% operator is the standard way to construct an interval:
ymd("2010-01-01") %--% ymd("2010-06-01")
# [1] 2010-01-01 UTC--2010-06-01 UTC
The %within% operator checks whether a datetime or interval falls entirely inside another interval:
int <- interval(ymd("2010-01-01"), ymd("2010-12-31"))
ymd("2010-06-01") %within% int
# [1] TRUE
ymd("2011-01-01") %within% int
# [1] FALSE
Compare intervals by their start points using int_start():
int1 <- interval(ymd("2010-01-01"), ymd("2010-06-01"))
int2 <- interval(ymd("2010-03-01"), ymd("2010-09-01"))
int_start(int1) < int_start(int2)
# [1] TRUE
Use is.interval() to test whether an object is an Interval:
is.interval(interval(ymd("2010-01-01"), ymd("2010-06-01")))
# [1] TRUE
is.interval(Sys.time())
# [1] FALSE
Accessors
Extract or manipulate the endpoints and metadata of an interval:
int <- interval(ymd_hms("2010-01-01 00:00:00"), ymd_hms("2010-06-01 12:00:00"))
int_start(int)
# [1] "2010-01-01 00:00:00 UTC"
int_end(int)
# [1] "2010-06-01 12:00:00 UTC"
int_length(int)
# [1] 13089600 # seconds (151 days + 12 hours)
int_length() returns elapsed seconds as a numeric vector. For a human-readable length in days:
int_length(int) / 86400
# [1] 151.5
int_flip() reverses the direction of an interval:
int_flip(int)
# [1] 2010-06-01 12:00:00 UTC--2010-01-01 00:00:00 UTC
int_shift() moves an interval by a specified period:
int <- interval(ymd("2010-01-01"), ymd("2010-03-01"))
int_shift(int, period = days(10))
# [1] 2010-01-11 UTC--2010-03-11 UTC
int_standardize() converts an interval to a positive-length equivalent with the same endpoints swapped:
int <- interval(ymd("2010-06-01"), ymd("2010-01-01"))
int_standardize(int)
# [1] 2010-01-01 UTC--2010-06-01 UTC
int_diff() takes a vector of times and returns the intervals between consecutive pairs:
times <- ymd_hms(c("2010-01-01 00:00:00", "2010-01-01 01:00:00", "2010-01-01 03:00:00"))
int_diff(times)
# [1] 2010-01-01 00:00:00 UTC--2010-01-01 01:00:00 UTC
# [2] 2010-01-01 01:00:00 UTC--2010-01-01 03:00:00 UTC
Overlap and Alignment
int_overlaps() returns TRUE if two intervals share any portion of the timeline:
int1 <- interval(ymd("2010-01-01"), ymd("2010-06-01"))
int2 <- interval(ymd("2010-03-01"), ymd("2010-09-01"))
int_overlaps(int1, int2)
# [1] TRUE
int3 <- interval(ymd("2010-07-01"), ymd("2010-12-01"))
int_overlaps(int1, int3)
# [1] FALSE
int_aligns() returns TRUE if two intervals share at least one endpoint:
int1 <- interval(ymd("2010-01-01"), ymd("2010-06-01"))
int2 <- interval(ymd("2010-06-01"), ymd("2010-09-01"))
int_aligns(int1, int2)
# [1] TRUE
intersect() returns the overlapping portion of two intervals, or NA if they do not overlap:
int1 <- interval(ymd("2010-01-01"), ymd("2010-06-01"))
int2 <- interval(ymd("2010-03-01"), ymd("2010-09-01"))
intersect(int1, int2)
# [1] 2010-03-01 UTC--2010-06-01 UTC
int3 <- interval(ymd("2010-07-01"), ymd("2010-12-01"))
intersect(int1, int3)
# [1] NA
Conversion
Convert an interval to a Duration (elapsed seconds) or Period (human-readable units):
int <- interval(ymd("2010-01-01"), ymd("2010-01-02"))
as.duration(int)
# [1] "86400s (~1 days)"
as.period(int)
# [1] "1d 0h 0m 0s"
Note that converting to a Period can produce surprising results when the interval spans a DST transition, because periods are calendar-based and account for hour-length changes:
# A 24-hour span that crosses a spring-forward DST transition
int <- interval(ymd_hms("2021-03-13 00:00:00", tz = "America/New_York"),
ymd_hms("2021-03-14 00:00:00", tz = "America/New_York"))
as.period(int)
# [1] "1d 1h 0m 0s" # 25 hours due to the lost hour
The reverse conversion (interval to duration) is exact and lossless:
as.interval(as.duration(interval(ymd("2010-01-01"), ymd("2010-01-02"))),
ymd("2010-01-01"))
# [1] 2010-01-01 UTC--2010-01-02 UTC
Gotchas
Negative intervals when start > end. The interval retains the original start/end order. Use int_standardize() to get a positive-length interval with swapped endpoints, or construct with the correct order to begin with:
int <- interval(ymd("2010-06-01"), ymd("2010-01-01"))
int_length(int)
# [1] -15552000 # seconds
DST gaps are not visible in interval length. At a spring-forward transition, clocks jump forward — the same clock hour is skipped. A 24-hour clock span across this transition covers 25 hours of real elapsed time. int_length() reports elapsed seconds (90000, or 25 hours), not clock hours:
int <- interval(ymd_hms("2010-03-14 01:00:00", tz = "America/New_York"),
ymd_hms("2010-03-15 01:00:00", tz = "America/New_York"))
int_length(int)
# [1] 90000 # 25 hours of real elapsed time (clocks spring forward)
Leap years with day units. Arithmetic on intervals using days() does not account for leap days. An interval spanning February 29 may not behave as expected when adding or subtracting day-based periods:
int <- interval(ymd("2020-02-28"), ymd("2020-03-01"))
# [1] 2020-02-28 UTC--2020-03-01 UTC (2 days in a leap year)
int <- interval(ymd("2019-02-28"), ymd("2019-03-01"))
# [1] 2019-02-28 UTC--2019-03-01 UTC (1 day in a non-leap year)
Month-boundary intervals with variable-length months. An interval of one month anchored to January 31 produces a different result than the same operation anchored to February 28:
interval(ymd("2021-01-31"), ymd("2021-02-28"))
# [1] 2021-01-31 UTC--2021-02-28 UTC (28 days)
interval(ymd("2021-01-31"), ymd("2021-03-31"))
# [1] 2021-01-31 UTC--2021-03-31 UTC (59 days)
Use as.period() to inspect the calendar interpretation of an interval’s length.
See Also
ymd()— parse date strings without timeymd_hms()— parse date-time strings with timezone supportdplyr::filter()— filter rows by conditions (often used with interval checks)