【问题标题】:Adding YEAR TO MONTH interval to TIMESTAMP value将 YEAR TO MONTH 间隔添加到 TIMESTAMP 值
【发布时间】:2013-07-09 13:01:01
【问题描述】:

我有一个案例,我将 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 作为返回值)

我错过了什么吗?

【问题讨论】:

  • :你的结束日期是否包含时间戳?
  • end_date 是否有小数秒?如果是月底,您是否总是想调整日期?那么 2013-02-28 之后的 2 个月是 2013-04-28 还是 2013-04-30?确实,这总是一个月的最后一天吗?
  • @GauravSoni 是的,END_DATE 是时间戳值。

标签: sql oracle date plsql


【解决方案1】:

由于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 版本带来了不同的结果。你需要确定你得到了什么,以及你希望数据如何表现。

【讨论】:

    【解决方案2】:

    您可以使用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;
    

    请注意,您将丢失时区。

    【讨论】:

      【解决方案3】:

      这是 ANSI 指定的预期行为 - 请参阅 this AskTom。如果将 30-JUL-2013 加上两个月,您将得到 30-SEP-2013,我认为这是完全可以理解的。如果你在 2013 年 7 月 31 日加上两个月,你会得到……什么?没有 31-SEP-2013 - 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 值上做。我想这就是所谓的“工作保障”……

      :-)

      分享和享受。

      【讨论】:

      • +1,尽管 OP 提到了add_months,并且 cmets 建立的小数秒在这里不是问题(但不是为什么在这种情况下它根本是时间戳)。不过,这对某人有用。 add_month 行为也不总是如预期/想要的那样。第一步是为边缘情况建立所需的输出。
      • @AlexPoole - 谢谢。我经历了使用 ADD_MONTHS 所需的所有垃圾,因为 OP 提到它丢弃了小数秒。 (我最初是从一个很像你的解决方案开始的,但在中途换了马)。这无疑是一种克力,但如果我们要接近众神,我们必须愿意抓住(当然是用手套)宇宙的性腺并挤压——或者天堂有什么用? :-)
      • 嗯,有一张图片...谢谢!我认为 OP 的评论(现已删除)是没有小数秒,它只是用户输入的日期,而不是它们存在和被删除。也许我误解了。只是为了改变。
      猜你喜欢
      • 1970-01-01
      • 2012-03-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-02-11
      • 2021-10-05
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多