TO_CHAR возвращает неверные результаты при сравнении со строкой даты в Oracle - PullRequest
0 голосов
/ 01 апреля 2020

Я пытаюсь написать запрос, который получает первый рабочий день месяца текущего года. Правила таковы: если 1-е число месяца - суббота / воскресенье, возьмите дату следующего понедельника. Кроме того, если это 1 января, в любом случае всегда принимайте следующую рабочую дату (пятница, суббота, воскресенье - следующий понедельник, а все остальные дела - на следующий рабочий день).

Logi c отлично работает с февраля по декабрь, но для января он возвращает 2 строки для расчета даты. Тот, который является правильным, и тот, который является неправильным.

Я думаю, что проблема возникает, когда я пытаюсь сравнить sysdate с '01 .01 '.

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

Вы увидите, что за январь возвращаются 2 строки (первая неверна, а вторая верна):

BD_DATE   DAY_NUM   ADD_DAYS DATE_CALC
--------- ------- ---------- ---------
01-JAN-20 4                0 01-JAN-20
01-JAN-20 4                1 02-JAN-20
01-FEB-20 7                2 03-FEB-20
01-MäR-20 1                1 02-MäR-20
01-APR-20 4                0 01-APR-20
01-MAI-20 6                0 01-MAI-20
01-JUN-20 2                0 01-JUN-20
01-JUL-20 4                0 01-JUL-20
01-AUG-20 7                2 03-AUG-20
01-SEP-20 3                0 01-SEP-20
01-OKT-20 5                0 01-OKT-20
01-NOV-20 1                1 02-NOV-20
01-DEZ-20 3                0 01-DEZ-20

13 rows selected.

Мой SQL запрос:

/* Formatted on 01.04.2020 18:37:56 (QP5 v5.163.1008.3004) */
WITH dateparam
     AS (    SELECT TRUNC (SYSDATE, 'YYYY') + LEVEL - 1 AS mydate
               FROM DUAL
         CONNECT BY TRUNC (TRUNC (SYSDATE, 'YYYY') + LEVEL - 1, 'YYYY') =
                       TRUNC (SYSDATE, 'YYYY'))
  SELECT DISTINCT
         ADD_MONTHS (LAST_DAY (mydate) + 1, -1) bd_date,
         TO_CHAR (ADD_MONTHS (LAST_DAY (mydate) + 1, -1), 'fmD') day_num,
         /*TO_CHAR (mydate, 'DD.MM') IN ('01.01') THEN
                                                   (DECODE (
                                                       (to_char(add_months(last_day(mydate)+1,-1), 'fmD')),
                                                       6, 3,
                                                       7, 2,
                                                       1, 1,
                                                       1))
                                                ELSE*/
         CASE
            WHEN TO_CHAR (mydate, 'DD.MM') IN ('01.01') 
            THEN
               CASE
                  WHEN TO_CHAR (ADD_MONTHS (LAST_DAY (mydate) + 1, -1), 'fmD') =
                          7
                  THEN
                     2
                  WHEN TO_CHAR (ADD_MONTHS (LAST_DAY (mydate) + 1, -1), 'fmD') =
                          6
                  THEN
                     3
                  ELSE
                     1
               END
            ELSE
               CASE
                  WHEN TO_CHAR (ADD_MONTHS (LAST_DAY (mydate) + 1, -1), 'fmD') =
                          7
                  THEN
                     2
                  WHEN TO_CHAR (ADD_MONTHS (LAST_DAY (mydate) + 1, -1), 'fmD') =
                          1
                  THEN
                     1
                  ELSE
                     0
               END
         END
            add_days,
         ADD_MONTHS (LAST_DAY (mydate) + 1, -1)
         + CASE
              WHEN TO_CHAR (mydate, 'DD.MM') IN ('01.01')
              THEN
                 (DECODE (
                     (TO_CHAR (ADD_MONTHS (LAST_DAY (mydate) + 1, -1), 'fmD')),
                     6, 3,
                     7, 2,
                     1, 1,
                     1))
              ELSE
                 (DECODE (
                     (TO_CHAR (ADD_MONTHS (LAST_DAY (mydate) + 1, -1), 'fmD')),
                     7, 2,
                     1, 1,
                     0))
           END
            date_calc
    FROM dateparam
ORDER BY 1;

Я пытался использовать оба DECODE и CASE, и оба блока возвращают одинаковые результаты.

Есть идеи, что я сделал неправильно?

Ответы [ 3 ]

2 голосов
/ 01 апреля 2020

TO_CHAR зависит от настроек NLS, включая территорию и язык. Если вы хотите выполнить расчет независимо от настроек NLS, вы можете использовать тот факт, что неделя ISO всегда начинается в понедельник, и рассчитать разницу в количестве дней между первым днем ​​месяца и началом (понедельником) ISO. неделя, содержащая первый день месяца:

WITH months ( first_day ) AS (
  SELECT CASE LEVEL
         WHEN 1 THEN TRUNC( SYSDATE, 'YYYY' ) + INTERVAL '1' DAY
         ELSE ADD_MONTHS( TRUNC( SYSDATE, 'YYYY' ), LEVEL - 1 )
         END
  FROM   DUAL
  CONNECT BY LEVEL <= 12
)
SELECT first_day
       + CASE first_day - TRUNC( first_day, 'IW' )
         WHEN 5 THEN 2 -- Saturday
         WHEN 6 THEN 1 -- Sunday
                ELSE 0 -- Weekday
         END
         AS first_business_day
FROM   months;

Что выводит:

| FIRST_BUSINESS_DAY |
| :----------------- |
| 2020-01-02 (THU)   |
| 2020-02-03 (MON)   |
| 2020-03-02 (MON)   |
| 2020-04-01 (WED)   |
| 2020-05-01 (FRI)   |
| 2020-06-01 (MON)   |
| 2020-07-01 (WED)   |
| 2020-08-03 (MON)   |
| 2020-09-01 (TUE)   |
| 2020-10-01 (THU)   |
| 2020-11-02 (MON)   |
| 2020-12-01 (TUE)   |

db <> скрипка здесь


Есть идеи, что я сделал неправильно?

Запрос слишком сложный. Вы генерируете все дни года в условии факторинга подзапроса (WITH), когда вас интересуют только первые дни месяца, а это означает, что во второй части запроса вы заменяете каждый день на первое число месяца с использованием:

ADD_MONTHS (LAST_DAY (mydate) + 1, -1)

Что можно упростить до:

TRUNC( mydate, 'MM' )

Или, если вы только что сгенерировали в инкрементных месяцах, чтобы начать с, вы бы даже не нужно усечь, так как у вас будут только строки для первого дня месяца:

WITH months ( first_day ) AS (
  SELECT ADD_MONTHS( TRUNC( SYSDATE, 'YYYY' ), LEVEL - 1 )
  FROM   DUAL
  CONNECT BY LEVEL <= 12
)

Но, оставив это в стороне, ваш оператор add_days проверяет, является ли дата первым месяца, а затем применяется два различных набора логик c дней, которые являются или не являются первыми месяца:

Этот раздел вашего запроса можно записать проще:

CASE
WHEN TO_CHAR (mydate, 'DD.MM')  = '01.01'
THEN CASE TO_CHAR( TRUNC( mydate, 'MM' ), 'fmD' )
     WHEN '7' THEN 2
     WHEN '6' THEN 3
              ELSE 1
     END
ELSE CASE TO_CHAR( TRUNC( mydate, 'MM' ), 'fmD' )
     WHEN '7' THEN 2
     WHEN '1' THEN 1       -- Different to the above
              ELSE 0       -- Again, different.
     END
END AS add_days

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

все должно (опять же, игнорируя, что вы генерируете слишком много строк) быть примерно таким:

WITH dateparam ( mydate ) AS (
  SELECT TRUNC(SYSDATE, 'YYYY') + LEVEL - 1
  FROM DUAL
  CONNECT BY LEVEL <= ADD_MONTHS( TRUNC( SYSDATE, 'YYYY' ), 12 ) - TRUNC( SYSDATE, 'YYYY' )
)
SELECT DISTINCT
       TRUNC( mydate, 'MM' ) bd_date,
       TO_CHAR( TRUNC( mydate, 'MM' ) , 'fmD') day_num,
       CASE TO_CHAR( TRUNC( mydate, 'MM' ), 'fmD')
       WHEN '7' THEN 2
       WHEN '1' THEN 1
       ELSE 0
       END add_days,
       TRUNC( mydate, 'MM' )
       + CASE TO_CHAR( TRUNC( mydate, 'MM' ), 'fmD')
         WHEN '7' THEN 2
         WHEN '1' THEN 1
         ELSE 0
         END date_calc
FROM dateparam
ORDER BY 1;

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

Например:

ALTER SESSION SET NLS_TERRITORY = 'America';
ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD "("DY")"';

и приведенный выше запрос выводит:

BD_DATE          | DAY_NUM | ADD_DAYS | DATE_CALC       
:--------------- | :------ | -------: | :---------------
2020-01-01 (WED) | 4       |        0 | 2020-01-01 (WED)
2020-02-01 (SAT) | 7       |        2 | 2020-02-03 (MON)
2020-03-01 (SUN) | 1       |        1 | 2020-03-02 (MON)
2020-04-01 (WED) | 4       |        0 | 2020-04-01 (WED)
2020-05-01 (FRI) | 6       |        0 | 2020-05-01 (FRI)
2020-06-01 (MON) | 2       |        0 | 2020-06-01 (MON)
2020-07-01 (WED) | 4       |        0 | 2020-07-01 (WED)
2020-08-01 (SAT) | 7       |        2 | 2020-08-03 (MON)
2020-09-01 (TUE) | 3       |        0 | 2020-09-01 (TUE)
2020-10-01 (THU) | 5       |        0 | 2020-10-01 (THU)
2020-11-01 (SUN) | 1       |        1 | 2020-11-02 (MON)
2020-12-01 (TUE) | 3       |        0 | 2020-12-01 (TUE)

но:

ALTER SESSION SET NLS_TERRITORY = 'France';
ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD "("DY")"';

и выводит:

BD_DATE          | DAY_NUM | ADD_DAYS | DATE_CALC       
:--------------- | :------ | -------: | :---------------
2020-01-01 (WED) | 3       |        0 | 2020-01-01 (WED)
2020-02-01 (SAT) | 6       |        0 | 2020-02-01 (SAT)
2020-03-01 (SUN) | 7       |        2 | 2020-03-03 (TUE)
2020-04-01 (WED) | 3       |        0 | 2020-04-01 (WED)
2020-05-01 (FRI) | 5       |        0 | 2020-05-01 (FRI)
2020-06-01 (MON) | 1       |        1 | 2020-06-02 (TUE)
2020-07-01 (WED) | 3       |        0 | 2020-07-01 (WED)
2020-08-01 (SAT) | 6       |        0 | 2020-08-01 (SAT)
2020-09-01 (TUE) | 2       |        0 | 2020-09-01 (TUE)
2020-10-01 (THU) | 4       |        0 | 2020-10-01 (THU)
2020-11-01 (SUN) | 7       |        2 | 2020-11-03 (TUE)
2020-12-01 (TUE) | 2       |        0 | 2020-12-01 (TUE)

db <> fiddle здесь

Необходимо либо убедиться, что параметр NLS_TERRITORY никогда не изменится (однако, ANY пользователь может установите его на любое значение, которое они хотят в своем сеансе в любое время) или используйте что-то, что agnosti c, к настройкам NLS (как мой пример вверху). Вы не можете исправить запрос с моделью формата D, используя третий аргумент TO_CHAR, поскольку он позволяет только установить NLS_DATE_LANGUAGE (который не действует в первый день недели) и не принимает NLS_TERRITORY (который контролирует первый день недели), но вы можете использовать модель формата DY, если вы действительно зациклены на использовании TO_CHAR. Т.е.:

CASE TO_CHAR( TRUNC( mydate, 'MM' ), 'DY', 'NLS_DATE_LANGUAGE=American' )
WHEN 'SAT' THEN 2
WHEN 'SUN' THEN 1
           ELSE 0
END add_days
1 голос
/ 02 апреля 2020

Я нашел проблему. Это не имеет ничего общего с NLS, а с именем столбца, которое я передаю to_char. Вместо mydate я должен был заменить его на bd_date.

Чтобы показать мою работу:

WITH dateparam
     AS (    SELECT TRUNC (SYSDATE, 'YYYY') + LEVEL - 1 AS mydate
               FROM DUAL
         CONNECT BY TRUNC (TRUNC (SYSDATE, 'YYYY') + LEVEL - 1, 'YYYY') =
                       TRUNC (SYSDATE, 'YYYY'))
  SELECT
         mydate,
         ADD_MONTHS (LAST_DAY (mydate) + 1, -1) bd_date,
         TO_CHAR (ADD_MONTHS (LAST_DAY (mydate) + 1, -1), 'fmD') day_num,
         CASE
            WHEN TO_CHAR (mydate, 'DD.MM') IN ('01.01') 
            THEN
               CASE
                  WHEN TO_CHAR (ADD_MONTHS (LAST_DAY (mydate) + 1, -1), 'fmD') =
                          7
                  THEN
                     2
                  WHEN TO_CHAR (ADD_MONTHS (LAST_DAY (mydate) + 1, -1), 'fmD') =
                          6
                  THEN
                     3
                  ELSE
                     1
               END               
            ELSE
               CASE
                  WHEN TO_CHAR (ADD_MONTHS (LAST_DAY (mydate) + 1, -1), 'fmD') =
                          7
                  THEN
                     2
                  WHEN TO_CHAR (ADD_MONTHS (LAST_DAY (mydate) + 1, -1), 'fmD') =
                          1
                  THEN
                     1
                  ELSE
                     0
               END
         END
            add_days
         /*ADD_MONTHS (LAST_DAY (mydate) + 1, -1)
         + CASE
              WHEN TO_CHAR (mydate, 'DD.MM') IN ('01.01')
              THEN
                 (DECODE (
                     (TO_CHAR (ADD_MONTHS (LAST_DAY (mydate) + 1, -1), 'fmD')),
                     6, 3,
                     7, 2,
                     1, 1,
                     1))
              ELSE
                 (DECODE (
                     (TO_CHAR (ADD_MON(LAST_DAY (mydate) + 1, -1), 'fmD')),
                     7, 2,
                     1, 1,
                     0))
           END
            date_calc*/
    FROM dateparam where mydate <= '03JAN2020'
ORDER BY 1;

Выход:

MYDATE    BD_DATE   DAY_NUM   ADD_DAYS
--------- --------- ------- ----------
01-JAN-20 01-JAN-20 4                1
02-JAN-20 01-JAN-20 4                0
03-JAN-20 01-JAN-20 4                0

3 rows selected.

Проблема была в моем исходном коде:

WHEN TO_CHAR (mydate, 'DD.MM') IN ('01.01') 

Это делает расчет для 01.01 и 02.01, но bd_date одинаков для всех дней в январе. Поэтому, как только я сделаю distinct для всех строк в январе, я получу две строки, как показано выше, после удаления столбца mydate.

Исправленное сравнение должно быть вместо bd_date:

WHEN TO_CHAR ( ADD_MONTHS (LAST_DAY (mydate) + 1, -1), 'DD.MM') IN ('01.01') --Get bd_date using mydate

Рабочий код:

/* Formatted on 02.04.2020 10:42:53 (QP5 v5.163.1008.3004) */
WITH dateparam
     AS (    SELECT TRUNC (SYSDATE, 'YYYY') + LEVEL - 1 AS mydate
               FROM DUAL
         CONNECT BY TRUNC (TRUNC (SYSDATE, 'YYYY') + LEVEL - 1, 'YYYY') =
                       TRUNC (SYSDATE, 'YYYY'))
  SELECT DISTINCT
         ADD_MONTHS (LAST_DAY (mydate) + 1, -1) bd_date,
         TO_CHAR (ADD_MONTHS (LAST_DAY (mydate) + 1, -1), 'fmD') day_num,
         CASE
            WHEN TO_CHAR (ADD_MONTHS (LAST_DAY (mydate) + 1, -1), 'DD.MM') IN
                    ('01.01')
            THEN
               CASE
                  WHEN TO_CHAR (ADD_MONTHS (LAST_DAY (mydate) + 1, -1), 'fmD') =
                          7
                  THEN
                     2
                  WHEN TO_CHAR (ADD_MONTHS (LAST_DAY (mydate) + 1, -1), 'fmD') =
                          6
                  THEN
                     3
                  ELSE
                     1
               END
            ELSE
               CASE
                  WHEN TO_CHAR (ADD_MONTHS (LAST_DAY (mydate) + 1, -1), 'fmD') =
                          7
                  THEN
                     2
                  WHEN TO_CHAR (ADD_MONTHS (LAST_DAY (mydate) + 1, -1), 'fmD') =
                          1
                  THEN
                     1
                  ELSE
                     0
               END
         END
            add_days,
         ADD_MONTHS (LAST_DAY (mydate) + 1, -1)
         + CASE
              WHEN TO_CHAR (ADD_MONTHS (LAST_DAY (mydate) + 1, -1), 'DD.MM') IN
                      ('01.01')
              THEN
                 (DECODE (
                     (TO_CHAR (ADD_MONTHS (LAST_DAY (mydate) + 1, -1), 'fmD')),
                     6, 3,
                     7, 2,
                     1, 1,
                     1))
              ELSE
                 (DECODE (
                     (TO_CHAR (ADD_MONTHS (LAST_DAY (mydate) + 1, -1), 'fmD')),
                     7, 2,
                     1, 1,
                     0))
           END
            date_calc
    FROM dateparam
ORDER BY 1;

Результаты:


BD_DATE   DAY_NUM   ADD_DAYS DATE_CALC
--------- ------- ---------- ---------
01-JAN-20 4                1 02-JAN-20
01-FEB-20 7                2 03-FEB-20
01-MäR-20 1                1 02-MäR-20
01-APR-20 4                0 01-APR-20
01-MAI-20 6                0 01-MAI-20
01-JUN-20 2                0 01-JUN-20
01-JUL-20 4                0 01-JUL-20
01-AUG-20 7                2 03-AUG-20
01-SEP-20 3                0 01-SEP-20
01-OKT-20 5                0 01-OKT-20
01-NOV-20 1                1 02-NOV-20
01-DEZ-20 3                0 01-DEZ-20

12 rows selected.

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

Спасибо всем за ваш вклад!

0 голосов
/ 01 апреля 2020

Я думаю, вы можете использовать следующий запрос:

with dates (dt) as
(select
add_months(trunc(sysdate,'year'),level-1)
from dual
connect by level <=12)
select dt, dt-dto as daysdiff from
(select case to_char(dt,'dy')
          when 'sat' 
          then dt+2 
          when 'sun'
          then dt+1
          else dt
          end as dt,
          dto
     from
(select case when extract(month from dt) = 1
       then dt + 1
       else dt 
       end as dt,
       dt as dto
  from dates))

См. db <> fiddle demo.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...