Omar Gonzales
Omar Gonzales

Reputation: 4008

make monthly ranges in R

I've this function to generate monthly ranges, it should consider years where february has 28 or 29 days:

   starts     ends
1  2017-01-01 2017-01-31
2  2017-02-01 2017-02-28
3  2017-03-01 2017-03-31

It works with:

make_date_ranges(as.Date("2017-01-01"), Sys.Date())

But gives error with:

make_date_ranges(as.Date("2017-01-01"), as.Date("2019-12-31"))

Why?

make_date_ranges(as.Date("2017-01-01"), as.Date("2019-12-31"))
Error in data.frame(starts, ends) : 
  arguments imply differing number of rows: 38, 36

add_months <-  function(date, n){
  seq(date, by = paste (n, "months"), length = 2)[2]
}

make_date_ranges <- function(start, end){

  starts <- seq(from = start,
                to =  Sys.Date()-1 ,
                by = "1 month")

  ends <- c((seq(from = add_months(start, 1),
                 to = end,
                 by = "1 month" ))-1,
            (Sys.Date()-1))

  data.frame(starts,ends)

}

## useage
make_date_ranges(as.Date("2017-01-01"), as.Date("2019-12-31"))

Upvotes: 1

Views: 299

Answers (2)

G. Grothendieck
G. Grothendieck

Reputation: 269596

1) First, define start of month, som, and end of month, eom functions which take a Date class object, date string in standard Date format or yearmon object and produce a Date class object giving the start or end of its year/months.

Using those, create a monthly Date series s using the start of each month from the month/year of from to that of to. Use pmax to ensure that the series does not extend before from and pmin so that it does not extend past to.

The input arguments can be strings in standard Date format, Date class objects or yearmon class objects. In the yearmon case it assumes the user wanted the full month for every month. (The if statement can be omitted if you don't need to support yearmon inputs.)

library(zoo)

som <- function(x) as.Date(as.yearmon(x))
eom <- function(x) as.Date(as.yearmon(x), frac = 1)

date_ranges2 <- function(from, to) {
  if (inherits(to, "yearmon")) to <- eom(to)
  s <- seq(som(from), eom(to), "month")
  data.frame(from = pmax(as.Date(from), s), to = pmin(as.Date(to), eom(s)))
}

date_ranges2("2000-01-10", "2000-06-20")
##         from         to
## 1 2000-01-10 2000-01-31
## 2 2000-02-01 2000-02-29
## 3 2000-03-01 2000-03-31
## 4 2000-04-01 2000-04-30
## 5 2000-05-01 2000-05-31
## 6 2000-06-01 2000-06-20

date_ranges2(as.yearmon("2000-01"), as.yearmon("2000-06"))
##         from         to
## 1 2000-01-01 2000-01-31
## 2 2000-02-01 2000-02-29
## 3 2000-03-01 2000-03-31
## 4 2000-04-01 2000-04-30
## 5 2000-05-01 2000-05-31
## 6 2000-06-01 2000-06-30

2) This alternative takes the same approach but defines start of month (som) and end of month (eom) functions without using yearmon so that only base R is needed. It takes character strings in standard Date format or Date class inputs and gives the same output as (1).

som <- function(x) as.Date(cut(as.Date(x), "month")) # start of month
eom <- function(x) som(som(x) + 32) - 1 # end of month

date_ranges3 <- function(from, to) {
  s <- seq(som(from), as.Date(to), "month")
  data.frame(from = pmax(as.Date(from), s), to = pmin(as.Date(to), eom(s)))
}

date_ranges3("2000-01-10", "2000-06-20")
##         from         to
## 1 2000-01-10 2000-01-31
## 2 2000-02-01 2000-02-29
## 3 2000-03-01 2000-03-31
## 4 2000-04-01 2000-04-30
## 5 2000-05-01 2000-05-31
## 6 2000-06-01 2000-06-20

date_ranges3(som("2000-01-10"), eom("2000-06-20"))
##         from         to
## 1 2000-01-01 2000-01-31
## 2 2000-02-01 2000-02-29
## 3 2000-03-01 2000-03-31
## 4 2000-04-01 2000-04-30
## 5 2000-05-01 2000-05-31
## 6 2000-06-01 2000-06-30

Upvotes: 1

MichaelChirico
MichaelChirico

Reputation: 34703

You don't need to use seq twice -- you can subtract 1 day from the firsts of each month to get the ends, and generate one too many starts, then shift & subset:

make_date_ranges = function(start, end) {
  # format(end, "%Y-%m-01") essentially truncates end to 
  #   the first day of end's month; 32 days later is guaranteed to be
  #   in the subsequent month
  starts = seq(from = start, to = as.Date(format(end, '%Y-%m-01')) + 32, by = 'month')
  data.frame(starts = head(starts, -1L), ends = tail(starts - 1, -1L))
}
x = make_date_ranges(as.Date("2017-01-01"), as.Date("2019-12-31"))
rbind(head(x), tail(x))
#        starts       ends
# 1  2017-01-01 2017-01-31
# 2  2017-02-01 2017-02-28
# 3  2017-03-01 2017-03-31
# 4  2017-04-01 2017-04-30
# 5  2017-05-01 2017-05-31
# 6  2017-06-01 2017-06-30
# 31 2019-07-01 2019-07-31
# 32 2019-08-01 2019-08-31
# 33 2019-09-01 2019-09-30
# 34 2019-10-01 2019-10-31
# 35 2019-11-01 2019-11-30
# 36 2019-12-01 2019-12-31

Upvotes: 1

Related Questions