floor_date <- function(x, unit = "seconds") { if(!length(x)) return(x) parsed_unit <- parse_period_unit(unit) n <- parsed_unit$n unit <- standardise_period_names(parsed_unit$unit) if(unit %in% c("second", "minute", "hour", "day")){ out <- trunc_multi_unit(x, unit, n) reclass_date_maybe(out, x, unit) } else { if(n > 1 && unit == "week"){ ## fixme: warning("Multi-unit not supported for weeks. Ignoring.") } if(unit %in% c("bimonth", "quarter", "halfyear")){ switch(unit, bimonth = n <- 2 * n, quarter = n <- 3 * n, halfyear = n <- 6 * n) unit <- "month" } switch(unit, week = update(x, wdays = 1, hours = 0, minutes = 0, seconds = 0), month = { if(n > 1) update(x, months = floor_multi_unit1(month(x), n), mdays = 1, hours = 0, minutes = 0, seconds = 0) else update(x, mdays = 1, hours = 0, minutes = 0, seconds = 0) }, year = { ## due to bug https://github.com/hadley/lubridate/issues/319 we ## need to do it in two steps if(n > 1){ y <- update(x, ydays = 1, hours = 0, minutes = 0, seconds = 0) update(y, years = floor_multi_unit(year(y), n)) } else { update(x, ydays = 1, hours = 0, minutes = 0, seconds = 0) } }) } } parse_period_unit <- function(unit) { if (length(unit) > 1) { warning("Unit argument longer than 1. Taking first element.") unit <- unit[[1]] } p <- .Call("c_parse_period", as.character(unit)) if (!is.na(p[[1]])) { period_units <- c("second", "minute", "hour", "day", "week", "month", "year") wp <- which(p > 0) if(length(wp) > 1){ stop("Multi unit periods are not yet supported") } list(n = p[wp], unit = period_units[wp]) } else { ## this is for backward compatibility and allows for bimonth, halfyear and quarter m <- regexpr(" *(?[0-9.,]+)? *(?[^ \t\n]+)", unit[[1]], perl = T) if(m > 0){ ## should always match nms <- attr(m, "capture.names") nms <- nms[nzchar(nms)] start <- attr(m, "capture.start") end <- start + attr(m, "capture.length") - 1L n <- if(end[[1]] >= start[[1]]){ as.integer(str_sub(unit, start[[1]], end[[1]])) } else { 1 } unit <- str_sub(unit, start[[2]], end[[2]]) list(n = n, unit = unit) } else { stop(sprintf("Invalid unit specification '%s'", unit)) } } } trunc_multi_unit <- function(x, unit, n){ y <- as.POSIXlt(x) switch(unit, second = { y$sec <- if(n == 1) trunc(y$sec) else floor_multi_unit(y$sec, n) }, minute = { y$sec[] <- 0 y$min <- floor_multi_unit(y$min, n) }, hour = { y$sec[] <- 0 y$min[] <- 0L y$hour <- floor_multi_unit(y$hour, n) }, day = { y$sec[] <- 0 y$min[] <- 0L y$hour[] <- 0L y$isdst[] <- -1L y$mday <- floor_multi_unit1(y$mday, n) }, stop("Invalid unit ", unit)) y } floor_multi_unit <- function(n, len) { (n %/% len) * len } floor_multi_unit1 <- function(n, len) { (((n - 1) %/% len) * len) + 1L } standardise_period_names <- function(x) { dates <- c("second", "minute", "hour", "day", "week", "month", "year", ## these ones are used for rounding only "bimonth", "quarter", "halfyear") y <- gsub("(.)s$", "\\1", x) y <- substr(y, 1, 3) res <- dates[pmatch(y, dates)] if (any(is.na(res))) { stop("Invalid period name: ", paste(x[is.na(res)], collapse = ", "), call. = FALSE) } res } reclass_date_maybe <- function(new, orig, unit){ if(is.Date(orig) && !unit %in% c("day", "week", "month", "year")) as.POSIXct(new) else reclass_date(new, orig) }