将YEAR TO MONTH间隔添加到TIMESTAMP值

时间:2013-07-11 09:51:09

标签: sql oracle date plsql

我有一个案例,我已将YEAR TO MONTH间隔添加到TIMESTAMP值&实现这一目标我正在以这种方式使用它

SELECT (END_DATE + NUMTOYMINTERVAL(2, 'MONTH')) FROM DUAL

上述代码几乎可以在除某些值之外的所有END_DATE值中成功运行。

例如,当END_DATE = 31-JULY-2013时,上述代码的预期结果为30-SEPT-2013,但会引发错误

ORA-01839: date not valid for month specified

这是因为上面的代码返回31-SEPT-2013,这是一个无效的日期。

有没有其他方法可以实现这一目标?

(我可以使用ADD_MONTHS但是对此功能有问题,它只返回DATE个值并且我需要TIMESTAMP作为返回值)

我错过了什么吗?

3 个答案:

答案 0 :(得分:3)

由于end_date没有小数秒,或者实际上是任何时间组件,因此您可以使用add_months并将其转换为timestamp

select cast(add_months(end_date, 2) as timestamp) from ...

add_months有其自身的怪癖。如果原始日期是该月的最后一天,您将获得调整月份的最后一天 - 如果您在这种情况下缩短月份,这就是您想要的,但如果您要去的话,可能不会另一种方式:

with t as (
select to_timestamp('2013-07-31', 'YYYY-MM-DD') as end_date from dual
union all select to_timestamp('2013-06-30', 'YYYY-MM-DD') from dual
union all select to_timestamp('2013-02-28', 'YYYY-MM-DD') from dual
union all select to_timestamp('2012-02-29', 'YYYY-MM-DD') from dual
)
select end_date, cast(add_months(end_date, 2) as timestamp)
from t;

END_DATE                       CAST(ADD_MONTHS(END_DATE,2)AST
------------------------------ ------------------------------
2013-07-31 00:00:00.000000     2013-09-30 00:00:00.000000
2013-06-30 00:00:00.000000     2013-08-31 00:00:00.000000
2013-02-28 00:00:00.000000     2013-04-30 00:00:00.000000
2012-02-29 00:00:00.000000     2012-04-30 00:00:00.000000

或者您可以创建自己的函数来处理错误的日期,然后向后调整直到找到有效日期:

create or replace function adjust_timestamp(orig_ts in timestamp,
  months in number)
return timestamp is
  new_ts timestamp;
  offset number := 0;
  bad_adjustment exception;
  pragma exception_init(bad_adjustment, -01839);
begin
  while new_ts is null loop
    begin
      new_ts := orig_ts - numtodsinterval(offset, 'DAY')
        + numtoyminterval(months, 'MONTH');
    exception
      when bad_adjustment then
        offset := offset + 1;
        continue;
    end;
  end loop;
  return new_ts;
end;
/

这使用为ORA-01839错误代码定义的异常来捕获错误的日期,并且它在一个循环中执行,因此它可以向后工作(通过offset),直到它找到一个不是错误。

with t as (
select to_timestamp('2013-07-31', 'YYYY-MM-DD') as end_date from dual
union all select to_timestamp('2013-06-30', 'YYYY-MM-DD') from dual
union all select to_timestamp('2013-02-28', 'YYYY-MM-DD') from dual
union all select to_timestamp('2012-02-29', 'YYYY-MM-DD') from dual
)
select end_date, adjust_timestamp(end_date, 2)
from t;

END_DATE                       ADJUST_TIMESTAMP(END_DATE,2)
------------------------------ ------------------------------
2013-07-31 00:00:00.000000     2013-09-30 00:00:00.000000
2013-06-30 00:00:00.000000     2013-08-30 00:00:00.000000
2013-02-28 00:00:00.000000     2013-04-28 00:00:00.000000
2012-02-29 00:00:00.000000     2012-04-29 00:00:00.000000

这为add_months版本提供了不同的结果。您需要确定所获得的内容以及数据的行为方式。

答案 1 :(得分:2)

这是ANSI规定的预期行为 - 请参阅this AskTom。如果您在2013年7月30日之前添加两个月,您将获得2013年9月30日,我认为这是完全可以理解的。如果你在2013年7月31日增加两个月,你会得到......什么?没有2013年9月31日 - 9月只有30天。那么,系统应该做什么?它应该给你30-SEP-2013吗?它应该给你01-OCT-2013吗?这些都不正确。你已经问过两个月将月值改为两个月。好吧,它会尝试并发现结果日期无效 - 因此它会抛出错误。

哦,亲爱的。

但是 - 谢天谢地,我们不仅仅是凡人。我们是优越的生命。我们是软件开发人员。我们有手册!!!!我们不是上帝!!!!!!!!!!

所以,咨询the manual我们发现我们可以使用ADD_MONTHS功能,这几乎可以满足您的需求。但是,ADD_MONTHS仅在DATE值上运行,因此如果您没有进行额外的游戏以保存它们,则会丢失小数秒。但是,正如我所说,我们是软件开发人员......

示例:

DECLARE 
  tsIn  TIMESTAMP := TO_TIMESTAMP('31-JUL-2013 17:31:01', 'DD-MON-YYYY HH24:MI:SS');
  tsOut TIMESTAMP;
  nFrac_secs  NUMBER;
  strBuffer   VARCHAR2(1000);
  strFrac_secs VARCHAR2(1000);
BEGIN
  tsIn := tsIn + NUMTODSINTERVAL(0.1234, 'SECOND');

  strBuffer := TO_CHAR(tsIn);
  strFrac_secs := SUBSTR(strBuffer, -10, 7);

  DBMS_OUTPUT.PUT_LINE('tsIn=' || tsIn);
  DBMS_OUTPUT.PUT_LINE('strBuffer=' || strBuffer);
  DBMS_OUTPUT.PUT_LINE('strFrac_secs=' || strFrac_secs);

  nFrac_secs := TO_NUMBER(strFrac_secs);

  DBMS_OUTPUT.PUT_LINE('nFrac_secs=' || nFrac_secs);

  tsOut := ADD_MONTHS(tsIn, 2);

  DBMS_OUTPUT.PUT_LINE('tsOut before restoring fractional seconds=' || tsOut);

  tsOut := tsOut + NUMTODSINTERVAL(nFrac_secs, 'SECOND');

  DBMS_OUTPUT.PUT_LINE('tsOut after restoring fractional seconds=' || tsOut);
END;

所以,基本上,如果你尝试进行区间运算,Oracle遵循& ^#@ $# ANSI规范并播放愚蠢。然后他们会给你一个函数(这是公平的,记录在案的),它或多或少地做了什么,但只在DATE值上做。我认为这就是所谓的“工作保障”......

: - )

分享并享受。

答案 2 :(得分:0)

您可以使用add_months转到正确的日期,然后添加时间戳的小数部分:

SELECT CURRENT_TIMESTAMP,
       CAST(CAST(add_months(trunc(CURRENT_TIMESTAMP), 2) AS TIMESTAMP) +
            (CURRENT_TIMESTAMP -
             CAST(trunc(CURRENT_TIMESTAMP) as timestamp)) as timestamp)
FROM DUAL;

请注意,您将失去时区。