将一个日期加一个月

73

我正在尝试给一个已有的日期加一个月。但目前为止,直接加并不可能。以下是我尝试过的方法。

d <- as.Date("2004-01-31")
d + 60
# [1] "2004-03-31"

由于这个月份不会重叠,所以添加是没有帮助的。

seq(as.Date("2004-01-31"), by = "month", length = 2) 
# [1] "2004-01-31" "2004-03-02"

上述方法可能有效,但并不是直接明了的。此外,它还会给日期添加30天或其他一些问题,如下所示。

seq(as.Date("2004-01-31"), by = "month", length = 10) 
#  [1] "2004-01-31" "2004-03-02" "2004-03-31" "2004-05-01" "2004-05-31" "2004-07-01" "2004-07-31" "2004-08-31" "2004-10-01" "2004-10-31"
在上述示例中,前两个日期的月份未改变。 此外,以下方法对于月份也失败了,但对年份却成功了。
d <- as.POSIXlt(as.Date("2010-01-01"))
d$year <- d$year +1
d
# [1] "2011-01-01 UTC"
d <- as.POSIXlt(as.Date("2010-01-01"))
d$month <- d$month +1
d

Error in format.POSIXlt(x, usetz = TRUE) : invalid 'x' argument

怎样才是正确的方法?

8个回答

160

lubridate中的函数%m+%会添加一个月,而不会超过新月份的最后一天。

library(lubridate)
(d <- ymd("2012-01-31"))
 1 parsed with %Y-%m-%d
[1] "2012-01-31 UTC"
d %m+% months(1)
[1] "2012-02-29 UTC"

4
这应该被接受为正确答案,因为它正确避免超过新月的最后一天。 +1 - NickBraunagel
1
当您将二月作为输入时,此代码无法正常工作,因为它会将接下来的几个月更改为最后一个日期,就像二月一样。 - Bruce Wayne
1
@BruceWayne 你有一个reprex吗?对我来说,ymd("2011-02-20") %m+% months(1)似乎可以正常工作。 - lost
1
@BruceWayne 只有在选择最后一个月不存在的日期时才会发生这种情况。例如,ymd("2019-01-31") %m+% months(1) 将得到 2019-02-28。如果该年是闰年,则该日将为29日。这是正确的行为。如果您想要 +30 天,只需使用正常的 + 运算符添加 30 天即可。 - Felipe Gerard

64

当你说“给日期加一个月”时并不明确。

你的意思是:

  1. 增加30天吗?
  2. 将日期中的月份部分增加1个月吗?

在这两种情况下,为简单的加法使用整个包装似乎有点夸张。

对于第一点,当然,简单的+运算符就可以了:

d=as.Date('2010-01-01') 
d + 30 
#[1] "2010-01-31"

至于第二件事,我会创建一个只有一行代码的函数,就这么简单(而且具有更广泛的范围):

对于第二个问题,我将创建一个仅有一行代码的函数来解决,如此简单(并且具有更普遍的适用性):

add.months= function(date,n) seq(date, by = paste (n, "months"), length = 2)[2]
您可以使用任意月份,包括负数:
add.months(d, 3)
#[1] "2010-04-01"
add.months(d, -3)
#[1] "2009-10-01"

当然,如果你只想添加单个月并且经常这样做:

add.month=function(date) add.months(date,1)
add.month(d)
#[1] "2010-02-01"
如果你想在1月31日加上一个月,因为2月31日没有意义,最好的方法是将缺失的3天加到下一个月——也就是3月。所以正确的方式是:

如果你在1月31日加一个月,则应该加上以下月份中缺失的3天,即3月。

add.month(as.Date("2010-01-31"))
#[1] "2010-03-03"

如果由于某些非常特殊的原因,你需要对月份的最后一天设置一个上限,那么它会略微延长:

add.months.ceil=function (date, n){

  #no ceiling
  nC=add.months(date, n)

  #ceiling
  day(date)=01
  C=add.months(date, n+1)-1

  #use ceiling in case of overlapping
  if(nC>C) return(C)
  return(nC)
}

像往常一样,您可以添加单月版本:

add.month.ceil=function(date) add.months.ceil(date,1)    
所以:
  d=as.Date('2010-01-31')
  add.month.ceil(d)
  #[1] "2010-02-28"
  d=as.Date('2010-01-21')
  add.month.ceil(d)
  #[1] "2010-02-21"

并使用递减:

  d=as.Date('2010-03-31')
  add.months.ceil(d, -1)
  #[1] "2010-02-28"
  d=as.Date('2010-03-21')
  add.months.ceil(d, -1)
  #[1] "2010-02-21"

除此之外,您没有说明您是否对标量或向量解决方案感兴趣。至于后者:

add.months.v= function(date,n) as.Date(sapply(date, add.months, n), origin="1970-01-01")

注意:*apply函数族会破坏类的数据,因此必须重新构建。向量版本带来:

d=c(as.Date('2010/01/01'), as.Date('2010/01/31'))
add.months.v(d,1)
[1] "2010-02-01" "2010-03-03"

希望你喜欢它))


2
如果你必须经过许多思考才能添加一个月,并且想要放心,专家已经仔细考虑过了,那么使用这个包是值得的。 - ok1more
2
使用lubridate将3个月添加到日期“d”:ymd(d) %m+% months(3);使用上面的一行代码:add.months(d, 3)。因此,在第一个示例中,您必须学习三个函数ymd%m+%months(),以及如何粘合它们的语法,后者早于标准R months()。当然,对于复杂的日期操作,lubridate的复杂性是有意义的。 - antonio

45

Vanilla R有一个天真的difftime类,但LubridateCRAN软件包可以让您实现您要求的功能:

require(lubridate)
d <- ymd(as.Date('2004-01-01')) %m+% months(1)
d
[1] "2004-02-01"
希望能帮到你。

4
但是这并不总是奏效:d <- as.Date("2004-01-31") 返回 NA下面的答案 给出了该情况的预期答案。 - Matt Parker
d <- as.Date("2004-01-31") d # 嗯? [1] "2004-01-31" - hd1
3
抱歉,我表达不够清晰。如果您将第二行中的日期替换为“2004-01-31”,然后运行代码的其余部分,您将得到“NA”。在这种情况下,当您增加月份时,它会尝试将它设置为“2004-02-31”,但此日期无效,因此返回“NA”。在下一行代码中设置“day(d)”时,“d”已经是“NA”。 - Matt Parker
5
正如 @MattParker 所指出的那样,这种解决方案不能解决像 2004-01-31 这样的情况,而 Wojciech Sobala 提供的方法可以正确工作,并且在 lubridate 中是推荐使用的方法。 - Tim

11

最简单的方法是将日期转换为POSIXlt格式,然后按照以下方式执行算术操作:

date_1m_fwd     <- as.POSIXlt("2010-01-01")
date_1m_fwd$mon <- date_1m_fwd$mon +1

另外,如果你想在data.table中处理日期列,遗憾的是,不支持POSIXlt格式。

但是,你仍然可以使用基本R代码执行添加月份操作,如下所示:

library(data.table)  
dt <- as.data.table(seq(as.Date("2010-01-01"), length.out=5, by="month"))  
dt[,shifted_month:=tail(seq(V1[1], length.out=length(V1)+3, by="month"),length(V1))]

希望能对你有所帮助。


谢谢!这也适用于2月29日的日期。但lubridate不支持这种情况。 - giordano
是的,它默认基于格里高利历,闰年也已经为您计算好了...(但与中国相比,并不是一个复杂的算法) - Steven Chau

8
"mondate"与"Date"有些相似,只是添加n会增加n个月,而不是n天。(可以使用as.Date.mondate将mondate类对象转换为Date对象。)
library(mondate)

d <- as.Date("2004-01-31")
as.mondate(d) + 1
## mondate: timeunits="months"
## [1] 2004-02-29

as.Date(as.mondate(d) + 1)
## [1] "2004-02-29"

5
addedMonth <- seq(as.Date('2004-01-01'), length=2, by='1 month')[2]
addedQuarter <- seq(as.Date('2004-01-01'), length=2, by='1 quarter')[2]

1
在另一个答案中已经提到了使用seq。请考虑添加上下文以突出您的答案与其他答案的区别。 - BenBarnes

5
以下是一个不需要安装任何包的函数,它接受一个 Date 对象(或者可以转换为 Datecharacter),并将 n 个月添加到该日期中而不改变该月的日期除非你所到达的那个月份没有足够的天数,在这种情况下,它会默认返回该月的最后一天)。为了更好地理解,下面有一些示例。

函数定义

addMonth <- function(date, n = 1){
  if (n == 0){return(date)}
  if (n %% 1 != 0){stop("Input Error: argument 'n' must be an integer.")}

  # Check to make sure we have a standard Date format
  if (class(date) == "character"){date = as.Date(date)}

  # Turn the year, month, and day into numbers so we can play with them
  y = as.numeric(substr(as.character(date),1,4))
  m = as.numeric(substr(as.character(date),6,7))
  d = as.numeric(substr(as.character(date),9,10))

  # Run through the computation
  i = 0
  # Adding months
  if (n > 0){
    while (i < n){
      m = m + 1
      if (m == 13){
        m = 1
        y = y + 1
      }
      i = i + 1
    }
  }
  # Subtracting months
  else if (n < 0){
    while (i > n){
      m = m - 1
      if (m == 0){
        m = 12
        y = y - 1
      }
      i = i - 1
    }
  }

  # If past 28th day in base month, make adjustments for February
  if (d > 28 & m == 2){
      # If it's a leap year, return the 29th day
      if ((y %% 4 == 0 & y %% 100 != 0) | y %% 400 == 0){d = 29}
      # Otherwise, return the 28th day
      else{d = 28}
    }
  # If 31st day in base month but only 30 days in end month, return 30th day
  else if (d == 31){if (m %in% c(1, 3, 5, 7, 8, 10, 12) == FALSE){d = 30}}

  # Turn year, month, and day into strings and put them together to make a Date
  y = as.character(y)

  # If month is single digit, add a leading 0, otherwise leave it alone
  if (m < 10){m = paste('0', as.character(m), sep = '')}
  else{m = as.character(m)}

  # If day is single digit, add a leading 0, otherwise leave it alone
  if (d < 10){d = paste('0', as.character(d), sep = '')}
  else{d = as.character(d)}

  # Put them together and convert return the result as a Date
  return(as.Date(paste(y,'-',m,'-',d, sep = '')))
}

一些例子

添加月份

> addMonth('2014-01-31', n = 1)
[1] "2014-02-28"  # February, non-leap year
> addMonth('2014-01-31', n = 5)
[1] "2014-06-30"  # June only has 30 days, so day of month dropped to 30
> addMonth('2014-01-31', n = 24)
[1] "2016-01-31"  # Increments years when n is a multiple of 12 
> addMonth('2014-01-31', n = 25)
[1] "2016-02-29"  # February, leap year

减去月份

> addMonth('2014-01-31', n = -1)
[1] "2013-12-31"
> addMonth('2014-01-31', n = -7)
[1] "2013-06-30"
> addMonth('2014-01-31', n = -12)
[1] "2013-01-31"
> addMonth('2014-01-31', n = -23)
[1] "2012-02-29"

1

我将Antonio的想法转化为一个具体的函数:

library(DescTools)

> AddMonths(as.Date('2004-01-01'), 1)
[1] "2004-02-01"

> AddMonths(as.Date('2004-01-31'), 1)
[1] "2004-02-29"

> AddMonths(as.Date('2004-03-30'), -1)
[1] "2004-02-29"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接