lubridate hour() in R: Extract Hour From Datetimes
The hour() function in lubridate returns the hour of a POSIXct or POSIXlt value as an integer between 0 and 23. It is vectorised over datetime columns, supports in-place replacement, and pairs with minute() and second() for full time-of-day extraction.
hour(ymd_hms("2024-07-15 14:30:00")) # 14 (2 PM as 24-hour integer)
hour(now()) # hour of current datetime
hour(as.POSIXct("2024-07-15 09:15:00")) # works on base POSIXct too
hour(c(ymd_hms("2024-07-15 09:00:00"),
ymd_hms("2024-07-15 21:30:00"))) # vectorised
hour(x) <- 0 # replace hour in place
df %>% mutate(h = hour(timestamp)) # extract a column
filter(logs, between(hour(ts), 9, 16)) # business-hours filter
table(hour(events$ts)) # hourly countsNeed explanation? Read on for examples and pitfalls.
What hour() does in one sentence
hour() returns the hour-of-day component of a datetime as an integer between 0 and 23. Pass any POSIXct or POSIXlt vector and you get a numeric vector of the same length, with midnight as 0 and 11 PM as 23.
This is the lubridate counterpart to base R's format(x, "%H") and as.POSIXlt(x)$hour. The lubridate version is shorter, vectorised, and returns an integer you can use directly in arithmetic, comparisons, or dplyr::filter().
Syntax
hour(x) accepts a datetime vector and returns an integer vector of the same length. There are no optional arguments. The replacement form hour(x) <- value overwrites the hour while preserving the date, minute, and second.
The input must be a class lubridate recognises as a datetime: POSIXct or POSIXlt. A plain Date has no time component, so hour(as.Date("2024-07-15")) returns 0 after silent coercion to midnight UTC, which is rarely what you want. Parse strings with ymd_hms(), mdy_hms(), or as.POSIXct() first.
hour(x) <- value to overwrite the hour in place. This is the replacement form. It keeps the date, minute, and second untouched, so you can snap an entire column of timestamps to 9 AM with one assignment without touching the rest of the timestamp.Six common patterns
1. Extract hour from a single datetime
The result is integer 14, not the string "14". Use it in arithmetic directly: hour(ts) - 12 returns 2, the same hour expressed in PM.
2. Extract hour from a vector of datetimes
hour() is fully vectorised. A million-row timestamp column becomes a million-row integer vector in one call. No loop or sapply() needed. Midnight returns 0, 11 PM returns 23, and the values are always integer.
3. Replace hour values in place
The replacement form keeps the date untouched. Pairing it with minute(x) <- 0 and second(x) <- 0 is the cleanest way to align a column of arbitrary timestamps to a fixed time-of-day without leaving the original date class.
4. Use hour() inside a dplyr pipeline
mutate(h = hour(ts)) adds an hour-of-day column. This is the start of any analysis that asks "when in the day do users hit the checkout flow?" or "what hour has the highest error rate?".
5. Filter rows by business hours
hour(ts) >= 9, hour(ts) < 17 evaluates row by row and keeps timestamps inside the 9 AM to 5 PM window. The upper bound is exclusive, which matches the business-hours convention of "9 to 5" meaning the workday ends as the clock strikes 5 PM.
6. Group counts by hour of day
count(h = hour(ts)) produces a tidy hour-by-count table. Feed it straight into ggplot2::geom_col() for a 24-bar diurnal-pattern chart. Real datasets quickly reveal lunch-hour dips, end-of-day spikes, and overnight quiet periods.
hour() returns 0 for midnight, but day() returns 1 for the first of the month and month() returns 1 for January. When you build numeric features for a model, a 0 in the hour column is real data, not a missing value. Treat zero hour with the same care as any other valid level.hour() vs minute() vs second() vs format()
Three lubridate accessors split a timestamp into hour, minute, and second integers; format() returns the same parts as a string.
| Function | Returns | Range | Typical use |
|---|---|---|---|
hour() |
Hour of day | 0 to 23 | Diurnal grouping, business-hours filters |
minute() |
Minute of hour | 0 to 59 | Sub-hour bucketing, latency analysis |
second() |
Second of minute | 0 to 59.99 | Precision timing, log replay |
format(x, "%H:%M") |
"HH:MM" string | character | Display only; not numeric |
The lubridate accessors return integers you can compare and aggregate. format() returns characters that look right in a report but force a as.numeric() conversion if you want to compute on them. Reach for hour(), minute(), second() for analysis; reach for format() for printed output.
July 15, 2024 at 14:30:45 returns 14 for the hour, 30 for the minute, and 45 for the second. The three integers together reconstruct the time-of-day; format() produces the same information as a single character string.
Common pitfalls
Pitfall 1: passing a Date instead of a POSIXct. hour(as.Date("2024-07-15")) returns 0 because Date has no time component and lubridate silently coerces to midnight UTC. The result is technically correct but rarely useful. If you need hour-of-day, parse with ymd_hms() or as.POSIXct() so the time is preserved.
Pitfall 2: forgetting timezone shifts. hour(ymd_hms("2024-07-15 14:00:00", tz = "America/New_York")) returns 14, but hour(with_tz(x, "UTC")) returns 18 for the same instant. Always pin the timezone explicitly when extracting the hour from data that crosses zones; otherwise Sys.timezone() decides for you.
2024-07-15 14:00:00 in America/New_York is the same instant as 2024-07-15 18:00:00 in UTC. hour() returns whatever the underlying tzone of the vector says. Convert with with_tz() BEFORE calling hour() if you want hour values in a specific zone.hour(x) is s.dt.hour on a datetime Series. The dplyr pipeline mutate(h = hour(ts)) mirrors df.assign(h=df.ts.dt.hour). Pandas also returns a 0-to-23 integer, so the convention matches lubridate exactly.A practical workflow with hour()
Hour of day shows up in three places: filtering to a window, building a diurnal feature for a model, and bucketing timestamps for hourly aggregation.
The patterns:
- Filter to a within-day window:
filter(between(hour(ts), 9, 16))keeps business hours. - Build features for a model: extract
hour()alongsidewday(),month(), andis_weekendwhen daily and weekly seasonality matter. - Hourly aggregation: prefer
floor_date(ts, "hour")for a datetime axis that spans multiple days; usehour(ts)to collapse across days into a 0-to-23 distribution.
Try it yourself
Try it: Use the logs tibble above and filter to rows where the hour is between 10 and 18 inclusive. Save the result to ex_daytime.
Click to reveal solution
Explanation: between(hour(ts), 10, 18) is shorthand for hour(ts) >= 10 & hour(ts) <= 18. The result keeps the 11, 14, and 18 hour rows, which fall inside the inclusive 10-to-18 window.
Related lubridate functions
After mastering hour(), look at:
minute(),second(): extract the other time-of-day componentsday(),month(),year(): extract the date componentswday(),yday(),qday(): extract day-position numbersfloor_date(),ceiling_date(): round a datetime to hour, day, week, or monthmake_datetime(): build a datetime from year, month, day, hour, minute, second integerswith_tz(),force_tz(): shift or set the timezone before extracting partsymd_hms(),mdy_hms(),dmy_hms(): parse strings into datetimes first
For the official reference, see the lubridate hour() documentation.
FAQ
How do I extract the hour from a datetime in R?
Use lubridate::hour(x) where x is a POSIXct or POSIXlt value. The result is an integer 0 to 23. Base R alternatives are format(x, "%H") (returns a string) or as.POSIXlt(x)$hour (returns an integer). The lubridate version is shortest, fully vectorised, and slots straight into dplyr pipelines like mutate(h = hour(ts)).
What is the difference between hour() and format(x, "%H") in R?
hour() returns an integer 0 to 23 ready for arithmetic and comparison. format(x, "%H") returns a two-character string like "09" or "23" suited for display. Use hour() when you plan to filter, group, or feed the value into a model. Use format() when you want zero-padded text for a report or label. Converting between them costs an as.integer() or sprintf("%02d", h) call.
Why does hour() return 0 for a Date object?
A Date has no time component, so lubridate silently coerces it to midnight UTC and returns 0. The zero is a coercion artifact, not real data. To get an actual hour, parse the source with ymd_hms() or as.POSIXct() first so the time survives.
How do I change the hour of a datetime in R?
Use the replacement form: hour(x) <- 9. This sets the hour while keeping the date, minute, and second untouched. It is vectorised, so hour(timestamp_vector) <- 9 snaps an entire column to 9 AM. Pair it with minute(x) <- 0 and second(x) <- 0 to align timestamps to a clean hour boundary.
How do I extract the hour in a specific timezone?
Convert the datetime first with with_tz(), then call hour(): hour(with_tz(x, "America/New_York")). hour() returns the hour as the underlying tzone reads it, so a UTC-stored timestamp shows UTC hours unless you shift first. Pin the timezone whenever your data crosses zones; otherwise Sys.timezone() decides and the answer becomes machine-dependent.