lubridate years() in R: Add and Subtract Year Periods
The years() function in lubridate builds a Period object representing N calendar years. Add it to a Date or POSIXct to shift forward, subtract to shift backward, and pair it with %m+% when the start date is February 29 in a leap year.
years(1) # a 1-year period object ymd("2024-01-15") + years(1) # shift a date forward today() - years(5) # five years ago years(c(1, 5, 10)) # vector of period lengths ymd("2024-02-29") %m+% years(1) # safe leap-year rollover ymd("2024-02-29") + years(1) # NA (no Feb 29, 2025) df %>% mutate(anniv = start_date %m+% years(1)) # 1-year anniversary column interval(dob, today()) %/% years(1) # age in whole years
Need explanation? Read on for examples and pitfalls.
What years() does in one sentence
years() constructs a calendar-aware Period of N years that you can add to or subtract from a date. Pass an integer or numeric vector and lubridate returns a Period that respects calendar boundaries on Date or POSIXct values.
This differs from a fixed 365-day shift because calendar years are 365 or 366 days long. The trade-off is the leap-year rollover problem on February 29, which %m+% solves cleanly.
Syntax
years(x = 1) accepts a numeric vector and returns a Period of the same length. Default x is 1, so years() by itself is a one-year period. The result has class Period and prints with a y prefix to distinguish it from a Duration.
The "y" prefix confirms a Period, not a Duration. Periods track calendar units; Durations track exact seconds. For contract terms and anniversaries, Periods are what you want.
years() and year() are different functions one letter apart. Plural years() builds a Period for arithmetic; singular year() extracts the year integer from a date. Mixing them returns plausible but wrong results, so read the function name carefully every time.Six common patterns
1. Add N years to a date
The result is a Date that respects calendar boundaries. The same month and day appear N years later. No leap-year logic is needed for non-Feb-29 starts because the calendar handles it.
2. Subtract N years from a date
Both forms produce the same result. Negative values inside years() shift backwards, which helps when the offset comes from a column or variable that can carry either sign.
3. The leap-year rollover problem and the %m+% fix
Feb 29, 2024 plus one year would land on a non-existent Feb 29, 2025, so + returns NA. The %m+% operator rolls back to Feb 28. Adding years(4) lands on the next leap year, 2028, which has Feb 29 again.
4. Inside a dplyr pipeline
A five-year anniversary column is a textbook use of years(). %m+% survives the leap-day hire on row 2 by rolling to 2025-02-28; plain + years(5) would have returned NA there.
5. Generate an annual sequence
years(0:9) builds a length-10 Period vector. Paired with %m+%, this is the one-line way to produce annual snapshots from an anchor. Cleaner than seq.Date(start, by = "year", length.out = 10) for ggplot2 facets or feature engineering.
6. Compute age in whole years
Integer division of an interval by years(1) returns whole years between two dates. This is the standard lubridate idiom for age, tenure, and contract length, and it rolls down on partial years.
+ years() and %m+% years() is the choice between failing loudly and rolling silently. Plain + returns NA on impossible dates (February 29 plus one year), which surfaces the issue. The %m+% operator rolls back to February 28, which keeps pipelines running but hides the leap-year edge case. Pick the one that matches how you want to learn about Feb-29 inputs in your data.years() vs dyears() vs year()
Three calls that look almost identical do very different things in R.
| Call | Returns | One-year shift behaviour |
|---|---|---|
x + years(1) (Period) |
Date or POSIXct | Same calendar day next year, NA on Feb 29 |
x + dyears(1) (Duration) |
POSIXct | Adds 365.25 * 86400 seconds (a quarter day adrift) |
year(x) (extractor) |
Integer | n/a, returns the year number of the input |
years() is the calendar-aware Period; dyears() is the fixed-length Duration. The two diverge over leap years: years(4) lands on the same calendar date, while dyears(4) lands one day earlier because 365.25-day chunks fall short of four calendar years on non-leap quartets.
The Period stays on March 15. The Duration drifts back six hours because 2024 is a leap year and 365.25 days is shorter than the calendar gap. Prefer years() for human-facing dates; prefer dyears() for elapsed-time measurements.
Common pitfalls
Pitfall 1: relying on + years(N) for Feb-29 dates. ymd("2024-02-29") + years(1) returns NA. Switch to %m+% years(N) or add_with_rollback() whenever the start date might be the leap day. Build that into the pipeline by default.
Pitfall 2: confusing years() with year(). years(3) builds a Period for arithmetic; year(x) extracts the year integer. The names differ by one letter and a typo silently produces wrong groupings rather than an error.
Pitfall 3: treating one year as 365 days. years(1) is a calendar year, not a 365-day chunk. For literal 365-day shifts use days(365); mixing the two over a decade drifts by two or three days.
x %m+% years(3) is x + pd.DateOffset(years=3) on a Timestamp, which rolls Feb 29 back to Feb 28 in non-leap target years. The dyears() analogue is x + pd.Timedelta(days=365.25 * 3), but pandas does not separate Period and Duration as cleanly as lubridate.A practical workflow with years()
Year-level shifts show up in three places: anniversary and renewal columns, age and tenure features, and decade-level cohort buckets.
- Anniversary columns:
mutate(anniv = hire_date %m+% years(5))produces five-year service dates that survive Feb 29 hires. - Age and tenure:
mutate(age = interval(dob, today()) %/% years(1))is the canonical age formula in tidyverse pipelines. - Decade buckets:
mutate(decade = floor_date(event_date, "10 years"))collapses dates into their decade for long-horizon comparisons.
age returns whole years rounded down. decade_born snaps the birth date to the start of its decade, the right granularity for cohort analysis.
Try it yourself
Try it: Take the employees tibble from pattern 4 and add a ten_year column that is 10 calendar years after hire_date. Save the result to ex_ten_year.
Click to reveal solution
Explanation: hire_date %m+% years(10) shifts each date forward ten calendar years and rolls back to February 28 when the target year is not a leap year. Row 2 lands on 2030-02-28 because 2030 is not a leap year; rollover only triggers on Feb 29 starts.
Related lubridate functions
After mastering years(), look at:
days(),weeks(),months(): build Periods for other calendar unitsdyears(),dweeks(),ddays(): same N-unit shifts as exact-seconds Durations%m+%,%m-%,add_with_rollback(): Feb-29 safe arithmeticyear(): extract the year integer from a datefloor_date(),ceiling_date(): snap a date to a year, decade, or century boundaryinterval(),%within%: model a span from start to end, not an offsettoday(),now(): anchor expressions liketoday() %m+% years(-1)for trailing windows
For the official reference, see the lubridate period documentation.
FAQ
How do I add years to a date in R?
Use lubridate::years() with the %m+% operator: ymd("2024-01-15") %m+% years(5) returns "2029-01-15". Plain + years(5) works for most dates, but returns NA on February 29 inputs because the target year may not have a Feb 29. Defaulting to %m+% makes pipelines robust without changing the result on safe dates.
Why does ymd("2024-02-29") + years(1) return NA in R?
years(1) shifts the date forward one calendar year, which lands on February 29, 2025, a non-existent date. The plus operator returns NA rather than silently rolling to February 28. To roll back to the last valid day of February in the target year, use the %m+% operator: ymd("2024-02-29") %m+% years(1) returns "2025-02-28".
What is the difference between years() and dyears() in lubridate?
years() builds a Period of N calendar years; dyears() builds a Duration of N 365.25 86400 seconds. Periods track calendar units, so x + years(1) lands on the same month and day next year. Durations track exact seconds, so x + dyears(1) drifts by a quarter day around leap years. Use years() for human-facing dates and dyears() for elapsed-time calculations.
How do I calculate age in years from date of birth in R?
Use the interval-and-divide idiom: interval(dob, today()) %/% years(1). This returns the number of whole calendar years between the date of birth and today, rounded down. The integer division is critical; interval(dob, today()) / years(1) returns a fractional year, which is rarely what age formulas want.
How do I subtract years from a date in R?
Use the %m-% operator for safety: ymd("2024-02-29") %m-% years(1) returns "2023-02-28". The minus operator (without %m...%) also works, but returns NA on Feb 29 starts when the target year has no Feb 29. A negative argument works too: ymd("2024-07-15") %m+% years(-3) returns "2021-07-15".