36

I have a vector of POSIXct values and I would like to round them to the nearest quarter hour. I don't care about the day. How do I convert the values to hours and minutes?

For example, I would like the value

"2012-05-30 20:41:21 UTC"

to be

"20:45"
Rich Scriven
  • 97,041
  • 11
  • 181
  • 245
Dominic
  • 465
  • 1
  • 4
  • 9

7 Answers7

36

Indeed, an old question with some helpful answers so far. The last one by giraffhere seems to be the most elegant. however, ist not floor_date but round_date which will do the trick:

lubridate::round_date(x, "15 minutes") 
user3297928
  • 476
  • 4
  • 5
28

You can use round. The trick is to divide by 900 seconds (15 minutes * 60 seconds) before rounding and multiply by 900 afterwards:

a <-as.POSIXlt("2012-05-30 20:41:21 UTC")
b <-as.POSIXlt(round(as.double(a)/(15*60))*(15*60),origin=(as.POSIXlt('1970-01-01')))
b
[1] "2012-05-30 20:45:00 EDT"

To get only hour and minute, just use format

format(b,"%H:%M")
[1] "20:45"

as.character(format(b,"%H:%M"))
[1] "20:45"
Pierre Lapointe
  • 16,017
  • 2
  • 43
  • 56
  • 5
    I don't think we need a double, and origin accepts a string, so slightly simplified: `b <-as.POSIXlt(round(as.numeric(a)/(15*60))*(15*60),origin='1970-01-01')` – Mark Rajcok Aug 08 '15 at 18:47
  • Since `R4.3.*` we no longer need to explicitly specify the origin if it's the unix epoch as in this case. – jay.sf Jul 26 '23 at 07:04
15

something like

format(strptime("1970-01-01", "%Y-%m-%d", tz="UTC") + round(as.numeric(your.time)/900)*900,"%H:%M")

would work

shhhhimhuntingrabbits
  • 7,397
  • 2
  • 23
  • 23
14

Old question, but would like to note that the lubridate package handles this easily now with floor_date. To cut a vector of POSIXct objects to 15 minute intervals, use like this.

x <- lubridate::floor_date(x, "15 minutes")

EDIT: Noted by user @user3297928, use lubridate::round_date(x, "15 minutes") for rounding to the nearest 15 minutes. The above floors it.

giraffehere
  • 1,118
  • 7
  • 18
5

You can use the align.time function in the xts package to handle the rounding, then format to return a string of "HH:MM":

R> library(xts)
R> p <- as.POSIXct("2012-05-30 20:41:21", tz="UTC")
R> a <- align.time(p, n=60*15)  # n is in seconds
R> format(a, "%H:%M")
[1] "20:45"
Joshua Ulrich
  • 173,410
  • 32
  • 338
  • 418
3

Try this, which combines both requests and is based on looking at what round.POSIXt() and trunc.POSIXt() do.

myRound <- function (x, convert = TRUE)  {
    x <- as.POSIXlt(x)
    mins <- x$min
    mult <- mins %/% 15
    remain <- mins %% 15
    if(remain > 7L || (remain == 7L && x$sec > 29))
        mult <- mult + 1
    if(mult > 3) {
        x$min <- 0
        x <- x + 3600
    } else {
        x$min <- 15 * mult
    }
    x <- trunc.POSIXt(x, units = "mins")
    if(convert) {
        x <- format(x, format = "%H:%M")
    }
    x
}

This gives:

> tmp <- as.POSIXct("2012-05-30 20:41:21 UTC")
> myRound(tmp)
[1] "20:45"
> myRound(tmp, convert = FALSE)
[1] "2012-05-30 20:45:00 BST"
> tmp2 <- as.POSIXct("2012-05-30 20:55:21 UTC")
> myRound(tmp2)
[1] "21:00"
> myRound(tmp2, convert = FALSE)
[1] "2012-05-30 21:00:00 BST"
Gavin Simpson
  • 170,508
  • 25
  • 396
  • 453
  • this seems to be not vectorized well, try `structure(c(1313331280, 1313334917, 1313334917, 1313340309, 1313340309, 1313340895, 1313340895, 1313341133, 1313341218, 1313341475), class = c("POSIXct", "POSIXt"), tzone = "UTC")` – jangorecki Oct 14 '15 at 23:41
3

Using IDate and ITime classes from data.table and a IPeriod class (just developed) I was able to get more scalable solution.
Only shhhhimhuntingrabbits and PLapointe answer the question in terms of nearest. xts solution only rounds using ceiling, my IPeriod solution allows to specify ceiling or floor.
To get top performance you would need to keep your data in IDate and ITime classes. As seen on benchmark it is cheap to produce POSIXct from IDate/ITime/IPeriod. Below benchmark of some 22M timestamp:

# install only if you don't have
install.packages(c("microbenchmarkCore","data.table"),
                 repos = c("https://olafmersmann.github.io/drat",
                           "https://jangorecki.github.io/drat/iperiod"))
library(microbenchmarkCore)
library(data.table) # iunit branch
library(xts)
Sys.setenv(TZ="UTC")

## some source data: download and unzip csv
# "http://api.bitcoincharts.com/v1/csv/btceUSD.csv.gz"
# below benchmark on btceUSD.csv.gz 11-Oct-2015 11:35 133664801

system.nanotime(dt <- fread(".btceUSD.csv"))
# Read 21931266 rows and 3 (of 3) columns from 0.878 GB file in 00:00:10
#     user   system  elapsed 
#       NA       NA 9.048991

# take the timestamp only
x = as.POSIXct(dt[[1L]], tz="UTC", origin="1970-01-01")

# functions
shhhhi <- function(your.time){
    strptime("1970-01-01", "%Y-%m-%d", tz="UTC") + round(as.numeric(your.time)/900)*900
}

PLapointe <- function(a){
    as.POSIXlt(round(as.double(a)/(15*60))*(15*60),origin=(as.POSIXlt('1970-01-01')))
}

# myRound - not vectorized

# compare results
all.equal(
    format(shhhhi(x),"%H:%M"),
    format(PLapointe(x),"%H:%M")
)
# [1] TRUE
all.equal(
    format(align.time(x, n = 60*15),"%H:%M"),
    format(periodize(x, "mins", 15),"%H:%M")
)
# [1] TRUE

# IPeriod native input are IDate and ITime - will be tested too
idt <- IDateTime(x)
idate <- idt$idate
itime <- idt$itime
microbenchmark(times = 10L,
               shhhhi(x),
               PLapointe(x),
               xts = align.time(x, 15*60),
               posix_ip_posix = as.POSIXct(periodize(x, "mins", 15), tz="UTC"),
               posix_ip = periodize(x, "mins", 15),
               ip_posix = as.POSIXct(periodize(idate, itime, "mins", 15), tz="UTC"),
               ip = periodize(idate, itime, "mins", 15))
# Unit: microseconds
#            expr         min          lq         mean       median          uq         max neval
#       shhhhi(x)  960819.810  984970.363 1127272.6812 1167512.2765 1201770.895 1243706.235    10
#    PLapointe(x) 2322929.313 2440263.122 2617210.4264 2597772.9825 2792936.774 2981499.356    10
#             xts  453409.222  525738.163  581139.6768  546300.9395  677077.650  767609.155    10
#  posix_ip_posix 3314609.993 3499220.920 3641219.0876 3586822.9150 3654548.885 4457614.174    10
#        posix_ip 3010316.462 3066736.299 3157777.2361 3133693.0655 3234307.549 3401388.800    10
#        ip_posix     335.741     380.696     513.7420     543.3425     630.020     663.385    10
#              ip      98.031     151.471     207.7404     231.8200     262.037     278.789    10

IDate and ITime successfully scales not only in this particular task. Both types, same as IPeriod, are integer based. I would assume they will also scale nice on join or grouping by datetime fields.
Online manual: https://jangorecki.github.io/drat/iperiod/

jangorecki
  • 16,384
  • 4
  • 79
  • 160
  • Thanks for the post, but can you explain how to actually _install_ this package? It's not clear anywhere from the documentation. – shadowtalker May 20 '16 at 19:00
  • @ssdecontrol See first command in the code block to install from published repo. Otherwise the most reliable way is to get the [iunit](https://github.com/jangorecki/data.table/tree/iunit) branch, you can add it to remote and checkout to branch. It is based on Oct 2015 data.table. – jangorecki May 20 '16 at 21:43
  • Ah, I missed that line. I had assumed you would have a separate package called "periodize" or "IPeriod" or something, as opposed to a fork of data.table. I think it's somewhat unfortunate that the `IDateTime` stuff is bundled into data.table and not a separate package – shadowtalker May 20 '16 at 22:17
  • @ssdecontrol but IPeriod class is just a numeric, any package can handle it just with `%/%` operator, having periods hardcoded there is even no need to store any attributes, just a number, no black magic. – jangorecki May 21 '16 at 01:11
  • Abstraction is valuable imo – shadowtalker May 21 '16 at 01:17
  • @ssdecontrol agree, but extracting just that to a separate package isn't so necessary as data.table is a lightweight dependency already. Can be imported just for IDateTime without hassle. Be aware there is planned an extensions to IDateTime classes data.table [data.table#1451](https://github.com/Rdatatable/data.table/issues/1451), would be nice to IPeriod handle nanoseconds too. – jangorecki May 21 '16 at 11:13