lubridate ceiling_date() in R: Snap Dates to Period End
The ceiling_date() function in lubridate rounds a Date or POSIXct value up to the start of the next time unit, such as the next day, week, month, quarter, or 15-minute slot. It is the canonical tool for computing month-end deadlines, fiscal period boundaries, and the next occurrence of a calendar event.
ceiling_date(x, "day") # next midnight ceiling_date(x, "week") # next Sunday (default) ceiling_date(x, "week", week_start = 1) # next Monday ceiling_date(x, "month") # first day of next month ceiling_date(x, "month") - days(1) # last day of THIS month ceiling_date(x, "quarter") # start of next quarter ceiling_date(x, "year") # Jan 1 of next year ceiling_date(x, "hour", change_on_boundary = FALSE) # keep value if on boundary ceiling_date(x, "15 mins") # next 15-minute slot
Need explanation? Read on for examples and pitfalls.
What ceiling_date() does in one sentence
ceiling_date(x, unit) returns the smallest date or date-time at or after x that aligns with the chosen unit boundary. Pass a Date or POSIXct vector and a unit string; the function snaps each value forward to the start of the next bucket. February 14, 2024 with unit = "month" becomes March 1, 2024. A timestamp of 14:37:22 with unit = "15 mins" becomes 14:45:00.
The return class follows the input. A Date in produces a Date out when the unit is "day" or larger; a POSIXct in stays POSIXct. Sub-day units force the result to POSIXct because Date cannot represent intra-day time.
Syntax
ceiling_date(x, unit = "seconds", change_on_boundary = NULL, week_start = getOption("lubridate.week.start", 7)) takes four arguments. x is the input vector. unit names the rounding unit, optionally prefixed with a positive integer multiple. change_on_boundary controls behaviour when the input already sits exactly on a boundary. week_start defines the first day of the week and only matters when unit = "week".
Valid unit values include "second", "minute", "hour", "day", "week", "month", "bimonth", "quarter", "season", "halfyear", and "year". Multi-unit buckets work via integer prefix: "15 mins", "2 hours", "3 days".
A Date input with a whole-day unit returns a Date. A POSIXct input keeps time-of-day precision. The 15-minute bucket lands on the smallest multiple of 15 minutes at or after the timestamp.
ceiling_date(d, "month") - days(1) gives the last calendar day of the current month. The same trick with "quarter", "year", or "week" returns the last day of any period. This pattern avoids hardcoding 28, 30, or 31 day months and handles leap years correctly.Five common patterns
1. Last day of the month
This idiom handles every edge case: 31-day months, a 29-day leap February, and the year-end roll. The helper rollback(ceiling_date(d, "month")) returns the same answer.
2. Days remaining in the quarter
Useful for sales dashboards and fiscal close countdowns. ceiling_date(x, "quarter") returns the first day of the next quarter; subtracting days(1) lands on the current quarter end, and subtracting today_date gives the days remaining.
3. The change_on_boundary trick
The default is TRUE for Date input (always advance) and FALSE for POSIXct (stay put on the boundary). This asymmetry surprises many users; pass the argument explicitly when edge cases matter.
4. Next occurrence of a calendar event
week_start = 1 defines Monday-start weeks. With change_on_boundary = FALSE, dates already on a Monday stay put. This is the lubridate way to ask for the upcoming Monday.
5. Aligning two time series to common period ends
The 23:30 reading on Feb 29 stays in February; the 00:30 reading on March 1 lands in March. ceiling_date(ts, "month") - seconds(1) produces the inclusive end-of-month POSIXct for joining to a period-end calendar.
floor_date(x, "month"). When you need the first day of the next month, the last day of the current month, or the time until a period closes, use ceiling_date(x, "month"). The two functions are complementary halves of the same idea: snap a value to a period grid, looking backwards or forwards.ceiling_date() vs floor_date() vs round_date()
The three lubridate rounders differ only in which boundary they pick. For a timestamp midway through a period, each returns a different result.
| Function | Direction | 14:37:22 with "hour" | Pick when |
|---|---|---|---|
floor_date() |
Backward | 14:00:00 | Grouping into period buckets |
ceiling_date() |
Forward | 15:00:00 | Deadlines, period-end math, next-event |
round_date() |
Nearest | 15:00:00 | Visual binning, half-hour rounding |
floor_date() always rounds down and ceiling_date() always rounds up. round_date() picks whichever boundary is closer: the 14:10 timestamp rounds down to 14:00, and 14:37 rounds up to 15:00.
Common pitfalls
Pitfall 1: passing a character. ceiling_date("2024-02-14", "month") errors. Wrap the input with ymd(), as.Date(), or ymd_hms() first.
Pitfall 2: forgetting the change_on_boundary asymmetry. A Date already on the boundary advances by default; a POSIXct already on the boundary does not. Set the argument explicitly when correctness on boundary inputs matters.
Pitfall 3: confusing month-end with ceiling_date(). ceiling_date(ymd("2024-02-14"), "month") returns March 1, not February 29. For the last day of the input month, subtract one day: ceiling_date(d, "month") - days(1) or call rollback(ceiling_date(d, "month")).
Pitfall 4: multi-unit buckets anchor on the Unix epoch. ceiling_date(x, "2 weeks") snaps to a 2-week grid aligned to 1970-01-01, not a custom start date. Use floor_date(x, "week") + weeks(2) for a data-anchored alignment.
ceiling_date(as.Date("2024-02-14"), "hour") returns a POSIXct at UTC midnight of the next day, not a Date. The class change can silently break joins, ggplot scales, or as.Date() round trips. Keep the input as POSIXct from the start, or stick to whole-day units (day, week, month, quarter, year) when the output must remain a Date.ceiling_date(x, "month") is s.dt.to_period("M").dt.to_timestamp() + pd.offsets.MonthBegin(1) or s + pd.offsets.MonthEnd(0) + pd.Timedelta(days=1). For sub-day units, pandas exposes s.dt.ceil("15min"), mirroring ceiling_date(x, "15 mins"). The lubridate version handles week, quarter, bimonth, and halfyear in one call.A practical workflow with ceiling_date()
Period-end math appears in three places: deadlines, billing cycles, and event scheduling. Deadlines count days until a fiscal close. Billing cycles snap a service-start date to the next invoice anniversary. Scheduling finds the next standup or review on or after a date.
Each row gets its first full billing month, the next January 1 renewal, and a countdown. The leap-day customer (B) snaps cleanly to March 1.
Try it yourself
Try it: Take the airquality dataset, build a Date column from Month and Day (year 1973), then compute the month-end date and the days remaining until month-end for each row. Save the result to ex_air_with_end.
Click to reveal solution
Explanation: ceiling_date(date, "month") - days(1) returns the last day of each row's month, handling 30 versus 31 day months automatically. Subtracting date gives the integer count of days remaining until month-end, which is a useful feature for time-decay models and end-of-period reporting.
Related lubridate functions
After mastering ceiling_date(), look at:
floor_date(),round_date(): snap down or to nearest periodrollback(),rollforward(): jump to the last or first day of an adjacent monthyear(),month(),quarter(): extract calendar parts as integersmake_date(),make_datetime(): build Dates from componentswith_tz(),force_tz(): align time zone before roundingdays(),weeks(),months(),years(): build offset periods for arithmetic
See the lubridate round_date documentation for the official reference covering all three rounders.
FAQ
What does ceiling_date() do in R?
ceiling_date(x, unit) rounds a Date or POSIXct value up to the smallest unit boundary at or after x. With unit = "month", February 14 becomes March 1. With unit = "15 mins", 14:37:22 becomes 14:45:00. The function is vectorised and returns the same class as the input, except sub-day units which return POSIXct.
How do I get the last day of the month in R?
Subtract one day from ceiling_date() with unit = "month": ceiling_date(d, "month") - days(1). This handles 28, 29, 30, and 31 day months correctly, including leap years. The helper rollback(ceiling_date(d, "month")) returns the same answer. Both work for vectors of dates and POSIXct timestamps.
What is the difference between ceiling_date() and floor_date()?
ceiling_date() rounds up to the boundary at or after x; floor_date() rounds down to the boundary at or before x. For 2024-02-14 with unit = "month", ceiling returns 2024-03-01 and floor returns 2024-02-01. Pick ceiling for deadlines, period-end math, and next-event calculations; pick floor for grouping into period buckets.
What does change_on_boundary do in ceiling_date()?
change_on_boundary controls behaviour when x already sits exactly on a unit boundary. TRUE always advances by one period; FALSE keeps x unchanged. The default is TRUE for Date input and FALSE for POSIXct input. Pass it explicitly when boundary correctness matters, especially in scheduling code where a midnight timestamp should or should not advance to the next day.
Does ceiling_date() respect time zones?
Yes, but only the time zone of x. ceiling_date(t, "day") snaps to midnight in tz(t), which may differ from your local midnight if t is UTC. Call with_tz(t, "America/Los_Angeles") before rounding when the local calendar boundary matters.