lubridate floor_date() in R: Snap Dates to Period Start
The floor_date() function in lubridate rounds a Date or POSIXct value down to the start of a chosen time unit, such as day, week, month, quarter, or a multiple like "15 mins". It is the canonical tool for grouping timestamps into period buckets, aligning time series to a regular grid, and computing the first day of the current period.
floor_date(x, "day") # truncate time, keep date floor_date(x, "week") # Sunday-start week by default floor_date(x, "week", week_start = 1) # Monday-start week floor_date(x, "month") # first day of month floor_date(x, "quarter") # start of Q1/Q2/Q3/Q4 floor_date(x, "year") # January 1 of the year floor_date(x, "15 mins") # 15-minute bucket floor_date(x, "2 hours") # 2-hour bucket floor_date(now(), "hour") # top of current hour
Need explanation? Read on for examples and pitfalls.
What floor_date() does in one sentence
floor_date(x, unit) returns the largest date or date-time at or before x that aligns with the chosen unit boundary. Pass a Date or POSIXct vector and a unit string, and the function snaps each value backwards to the start of that bucket. February 14, 2024 with unit = "month" becomes February 1, 2024. A timestamp of 14:37:22 with unit = "15 mins" becomes 14:30:00.
The return class follows the input. A Date in produces a Date out when the unit is "day" or larger; a POSIXct in produces a POSIXct out at all unit sizes. Sub-day units force the result to POSIXct because Date cannot represent intra-day time.
Syntax
floor_date(x, unit = "seconds", week_start = getOption("lubridate.week.start", 7)) takes three arguments. x is the input date or date-time vector. unit is a string naming the rounding unit, optionally prefixed with a positive integer multiple. week_start controls which weekday starts a week and only matters when unit = "week"; 1 means Monday, 7 means Sunday.
Valid unit values include "second", "minute", "hour", "day", "week", "month", "bimonth", "quarter", "season", "halfyear", and "year". Plural forms ("seconds", "days") and abbreviations ("sec", "min", "mins") all work. A leading integer creates multi-unit buckets: "15 mins", "2 hours", "3 days".
The Date input returns a Date because unit = "month" is whole-day. The POSIXct input keeps time-of-day precision, and the 15-minute bucket lands on the largest multiple of 15 minutes at or before the timestamp.
floor_date(date, "month") returns a single Date per month, perfect as a group_by() key for monthly rollups. Mixing year() + month() works but produces two columns; floor_date returns one Date that sorts correctly and joins cleanly against a calendar table.Five common patterns
1. Monthly rollup of a daily series
One Date column per month, ready to plot on a continuous time axis. floor_date(x, "month") returns the first day of the month, which sorts correctly and labels axes naturally.
2. Snap a timestamp to a 15-minute bucket
The third event lands exactly on the 09:15 boundary and stays there because floor_date() is inclusive on the left edge. The fourth event snaps to 09:45 rather than rounding up to 10:00.
3. Week starts on Monday, not Sunday
The default week_start = 7 snaps to Sunday. Pass week_start = 1 for Monday-start weeks, matching ISO 8601 and most European business calendars. Set options(lubridate.week.start = 1) once at the top of a script to make Monday the default for all calls.
4. First day of the current quarter
"quarter" snaps to January, April, July, or October. "bimonth" snaps to odd-numbered months (Jan, Mar, May, Jul, Sep, Nov). "halfyear" snaps to January or July. These are pre-baked aggregation buckets for fiscal reporting without writing custom logic.
5. floor_date() vs ceiling_date() vs round_date()
floor_date() always rounds down (toward the earlier boundary). ceiling_date() always rounds up. round_date() picks whichever boundary is closer, breaking ties to the even one. For grouping into period buckets, pick floor_date() so timestamps in 14:00 to 14:59 collapse to the 14:00 bucket consistently.
floor_date(x, "month") collapses many dates to one (the period start). Operators like x %m+% months(1) shift one date by one period and keep the day-of-month. They answer opposite questions: "which bucket?" versus "one period later". Mixing the two pulls dates onto unintended grids; choose floor_date() whenever the goal is to align values onto a regular time grid.floor_date() vs the base R alternatives
floor_date() competes with cut() and as.Date(format(...)) for time bucketing. Each base route produces a different output class.
| Style | Example | Returns | Reads best when |
|---|---|---|---|
floor_date() |
floor_date(x, "month") |
Date or POSIXct | Bucketing for group_by, time-axis plots |
cut() |
cut(x, breaks = "month") |
Factor | Histograms, ordered categorical analysis |
as.Date(format()) |
as.Date(format(x, "%Y-%m-01")) |
Date | Base R only, simple month bucket |
Both return first-of-month Dates. floor_date() wins on readability, handles non-month units (week, quarter, "15 mins") in one call, and accepts multi-unit prefixes that base routes cannot.
Common pitfalls
Pitfall 1: passing a character. floor_date("2024-02-14", "month") errors with a class mismatch. Wrap the input with ymd(), as.Date(), or ymd_hms() first.
Pitfall 2: forgetting that the week default is Sunday. floor_date(d, "week") returns the prior Sunday, not Monday. Pass week_start = 1 or set options(lubridate.week.start = 1) for ISO-aligned weeks.
Pitfall 3: time zone shifts at midnight. floor_date(t, "day") uses tz(t). If t is UTC and you live in Pacific, midnight UTC is 16:00 the prior day locally; the floored value crosses a calendar day. Call with_tz(t, "America/Los_Angeles") first when local-day buckets matter.
Pitfall 4: multi-unit buckets anchor on the Unix epoch. "2 weeks" aligns to 1970-01-01, not min(x). The first boundary may not match the start of your data.
floor_date(as.Date("2024-02-14"), "hour") returns a POSIXct at UTC midnight, not a Date. The implicit class change can break downstream code that expects a Date column (joins, ggplot scales). Either keep the input as POSIXct from the start, or stick to whole-day units like "day", "week", "month", "quarter", "year".floor_date(x, "month") is s.dt.to_period("M").dt.to_timestamp() or s.dt.floor("D") for the day case. For arbitrary frequencies, pandas uses s.dt.floor("15min") which mirrors floor_date(x, "15 mins"). The lubridate version handles week, quarter, bimonth, and halfyear in one call; pandas requires to_period() with an offset alias.A practical workflow with floor_date()
Time bucketing appears in three places: rollups, calendar joins, and event alignment. Rollups use group_by(period = floor_date(date, "week")) for one row per period. Calendar joins build a seq(start, end, by = "month") and left-join to fill missing periods. Event alignment floors two streams to the same bucket before joining.
The pattern guarantees every week in the range appears in the output, even weeks with zero sales. Without the calendar join, missing weeks silently drop from plots and bias every weekly metric downward.
Try it yourself
Try it: Take the airquality dataset, build a Date column from Month and Day (year 1973), then compute the monthly average Ozone using floor_date(). Save the result to ex_monthly_ozone.
Click to reveal solution
Explanation: floor_date(date, "month") collapses the 153 daily rows into one Date per month (May 1, June 1, ...). Grouping by that Date and calling mean(Ozone, na.rm = TRUE) produces the monthly average Ozone, peaking in July and August as expected.
Related lubridate functions
After mastering floor_date(), look at:
ceiling_date(),round_date(): round up or to nearest periodrollback(),rollforward(): snap to previous or next month endyear(),month(),quarter(): extract calendar parts as integersweek(),isoweek(),epiweek(): extract week-of-yearmake_date(),make_datetime(): build Dates from componentswith_tz(),force_tz(): align time zone before flooring
See the lubridate floor_date documentation for the official reference.
FAQ
What does floor_date() do in R?
floor_date(x, unit) rounds a Date or POSIXct down to the largest unit boundary at or before x. With unit = "month", February 14 becomes February 1. With unit = "15 mins", 14:37:22 becomes 14:30:00. The function is vectorised and returns the same class as the input, except sub-day units which return POSIXct.
How do I round a date to the nearest week in R?
For week snapping, call floor_date(date, "week") for the prior Sunday or floor_date(date, "week", week_start = 1) for the prior Monday. To round to the nearest week boundary instead, use round_date(date, "week"). To snap forward, use ceiling_date(date, "week"). All three accept week_start.
What is the difference between floor_date() and ceiling_date()?
floor_date() rounds down to the boundary at or before x; ceiling_date() rounds up to the boundary at or after x. For 2024-02-14 with unit = "month", floor returns 2024-02-01 and ceiling returns 2024-03-01. Pick floor for grouping into period buckets, ceiling for deadlines or month-end math.
Can floor_date() handle 15-minute buckets?
Yes. Pass a unit string with an integer prefix: floor_date(x, "15 mins"), floor_date(x, "30 mins"), or floor_date(x, "2 hours"). Any integer multiple of seconds, minutes, hours, days, or weeks works. Boundaries anchor on the Unix epoch, so verify the first bucket if exact calendar alignment matters.
Does floor_date() respect time zones?
Yes, but only the time zone of x. floor_date(t, "day") snaps to midnight in tz(t), which may differ from local midnight if t is stored in UTC. Call with_tz(t, "America/Los_Angeles") before flooring when the local calendar day matters.