计算两个日期之间每月的天数

时间:2016-10-17 13:00:27

标签: oracle

使用Oracle 12c,我需要在现有的项目摘要表上运行脚本。摘要表包含项目,开始日期和结束日期。我需要将这些数据分解为每个项目每月的天数。

一个例子是项目A的开始日期为2016年10月2日,结束日期为2016年10月3日。我这个例子的结局应该是:

Project A, February, 19
Project A, March, 10

这是一个更容易的,因为一些日期可能跨越2或3个月。这似乎并不太难,但由于某种原因,我无法绕过它并过度思考它。有人有一个快速简便的解决方案吗?我想将它作为SQL语句运行,但PL / SQL脚本也可以运行。

3 个答案:

答案 0 :(得分:1)

如果您可以对项目的最大天数(在我的示例中为1000)进行假设,则可以使用以下内容:

with yourTable(project, startDate, endDate) as 
(
    select 'Project a' as project,
           date '2016-02-10' as startDate,
           date '2016-03-10' as endDate
    from dual
    UNION ALL
    select 'Project XX',
           date '2016-01-01',
           date '2016-01-10'
    from dual
)
select project, to_char(startDate + n, 'MONTH'), count(1)
from yourTable
  inner join (
                select level n
                from dual
                connect by level <= 1000
             )
    on (startDate + n <= endDate)        
group by project, to_char(startDate + n, 'MONTH')  

带有CONNECT BY的零件用作日期生成器,假设每个项目最长1000天;外部查询使用日期生成器将项目的行分成多行,每天一个,在开始日期和结束日期之间,然后按月和项目聚合以构建输出。

基于月份而非日期的略有不同的方式可能是:

select project, to_char(add_months(startDate, n ), 'MONTH'),
       case
        when trunc(add_months(startDate, n ), 'MONTH') = trunc(add_months(endDate, n ), 'MONTH')
            then endDate - startDate +1
        when trunc(add_months(startDate, n ), 'MONTH') <= startDate
            then last_day(add_months(startDate, n)) - startDate
        when last_day(add_months(startDate, n )) >= endDate
            then endDate - trunc(add_months(startDate, n ), 'MONTH') +1      
        else
            last_day(add_months(startDate, n )) - trunc(last_day(add_months(startDate, n )), 'MONTH')
       end as numOfDays       
from yourTable
  inner join (
                select level -1 n
                from dual
                connect by level <= 1000
             )
    on trunc(add_months(startDate, n ), 'MONTH') <= trunc(endDate, 'MONTH') 

这有点复杂,处理不同的情况,但效率更高,因为它在月级而非日级工作

答案 1 :(得分:1)

在此解决方案中,我们不会假设所涵盖的时间段有任何先验知识。此外,此解决方案不使用连接(这可能对性能很重要)。

DateFormat gmtMinus12 = new SimpleDateFormat("yyyyMMdd");
gmtMinus12.setTimeZone(TimeZone.getTimeZone("Etc/GMT-12"));
// gmtMinus12.getCalendar().setTimeZone(TimeZone.getTimeZone("Etc/GMT-12"));
gmtMinus12
    .setCalendar(Calendar.getInstance(TimeZone.getTimeZone("Etc/GMT-12")));

try {
  Date date = gmtMinus12.parse("20160104");

} catch (ParseException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
}

<强>输出

with
-- begin test data (this section can be deleted)
     inputs ( project, start_date, end_date ) as (
       select 'A', date '2014-10-03', date '2014-12-15' from dual union all
       select 'B', date '2015-03-01', date '2015-03-31' from dual union all
       select 'C', date '2015-11-30', date '2016-03-01' from dual
     ),
-- end test data; solution begins here (it includes the word "with" from the first line)
     prep ( project, end_date, dt ) as (
       select project, end_date, start_date   from inputs union all
       select project, end_date, end_date + 1 from inputs union all
       select project, end_date, add_months( trunc(start_date, 'mm'), level )
         from inputs
         connect by add_months (trunc(start_date, 'mm'), level) <= end_date
                and prior project = project
                and prior sys_guid() is not null
     ),
     computations ( project, dt, month, day_count ) as (
       select project, dt, to_char(dt, 'Mon-yyyy'),
              lead(dt) over (partition by project order by dt) - dt
       from   prep
       where  dt <= end_date + 1
     )
select project, month, day_count
from   computations
where  day_count > 0
order by project, dt
;

答案 2 :(得分:1)

我认为你是这样的:

WITH sample_data AS (SELECT 'A' PROJECT, to_date('10/02/2016', 'dd/mm/yyyy') start_date, to_date('10/03/2016', 'dd/mm/yyyy') end_date FROM dual UNION ALL
                     SELECT 'B' PROJECT, to_date('10/02/2016', 'dd/mm/yyyy') start_date, to_date('10/06/2016', 'dd/mm/yyyy') end_date FROM dual UNION ALL
                     SELECT 'C' PROJECT, to_date('10/02/2016', 'dd/mm/yyyy') start_date, to_date('18/02/2016', 'dd/mm/yyyy') end_date FROM dual)
SELECT PROJECT,
       to_char(add_months(trunc(start_date, 'mm'), LEVEL -1), 'fmMonth yyyy', 'nls_date_language=english') mnth,
       CASE WHEN trunc(end_date, 'mm') = add_months(trunc(start_date, 'mm'), LEVEL -1)
                 THEN end_date
            ELSE add_months(trunc(start_date, 'mm'), LEVEL) -1
       END - CASE WHEN trunc(start_date, 'mm') = add_months(trunc(start_date, 'mm'), LEVEL -1)
                 THEN start_date + 1
            ELSE add_months(trunc(start_date, 'mm'), LEVEL -1)
       END + 1 num_days
FROM   sample_data
CONNECT BY PRIOR PROJECT = PROJECT
           AND PRIOR sys_guid() IS NOT NULL
           AND add_months(trunc(start_date, 'mm'), LEVEL -1) <= TRUNC(end_date, 'mm');

PROJECT MNTH             NUM_DAYS
------- -------------- ----------
A       February 2016          19
A       March 2016             10
B       February 2016          19
B       March 2016             31
B       April 2016             30
B       May 2016               31
B       June 2016              10
C       February 2016           8

这使用多行逐级连接技术(and prior sys_guid() is not null的存在使connect by能够分别循环遍历每一行)循环遍历sample_data表中的每个项目行(你可能有表中的项目信息已经存在,因此您根本不需要拥有sample_data子查询;您可以直接在主SQL中引用您的表。)

然后我们将开始日期的月份与connect by生成的行的月份进行比较,如果它是同一个月,那么我们知道我们需要使用开始日期,否则我们使用生成的行的第一个月;我们在结束日期也这样做。

这样,我们现在可以从另一个中减去一个并进行调整以使计算正确。如果您需要同一天的开始和结束日期计为1天而不是0,您可能需要自己调整一下 - 它可能需要一个额外的案例陈述来考虑开始和结束日期的时间是在同一个月。

使用这种方法不会限制您的项目长度;它可能只要你喜欢。

ETA:看起来Mathguy在我输入答案时发布了答案,虽然我们的基本方法是相同的,但我并没有使用分析函数来确定天数的差异。您可能会或可能不会发现他们的答案比我的更高效 - 您应该测试两者以确定哪一个最适合您的数据。