lubridate period() in R: Calendar-Aware Time Spans
The period() function in lubridate builds a Period object that stores a time span as calendar units, years, months, weeks, days, hours, minutes, and seconds. Use it when you want "same day next month" or "one year later" arithmetic that respects daylight saving and variable month lengths.
period(1, units = "month") # one calendar month period("2 hours 30 mins") # parse a friendly string period(c(1, 2), c("year", "month")) # multi-unit Period years(1) + months(6) # 1.5 calendar years ymd("2024-01-31") + months(1) # safe-ish month math (returns NA) ymd("2024-03-09 22:00", tz="US/Eastern") + days(1) # DST-aware shift period_to_seconds(period("1 week")) # 604800 as.period(interval(t1, t2)) # difference as Period
Need explanation? Read on for examples and pitfalls.
What period() does in one sentence
period() constructs a calendar-aware time span stored as a Period object. You pass a numeric vector of values plus a parallel vector of unit names, or a single descriptive string, and lubridate returns a Period that records each unit separately rather than collapsing the span to a single second count.
A Period holds years, months, weeks, days, hours, minutes, and seconds in distinct slots. When you add a Period to a date or POSIXct value, lubridate consults the calendar and the time zone, so months(1) lands on the same day of the next month and days(1) advances the clock through any daylight-saving transition.
Syntax
period(num = NULL, units = "second", ...) accepts a numeric vector and a parallel units vector, or a single parsable string. The function recognises units "second", "minute", "hour", "day", "week", "month", and "year", plus their plural forms.
The print method shows each unit with a single-letter suffix: y for year, m for month, d for day, then H, M, S for clock units. The lowercase m for month and uppercase M for minute are the visual signal that distinguishes the two.
period() when "the same calendar position" is the right semantic. Monthly subscriptions, recurring meetings, anniversary dates, and amortisation schedules all care about landing on the same day of the next month, not on the same number of elapsed seconds. Use a Duration when "exactly N seconds from now" is what you really mean.Six common patterns
1. Build a Period from numeric value and unit
Pass parallel vectors when you need a multi-unit Period. The two vectors must be the same length; lubridate fills the unused unit slots with zeros.
2. Parse a friendly string
The parser collapses weeks into days because the Period class has no separate week slot internally. weeks(1) is sugar for days(7) when stored in a Period.
3. Use the unit-name helper family
The helpers read more naturally inside expressions. months(6) is identical to period(6, "month"), and you can add them: years(1) + months(6) produces a 1.5 calendar year Period.
d prefix) build Periods, the d-prefixed helpers build Durations. Compare days(7) (a Period of 7 calendar days that respects DST) with ddays(7) (a Duration of exactly 7 times 86,400 seconds). The two diverge near time zone transitions and on leap days.4. Add a Period to a POSIXct timestamp across a DST boundary
The two results differ by one hour because the US "spring forward" jump happens that night. days(1) adds one calendar day and lets the clock follow the time zone rule. ddays(1) adds 86,400 raw seconds and the clock lands one hour past midnight.
5. Get a Period from an Interval
as.period() breaks the elapsed time into the largest whole calendar units that fit, walking from years down to seconds. The result is readable for humans and stable across leap years.
6. Vectorise across a column
months() is vectorised, so it works inside mutate() or any column-wise assignment without a loop. The second row returns NA because April has no 31st day, which is the canonical Period pitfall covered below.
Period vs Duration: when the calendar matters
Use a Period for human calendar units; use a Duration for elapsed clock seconds. The two classes look similar in print but answer different questions about time.
| Concept | Period | Duration |
|---|---|---|
| Stored as | Calendar units (years, months, days, hours, ...) | Exact seconds |
| Constructor | period(), years(), months(), days() |
duration(), dyears(), ddays() |
| One month equals | One calendar month (28-31 days) | 30.4375 days fixed |
| DST behaviour | Respects time zone rules | Adds raw seconds, clock shifts |
| Right for | Anniversaries, billing cycles, due dates | Stopwatches, timeouts, science |
The rule of thumb: if you care that the answer lands on a particular calendar date, use a Period. If you care that the answer is the same elapsed time on a stopwatch, use a Duration. See the companion guide lubridate duration() in R for the Duration-first view.
Date returns a Date; a Period plus a POSIXct returns a POSIXct. lubridate preserves the input class so date-only arithmetic stays date-only and you do not silently grow a time component you did not ask for.Common pitfalls
Adding a month to a month-end date can return NA. ymd("2024-01-31") + months(1) returns NA because February has no 31st day and lubridate refuses to silently roll forward. Use the safe addition operator %m+% if you want lubridate to clamp the result to the last day of the target month: ymd("2024-01-31") %m+% months(1) returns "2024-02-29".
Comparing two Periods by length is undefined when months are involved. months(1) > days(28) warns because one month can be 28 to 31 days. Convert to a Duration with as.duration() or anchor the comparison to a specific date with an Interval.
Confusing the lowercase m for month with uppercase M for minute in the print method. A Period printed as "1m 0d 0H 0M 0S" is one month plus zero days, zero hours, zero minutes, and zero seconds. The uppercase M only ever means minutes.
Try it yourself
Try it: Build a Period of 2 months and 15 days, add it to ymd("2024-01-31"), and store the result in ex_due. Then add the same Period to the same date with %m+% and store the result in ex_due_safe. Compare the two.
Click to reveal solution
Explanation: Adding 2 months to January 31 lands on February 31, which does not exist, so + returns NA. The safe operator %m+% clamps to the last valid day of the target month before adding the 15 extra days, giving a sensible billing date.
Related lubridate functions
duration()andddays(),dhours(),dyears()build exact-second Durations (see lubridate duration() in R).interval()builds an Interval bounded by two timestamps; convert to a Period withas.period().%m+%and%m-%are safe month-aware addition operators that clamp invalid dates.time_length()returns a numeric length of an Interval in any unit, including months and years.period_to_seconds()collapses a Period to a single second count using fixed conversion factors.
For the full lubridate reference, see the tidyverse lubridate documentation.
FAQ
What is the difference between period() and duration() in lubridate?
period() returns a Period that stores time as calendar units (years, months, days, hours, minutes, seconds), while duration() returns a Duration that stores time as an exact second count. A Period of one month is one calendar month, which can be 28 to 31 days. A Duration of one month is fixed at 30.4375 days (2,629,800 seconds). Pick Period for date arithmetic that should respect the calendar; pick Duration for elapsed time on a stopwatch.
How do I add months to a date in R without getting NA?
Use the safe addition operator %m+% from lubridate. The expression ymd("2024-01-31") %m+% months(1) returns "2024-02-29" by clamping the result to the last valid day of the target month, while ymd("2024-01-31") + months(1) returns NA because February has no 31st day. The %m-% operator does the same for subtraction.
Can I add a Period to a base R Date object?
Yes. Adding a Period to a Date returns a Date, and adding a Period to a POSIXct returns a POSIXct. lubridate preserves the input class. If the Period contains time-of-day units (hours, minutes, seconds) and you add it to a Date, lubridate ignores the sub-day part and returns a Date rather than silently promoting the result to POSIXct.
Why does months(1) sometimes change the displayed clock time on a POSIXct?
It does not change the wall-clock reading, but daylight saving shifts the time zone label. Adding months(1) to ymd_hms("2024-02-15 12:00", tz = "US/Eastern") returns "2024-03-15 12:00 EDT". The noon reading is preserved even though March is in EDT and February in EST, because Period arithmetic respects the zone.
How do I get the length of a Period in days?
Use period_to_seconds() divided by 86,400, or call time_length() on an anchored Interval. period_to_seconds(period("1 month")) / 86400 returns 30.4375 (lubridate's fixed conversion). For a calendar-correct count, build an Interval first: time_length(interval(start, start + months(1)), "days").