如何在给定日期增加工作日数

时间:2018-12-07 23:06:44

标签: sql postgresql

我正在寻找一种功能,可以将给定日期的工作日数增加。

假期表

create table pyha (pyha date primary key) ;
insert into pyha values ('2018-12-24'),('2018-12-25'),('2018-12-26'),('2019-01-01');

包含假期。星期六和星期天也是非工作日。

我试图创建函数

create or replace function add_business_day(from_date date, num_days int)
returns date
as $fbd$

with days as
(
    select dd, extract(DOW from dd) dw
    from generate_series(($1+ interval'1day')::date, ($1+ interval'1day'*$2+interval'10days')::date , '1 day'::interval) dd
)
select min(dd)::date
from   days
where  dw not in (6,0) and
dd not in (select pyha from pyha)
and dd>=$1+interval'1day'*$2+
interval'1day'*(select count(*) from pyha where pyha between $1+ interval'1day' and 
   $1+interval'1day'*$2 )
$fbd$ language sql;

但是有时返回错误的结果:

add_business_day('2018-12-08',2)  

返回2018-12-10
但正确的结果是2018-12-11

如何在Postgres 9.1+中创建此类功能?

3 个答案:

答案 0 :(得分:2)

关键是生成一系列工作日,并用row_number()进行编号:

create or replace function add_business_day(from_date date, num_days int)
returns date
as $fbd$
    select d
    from (
        select d::date, row_number() over (order by d)
        from generate_series(from_date+ 1, from_date+ num_days* 2+ 5, '1d') d
        where 
            extract('dow' from d) not in (0, 6) 
            and d not in (select pyha from pyha)
        ) s
    where row_number = num_days
$fbd$ language sql;

测试查询的结果似乎正确:

select days, add_business_day('2018-12-08', days)
from generate_series(1, 20) days

 days | add_business_day 
------+------------------
    1 | 2018-12-10
    2 | 2018-12-11
    3 | 2018-12-12
    4 | 2018-12-13
    5 | 2018-12-14
    6 | 2018-12-17
    7 | 2018-12-18
    8 | 2018-12-19
    9 | 2018-12-20
   10 | 2018-12-21
   11 | 2018-12-27
   12 | 2018-12-28
   13 | 2018-12-31
   14 | 2019-01-02
   15 | 2019-01-03
   16 | 2019-01-04
   17 | 2019-01-07
   18 | 2019-01-08
   19 | 2019-01-09
   20 | 2019-01-10
(20 rows)

或者,您可以循环查找日期:

create or replace function add_business_day_loop(from_date date, num_days int)
returns date
as $fbd$
begin
    while num_days > 0 loop
        from_date:= from_date+ 1;
        while from_date in (select pyha from pyha) or extract('dow' from from_date) in (0, 6) loop
            from_date:= from_date+ 1;
        end loop;
        num_days:= num_days- 1;
    end loop;
    return from_date;
end;
$fbd$ language plpgsql;

答案 1 :(得分:0)

我自己遇到了这个问题-这是我编写的函数,用于尽可能接近地替换excel workday()函数,既可以添加负数也可以添加正数

create or replace function workday(startdate date, i integer) returns date as 
$$ 
with workday_cte as (
    select s.a::date as workday from 
    generate_series(startdate - ((abs(i) * 2 + 10) || ' day')::interval, 
    startdate + ((abs(i) * 2 + 10) || ' day')::interval, '1 day'::interval) s(a)
    where extract(dow from s.a) between 1 and 5
    except 
    select holiday
    from holidays
)
select case when i > 0 then a.workday when i = 0 then startdate else b.workday end from 
(
   select * from workday_cte where workday > startdate 
   order by workday asc limit 1 offset greatest(abs(i) - 1,0)
) as a, 
(
   select * from workday_cte where workday < startdate 
   order by workday desc limit 1 offset greatest(abs(i) - 1,0)
) as b
$$ language sql;

您仍然需要将select holiday from holidays部分更新为您自己的假期日期表。

这里也是networkdays()函数的替代品,类似地,您需要更新节假日表-但请注意,如果startdate不在enddate之前,则它返回0,这与excel函数不同

create or replace function networkdays(startdate date, enddate date) returns bigint as
$$
with workday_cte as (
    select s.a::date as workday from 
    generate_series(startdate, enddate, '1 day'::interval) s(a)
    where extract(dow from s.a) between 1 and 5
    except 
    select holiday
    from data.dtdholidays
)
select count(workday_cte.workday) from workday_cte
$$ language sql;

运行此查询以检查输出:

select *, networkdays("wd-1", wd1) from 
(
   select day, workday(day, 1) as wd1, workday(day,0) as wd0, workday(day,-1) as "wd-1" 
   from (select day::date 
         from generate_series('2019-12-16'::date, '2019-12-23'::date, '1 day'::interval
         ) days(day)
) days(day)) a;

给我:

    day     |    wd1     |    wd0     |    wd-1    | networkdays
------------+------------+------------+------------+-------------
 2019-12-16 | 2019-12-17 | 2019-12-16 | 2019-12-13 |           3
 2019-12-17 | 2019-12-18 | 2019-12-17 | 2019-12-16 |           3
 2019-12-18 | 2019-12-19 | 2019-12-18 | 2019-12-17 |           3
 2019-12-19 | 2019-12-20 | 2019-12-19 | 2019-12-18 |           3
 2019-12-20 | 2019-12-23 | 2019-12-20 | 2019-12-19 |           3
 2019-12-21 | 2019-12-23 | 2019-12-21 | 2019-12-20 |           2
 2019-12-22 | 2019-12-23 | 2019-12-22 | 2019-12-20 |           2
 2019-12-23 | 2019-12-24 | 2019-12-23 | 2019-12-20 |           3

备用周末/节假日

在某些情况下,最好将通过添加或减去固定天数而创建的时间段中的工作日数设为常数-例如,在上表中,使用周末日期(12月21日或22日)作为开始日期,则networkdays是2而不是3。您可以更改工作日函数的行为,以便在添加或减去所需工作日之前滚动到下一个工作日,以使networkdays保持恒定。

create or replace function workdaycwd(startdate date, i integer) returns date as 
$$ 
with workday_cte as (
    select s.a::date as workday from 
    generate_series(startdate - ((abs(i) * 2 + 10) || ' day')::interval, 
    startdate + ((abs(i) * 2 + 10) || ' day')::interval, '1 day'::interval) s(a)
    where extract(dow from s.a) between 1 and 5
    except 
    select holiday
    from data.dtdholidays
)
select case when i >= 0 then a.workday else b.workday end from 
(
   select * from workday_cte where workday >= startdate 
   order by workday asc limit 1 offset (abs(i))
) as a, 
(
   select * from workday_cte where workday < startdate 
   order by workday desc limit 1 offset greatest(abs(i) - 1, 0)
) as b
$$ language sql;

运行与上面类似的查询会给出

    day     |    wd1     |    wd0     |    wd-1    | networkdays
------------+------------+------------+------------+-------------
 2019-12-16 | 2019-12-17 | 2019-12-16 | 2019-12-13 |           3
 2019-12-17 | 2019-12-18 | 2019-12-17 | 2019-12-16 |           3
 2019-12-18 | 2019-12-19 | 2019-12-18 | 2019-12-17 |           3
 2019-12-19 | 2019-12-20 | 2019-12-19 | 2019-12-18 |           3
 2019-12-20 | 2019-12-23 | 2019-12-20 | 2019-12-19 |           3
 2019-12-21 | 2019-12-24 | 2019-12-23 | 2019-12-20 |           3
 2019-12-22 | 2019-12-24 | 2019-12-23 | 2019-12-20 |           3
 2019-12-23 | 2019-12-24 | 2019-12-23 | 2019-12-20 |           3

答案 2 :(得分:0)

如果您不想依靠generate_series,这是一个解决方案。这是一个相当简单的算法,可让您通过数组指定自己的工作日定义。默认工作日(mo-fr)的定义如下:array[0,1,1,1,1,1,0]。 由于它不使用generate_series,因此它大概要快得多,尤其是对于较大的间隔。

它不考虑假期表的情况,但是希望它会成为有用的指针。

create or replace function add_business_days(start timestamp with time zone, business_days int[], add_days int)
  returns timestamp with time zone as $$
  declare
    days_left int := add_days;
    num_bds int := (select sum(x.val) from ( select unnest(business_days) val ) as x);
    add_real_days int := 0;
    start_dow int := extract(dow from start);
  begin
    -- if all weekdays are false just ignore the logic below
    if num_bds = 0 then
      return start + make_interval(days := add_days);
    end if;

    -- if the start day is no business day, go forward until we hit one
    while business_days[((start_dow + add_real_days) % 7) + 1] = 0 loop
      add_real_days := add_real_days + 1;
    end loop;

    -- jump to the final week
    add_real_days := add_real_days + (days_left / num_bds)::int * 7;
    days_left := days_left % num_bds;

    -- go ahead with the remaining days until we hit a business day
    while days_left > 0 or business_days[((start_dow + add_real_days) % 7) + 1] = 0 loop
      add_real_days := add_real_days + 1;
      days_left := days_left - business_days[((start_dow + add_real_days) % 7) + 1];
    end loop;
    return start + make_interval(days := add_real_days);
  end;
$$
language plpgsql immutable;

您可以使用以下功能:

select add_business_days(now(), array[0,1,1,1,1,0], 4);