Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "quarter" option to floor_date and ceiling_date #239

Closed
mmeierer opened this issue May 11, 2014 · 8 comments
Closed

Add "quarter" option to floor_date and ceiling_date #239

mmeierer opened this issue May 11, 2014 · 8 comments

Comments

@mmeierer
Copy link

Add "quarter" option to floor_date and ceiling_date functions, thus any date is transformed to be the first or last day of the quarter.

This is particularly useful if on wants to work with quarterly aggregated data (the quarter function does not output a date object and thus, is limited in its usefulness if one wants to calculate sequences etc. based on the quarter information).

@jonboiser
Copy link
Contributor

This is my implementation of adding "quarter" to floor_date:

floor_date <- function(x, unit = c("second","minute","hour","day", "week", "month", "year", "quarter")) {
  unit <- match.arg(unit)

  new <- switch(unit,
    second = update(x, seconds = floor(second(x))),
    minute = update(x, seconds = 0),
    hour =   update(x, minutes = 0, seconds = 0),
    day =    update(x, hours = 0, minutes = 0, seconds = 0),
    week =   update(x, wdays = 1, hours = 0, minutes = 0, seconds = 0),
    month =  update(x, mdays = 1, hours = 0, minutes = 0, seconds = 0),
    year =   update(x, ydays = 1, hours = 0, minutes = 0, seconds = 0),
    quarter = update(x, months = ceiling(month(x)/3) * 3 - 2, mdays = 1)
  )
  new
}

The formula for month is kind of confusing, but is based on the map from month to quarter number (e.g. February is month 2, so ceiling(2/3) = 1, Quarter 1).

This the implementation for ceiling_date, which has to bypass that step where a second is subtracted from the date.

ceiling_date <- function(x, unit = c("second","minute","hour","day", "week", "month", "year", "quarter")) {
    unit <- match.arg(unit)

    if(!length(x)) return(x)

  if (unit == "second") {
    second(x) <- ceiling(second(x))
    return(x)
  }

  if(unit != "quarter") {
    y <- floor_date(x - dseconds(1), unit)
  } else {
      y <- floor_date(x, unit)
  }

    switch(unit,
        minute = minute(y) <- minute(y) + 1,
        hour =   hour(y) <- hour(y) + 1,
        day =    yday(y) <- yday(y) + 1,
        week =   week(y) <- week(y) + 1,
        month =  month(y) <- month(y) + 1,
        year =   year(y) <- year(y) + 1,
        quarter = { month(y) <- month(y) + 3; y <- y - ddays(1) }
    )
    reclass_date(y, x)
}

Here are some example outputs:

days <- sample(ymd("2014-01-01") + ddays(1:365), 10) %>% sort
ceiling_date(days, "quarter")

data.frame(date = days, 
           floor_date = floor_date(days, "quarter"), 
           ceiling_date = ceiling_date(days, "quarter"))

         date floor_date ceiling_date
1  2014-01-09 2014-01-01   2014-03-31
2  2014-04-23 2014-04-01   2014-06-30
3  2014-06-09 2014-04-01   2014-06-30
4  2014-07-05 2014-07-01   2014-09-30
5  2014-07-28 2014-07-01   2014-09-30
6  2014-08-15 2014-07-01   2014-09-30
7  2014-10-04 2014-10-01   2014-12-31
8  2014-10-11 2014-10-01   2014-12-31
9  2014-10-29 2014-10-01   2014-12-31
10 2014-12-18 2014-10-01   2014-12-31

@jonboiser
Copy link
Contributor

I plugged in the "quarter" option into round_date as well, IDK if this is the desired behavior, which is just straightforward application of the mathematical definition of round in terms of ceiling and floor.

> data.frame(date = days, 
+            floor_date = floor_date(days, "quarter"), 
+            ceiling_date = ceiling_date(days, "quarter"),
+            round_date = round_date(days, "quarter"))
         date floor_date ceiling_date round_date
1  2014-01-09 2014-01-01   2014-03-31 2014-01-01
2  2014-04-23 2014-04-01   2014-06-30 2014-04-01
3  2014-06-09 2014-04-01   2014-06-30 2014-06-30
4  2014-07-05 2014-07-01   2014-09-30 2014-07-01
5  2014-07-28 2014-07-01   2014-09-30 2014-07-01
6  2014-08-15 2014-07-01   2014-09-30 2014-07-01
7  2014-10-04 2014-10-01   2014-12-31 2014-10-01
8  2014-10-11 2014-10-01   2014-12-31 2014-10-01
9  2014-10-29 2014-10-01   2014-12-31 2014-10-01
10 2014-12-18 2014-10-01   2014-12-31 2014-12-31

@danielkrizian
Copy link

Even more generally, why not wrap floor.Date <- floor_date; ceiling.Date <- ceiling_date; round.Date <- round_date, so that the base versions of the triad dispatch when class(x) %in% "Date"?
The benefit is short & intuitive syntax floor(dates) equivalent to longer floor_date(dates)

@vspinu
Copy link
Member

vspinu commented Feb 5, 2015

Even more generally, why not wrap floor.Date <- floor_date; ceiling.Date <- ceiling_date; round.Date <- round_date,

Because these generics accept only one argument and operations on dates are quite meaningless without the unit argument. floor_date operates on second by default, is this what you want?

@jonboiser
Copy link
Contributor

There are existing methods round.Date and trunc.Date in base R, but the units are limited to secs, mins, hours, and days. I think writing an S3 method that mimics floor_date's but using the names of stuff in base R might get confusing.

@vspinu
Copy link
Member

vspinu commented Feb 5, 2015

here are existing methods round.Date and trunc.Date in base R,

Ahh ... you are right. I looked at floor only. If round_date is a super set of round.Date and round.POSIXct then this re-definition is meaningful, but might have undesirable consequences.

To be honest I don't like xxx_date names also for the fact that they sound as operating only on Date class. xxx_time would be a better name IMO.

@vspinu
Copy link
Member

vspinu commented Feb 5, 2015

@jonboiser

quarter = update(x, months = ceiling(month(x)/3) * 3 - 2, mdays = 1)

This is incomplete. You need to set smaller units to 0 as well.

The formula for month is kind of confusing, but is based on the map from month to quarter number (e.g. February is month 2, so ceiling(2/3) = 1, Quarter 1).

Maybe a bit simpler would be ((month(x) - 1) %/% 3)*3 + 1?

This the implementation for ceiling_date, which has to bypass that step where a second is subtracted from the date.

I don't think so. Implementation for quarter must be very similar to that of the month. I think the rationale of that code is that If the date is exactly at the beginning of the month or quarter you want to keep it as it is.

BTW. Could you please propose a PR next time? It would be easier to comment on the code. Thanks.

@vspinu
Copy link
Member

vspinu commented May 1, 2015

@jonboiser fixed this in #303. I forgot to close it back then.

@vspinu vspinu closed this as completed May 1, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants