Функция ADD MONTHS не возвращает правильную дату в Oracle

см. результаты приведенных ниже запросов:

>> SELECT ADD_MONTHS(TO_DATE('30-MAR-11','DD-MON-RR'),-4) FROM DUAL;
30-NOV-10


>> SELECT ADD_MONTHS(TO_DATE('30-NOV-10','DD-MON-RR'),4) FROM DUAL;
31-MAR-11

Как я могу получить "30-MAR-11" при добавлении 4 месяцев к какой-то дате?

пожалуйста, помогите.

9 ответов


здесь есть еще один вопрос о Oracle и Java

в нем говорится, что

из ссылки Oracle на add_months http://download-west.oracle.com/docs/cd/B19306_01/server.102/b14200/functions004.htm

если дата-последний день месяца или если в полученном месяце меньше дней, чем в компоненте день даты, то результатом является последний день полученного месяца. В противном случае результат имеет компонент того же дня, что и дата.

поэтому, я думаю, вам нужно вручную проверить день и День окончания, чтобы изменить поведение функции. Или, может быть, добавляя дни вместо месяцев. (Но я не нашел add_day функция в ref)


в качестве обходного пути я мог бы использовать этот алгоритм:

  1. рассчитать срок TargetDate1 используя ADD_MONTHS.
  2. также можно рассчитать срок TargetDate2 такой:

    1) применить ADD_MONTHS до первого числа месяца исходной даты;
    2) добавить разницу в днях между исходной датой и началом того же месяца.

  3. выберите LEAST между TargetDate1 и TargetDate2.

таким образом, в конце концов, целевая дата будет содержать другой компонент дня, если компонент дня исходной даты больше, чем количество дней в целевом месяце. В этом случае целевой датой будет последний день соответствующего месяца.

Я не совсем уверен в своих знаниях синтаксиса SQL Oracle, но в основном реализация может выглядеть так:

SELECT
  LEAST(
    ADD_MONTHS(SourceDate, Months),
    ADD_MONTHS(TRUNC(SourceDate, 'MONTH'), Months)
      + (SourceDate - TRUNC(SourceDate, 'MONTH'))
  ) AS TargetDate
FROM (
  SELECT
    TO_DATE('30-NOV-10', 'DD-MON-RR') AS SourceDate,
    4 AS Months
  FROM DUAL
)

вот подробная иллюстрация как работает метод:

SourceDate = '30-NOV-10'
Months     = 4

TargetDate1 = ADD_MONTHS('30-NOV-10', 4) = '31-MAR-11'  /* unacceptable */
TargetDate2 = ADD_MONTHS('01-NOV-10', 4) + (30 - 1)
            = '01-MAR-11' + 29 = '30-MAR-11'            /* acceptable */
TargetDate  = LEAST('31-MAR-11', '30-MAR-11') = '30-MAR-11'

и вот еще несколько примеров, чтобы показать разные случаи:

SourceDate | Months | TargetDate1 | TargetDate2 | TargetDate
-----------+--------+-------------+-------------+-----------
 29-NOV-10 |    4   |   29-MAR-11 |   29-MAR-11 |  29-MAR-11
 30-MAR-11 |   -4   |   30-NOV-10 |   30-NOV-10 |  30-NOV-10
 31-MAR-11 |   -4   |   30-NOV-10 |   01-DEC-10 |  30-NOV-10
 30-NOV-10 |    3   |   28-FEB-11 |   02-MAR-11 |  28-FEB-11

вы можете использовать интервальную арифметику, чтобы получить желаемый результат

SQL> select date '2011-03-30' - interval '4' month
  2    from dual;

DATE'2011
---------
30-NOV-10

SQL> ed
Wrote file afiedt.buf

  1  select date '2010-11-30' + interval '4' month
  2*   from dual
SQL> /

DATE'2010
---------
30-MAR-11

имейте в виду, однако, что есть подводные камни для интервальной арифметики, если вы работаете с днями, которые не существуют в каждом месяце

SQL> ed
Wrote file afiedt.buf

  1  select date '2011-03-31' + interval '1' month
  2*   from dual
SQL> /
select date '2011-03-31' + interval '1' month
                         *
ERROR at line 1:
ORA-01839: date not valid for month specified

Как насчет чего-то вроде этого:

SELECT
    LEAST(
        ADD_MONTHS(TO_DATE('30-MAR-11','DD-MON-RR'),-4),
        ADD_MONTHS(TO_DATE('30-MAR-11','DD-MON-RR')-1,-4)+1
    )
FROM
    DUAL
;

результат: 30-ноя-10

SELECT
    LEAST(
        ADD_MONTHS(TO_DATE('30-NOV-10','DD-MON-RR'),4),
        ADD_MONTHS(TO_DATE('30-NOV-10','DD-MON-RR')-1,4)+1
    )
FROM
    DUAL
;

результат: 30-MAR-11


на add_months функция возвращает дату плюс N месяцев.

поскольку 30 ноября является последней датой месяца, добавление 4 месяцев приведет к дате окончания 4 месяцев. Это ожидаемое поведение. Если даты не обязаны меняться, обходной путь-вычесть день после возвращения новой даты

SQL> SELECT ADD_MONTHS(TO_DATE('30-NOV-10','DD-MON-RR'),4) -1 from dual;

ADD_MONTH
---------
30-MAR-11

SELECT TO_DATE('30-NOV-10','DD-MON-RR') + 
       (
        ADD_MONTHS(TRUNC(TO_DATE('30-NOV-10','DD-MON-RR'),'MM'),4) - 
        TRUNC(TO_DATE('30-NOV-10','DD-MON-RR'),'MM')
       ) RESULT
  FROM DUAL;

в этом разделе в скобках

ADD_MONTHS(TRUNC(TO_DATE('30-NOV-10','DD-MON-RR'),'MM'),4) - TRUNC(TO_DATE('30-NOV-10','DD-MON-RR'),'MM')

дает вам количество дней между введенной датой и 4 месяцами позже. Таким образом, добавление этого количества дней к указанной вами дате дает точную дату через 4 месяца.

Ref:http://www.dba-oracle.com/t_test_data_date_generation_sql.htm


простое решение:

ADD_MONTHS(date - 1, x) + 1

вот трюк:

select add_months(to_date('20160228', 'YYYYMMDD')-1, 1)+1 from dual;

наслаждайтесь!


    CREATE OR REPLACE FUNCTION My_Add_Month(
      STARTDATE           DATE,
      MONTHS_TO_ADD      NUMBER
    )
        RETURN DATE
    IS
        MY_ADD_MONTH_RESULT DATE;
    BEGIN

        SELECT ORACLES_ADD_MONTH_RESULT + NET_DAYS_TO_ADJUST INTO MY_ADD_MONTH_RESULT FROM
        (
            SELECT T.*,CASE WHEN SUBSTRACT_DAYS > ADD_DAYS THEN ADD_DAYS - SUBSTRACT_DAYS ELSE 0 END AS NET_DAYS_TO_ADJUST FROM
            (
                SELECT T.*,EXTRACT(DAY FROM ORACLES_ADD_MONTH_RESULT) AS SUBSTRACT_DAYS FROM
                (
                    SELECT ADD_MONTHS(STARTDATE,MONTHS_TO_ADD) AS ORACLES_ADD_MONTH_RESULT,EXTRACT(DAY FROM STARTDATE) AS ADD_DAYS FROM DUAL
                )T
            )T
        )T;
        RETURN TRUNC(MY_ADD_MONTH_RESULT);
    END My_Add_Month;
    /

    --test & verification of logic & function both
    SELECT T.*,ORACLES_ADD_MONTH_RESULT + NET_DAYS_TO_ADJUST AS MY_ADD_MONTH_RESULT,
    My_Add_Month(STARTDATE,MONTHS_TO_ADD) MY_ADD_MONTH_FUNCTION_RESULT
    FROM
    (
        SELECT T.*,CASE WHEN SUBSTRACT_DAYS > ADD_DAYS THEN ADD_DAYS - SUBSTRACT_DAYS ELSE 0 END AS NET_DAYS_TO_ADJUST FROM
        (
            SELECT T.*,EXTRACT(DAY FROM ORACLES_ADD_MONTH_RESULT) AS SUBSTRACT_DAYS FROM
            (
                SELECT T.*,ADD_MONTHS(STARTDATE,MONTHS_TO_ADD) AS ORACLES_ADD_MONTH_RESULT,EXTRACT(DAY FROM STARTDATE) AS ADD_DAYS FROM
                (
                    SELECT TO_DATE('28/02/2014','DD/MM/YYYY') AS STARTDATE, 1 AS MONTHS_TO_ADD FROM DUAL
                )T
            )T
        )T
    )T;        

запрос-результат

параметр StartDate 2/28/2014

MONTHS_TO_ADD 1

ORACLES_ADD_MONTH_RESULT 3/31/2014

ADD_DAYS 28

SUBSTRACT_DAYS 31

NET_DAYS_TO_ADJUST -3

MY_ADD_MONTH_RESULT 3/28/2014

MY_ADD_MONTH_FUNCTION_RESULT 3/28/2014