用多种SQL方式实现一个万年历(含年、月、周日...周六、当月天数字段)

Normal 0 7.8 磅 0 2 false false false MicrosoftInternetExplorer4 用SQL语句实现万年历

先上一个例子:

年

月

星期日

星期一

星期二

星期三

星期四

星期五

星期六

本月最后一日

2010

06

 

 

       1

       2

       3

       4

       5

30

2010

06

       6

       7

       8

       9

      10

      11

      12

30

2010

06

      13

      14

      15

      16

      17

      18

      19

30

2010

06

      20

      21

      22

      23

      24

      25

      26

30

2010

06

      27

      28

      29

      30

 

 

 

30

 

思路

首先构建年和月,每个年月对应至少6行(用于显示1-31号的日期,每行7天,所以最少需要ceil((31+6)/7)=6),然后按照每周7天分别列出7个字段。由于7个字段会横向递增,所以需要通过 (rownum - 1)×7来实现跨行顺序递增。但是又需要跨月,和跨年,所以需要借助分析函数row_number(row_number按照年和月进行分组,后面简称为rn)。

以上只是初步思路,进一步实验发现由于每月的月初并不是在周一(呵呵,是的,后来才发现),所以不能直接从周一就开始迭代。于是想了个办法,将每月第一天转为星期几,一旦rn大于等于星期几减1(实际通过to_char获取的星期几会大一天)便开始显示日期。

以上方式解决了开始日期的问题,但是又出现了结束日期的问题。由于没有对结束日期进行约束,所以迭代一直会到每行结束为止,这样就出现了大于最大日期的情况 (出现32、33这样的日期)。于是又用与处理开始日期同样的方式解决结束日期的问题,不过首先得取到该月的最后一日,才能控制结束日期的正确性。

以下是几个不同版本的实现方式。

下面这个脚本无需任何初始化便可以执行,而且只需修改第一个虚拟表的开始日期即可。而且这个脚本还有一个特点就是能够对当天的日期进行着重显示(用【】引起来):

with initday as (select date '1980-2-28' bsday, 6 lx from dual),
     strcday as (select to_char(add_months(bsday, ceil(rownum/lx) - 1), 'yyyy') year,
                        to_char(add_months(bsday, ceil(rownum/lx) - 1), 'mm') month,
                        to_number(to_char(trunc(add_months(bsday, ceil(rownum/lx) - 1), 'mm'), 'd')) kv,
                        to_number(to_char(last_day(add_months(bsday, ceil(rownum/lx) - 1)), 'dd')) mxdays
                   from initday
                 connect by rownum <= 10000)
select year
年, month 月,
       case when rn >= kv - 1 and rn - kv + 2 <= mxdays then
            case when year||month||lpad(rn - kv + 2, 2, '0') = to_char(sysdate, 'yyyymmdd')
            then '
【' || (rn - kv + 2) || '】' else to_char(rn - kv + 2) end end 星期日,
       case when rn >= kv - 2 and rn - kv + 3 <= mxdays then
            case when year||month||lpad(rn - kv + 3, 2, '0') = to_char(sysdate, 'yyyymmdd')
            then '
【' || (rn - kv + 3) || '】' else to_char(rn - kv + 3) end end 星期一,
       case when rn >= kv - 3 and rn - kv + 4 <= mxdays then
            case when year||month||lpad(rn - kv + 4, 2, '0') = to_char(sysdate, 'yyyymmdd')
            then '
【' || (rn - kv + 4) || '】' else to_char(rn - kv + 4) end end 星期二,
       case when rn >= kv - 4 and rn - kv + 5 <= mxdays then
            case when year||month||lpad(rn - kv + 5, 2, '0') = to_char(sysdate, 'yyyymmdd')
            then '
【' || (rn - kv + 5) || '】' else to_char(rn - kv + 5) end end 星期三,
       case when rn >= kv - 5 and rn - kv + 6 <= mxdays then
            case when year||month||lpad(rn - kv + 6, 2, '0') = to_char(sysdate, 'yyyymmdd')
            then '
【' || (rn - kv + 6) || '】' else to_char(rn - kv + 6) end end 星期四,
       case when rn >= kv - 6 and rn - kv + 7 <= mxdays then
            case when year||month||lpad(rn - kv + 7, 2, '0') = to_char(sysdate, 'yyyymmdd')
            then '
【' || (rn - kv + 7) || '】' else to_char(rn - kv + 7) end end 星期五,
       case when rn >= kv - 7 and rn - kv + 8 <= mxdays then
            case when year||month||lpad(rn - kv + 8, 2, '0') = to_char(sysdate, 'yyyymmdd')
            then '
【' || (rn - kv + 8) || '】' else to_char(rn - kv + 8) end end 星期六,
       mxdays
最大天数
  from (select a.*, (row_number()over(partition by a.year, a.month order by rownum) - 1)*7 rn
          from strcday a)  b
 where rn - kv + 2 <= mxdays --
过滤空行

 

增加函数mark版本,需要增加mark函数,使SQL脚本的结构看起来相对简单些:

with initday as (select date '2010-2-1' bsday, 6 lx from dual),
     strcday as (select to_char(add_months(bsday, ceil(rownum/lx) - 1), 'yyyy') year,
                        to_char(add_months(bsday, ceil(rownum/lx) - 1), 'mm') month,
                        to_number(to_char(trunc(add_months(bsday, ceil(rownum/lx) - 1), 'mm'), 'd')) kv,
                        to_number(to_char(last_day(add_months(bsday, ceil(rownum/lx) - 1)), 'dd')) mxdays
                   from initday
                 connect by rownum <= 10000)
select year
年, month 月,
       case when rn >= kv - 1 and rn - kv + 2 <= mxdays then
            mark(year, month, rn - kv + 2, to_char(sysdate, 'yyyymmdd')) end
星期日,
       case when rn >= kv - 2 and rn - kv + 3 <= mxdays then
            mark(year, month, rn - kv + 3, to_char(sysdate, 'yyyymmdd')) end
星期一,
       case when rn >= kv - 3 and rn - kv + 4 <= mxdays then
            mark(year, month, rn - kv + 4, to_char(sysdate, 'yyyymmdd')) end
星期二,
       case when rn >= kv - 4 and rn - kv + 5 <= mxdays then
            mark(year, month, rn - kv + 5, to_char(sysdate, 'yyyymmdd')) end
星期三,
       case when rn >= kv - 5 and rn - kv + 6 <= mxdays then
            mark(year, month, rn - kv + 6, to_char(sysdate, 'yyyymmdd')) end
星期四,
       case when rn >= kv - 6 and rn - kv + 7 <= mxdays then
            mark(year, month, rn - kv + 7, to_char(sysdate, 'yyyymmdd')) end
星期五,
       case when rn >= kv - 7 and rn - kv + 8 <= mxdays then
            mark(year, month, rn - kv + 8, to_char(sysdate, 'yyyymmdd')) end
星期六,
       mxdays
最大天数
  from (select a.*, (row_number()over(partition by a.year, a.month order by rownum) - 1)*7 rn
          from strcday a)  b
 where rn - kv + 2 <= mxdays --
过滤空行

 

简化版本(无当日标记)

with initday as (select date '1980-2-28' bsday, 6 lx from dual),
     strcday as (select to_char(add_months(bsday, ceil(rownum/lx) - 1), 'yyyy') year,
                        to_char(add_months(bsday, ceil(rownum/lx) - 1), 'mm') month,
                        to_number(to_char(trunc(add_months(bsday, ceil(rownum/lx) - 1), 'mm'), 'd')) kv,
                        to_number(to_char(last_day(add_months(bsday, ceil(rownum/lx) - 1)), 'dd')) mxdays
                   from initday
                 connect by rownum <= 10000)
select year
年, month 月,
       case when rn >= kv - 1 and rn - kv + 2 <= mxdays then rn - kv + 2 end
星期日,
       case when rn >= kv - 2 and rn - kv + 3 <= mxdays then rn - kv + 3 end
星期一,
       case when rn >= kv - 3 and rn - kv + 4 <= mxdays then rn - kv + 4 end
星期二,
       case when rn >= kv - 4 and rn - kv + 5 <= mxdays then rn - kv + 5 end
星期三,
       case when rn >= kv - 5 and rn - kv + 6 <= mxdays then rn - kv + 6 end
星期四,
       case when rn >= kv - 6 and rn - kv + 7 <= mxdays then rn - kv + 7 end
星期五,
       case when rn >= kv - 7 and rn - kv + 8 <= mxdays then rn - kv + 8 end
星期六,
       mxdays
最大天数
  from (select a.*, (row_number()over(partition by a.year, a.month order by rownum) - 1)*7 rn
          from strcday a)  b
 where rn - kv + 2 <= mxdays --
过滤空行

 

 

函数版本(可以查看指定月份)

这个脚本可以指定任意一个月的日历进行显示,但是需要依赖不少对象(具体内容已在下面列出):

SQL> select * from table(show_calendar(‘201005’));

 

年

月

星期日

星期一

星期二

星期三

星期四

星期五

星期六

本月最后一日

2010

05

 

 

 

 

 

 

       1

31

2010

05

       2

       3

       4

       5

       6

       7

       8

31

2010

05

       9

      10

      11

      12

      13

      14

      15

31

2010

05

      16

      17

      18

      19

      20

      21

      22

31

2010

05

      23

      24

      25

      26

      27

      28

      29

31

2010

05

      30

      31

 

 

 

 

 

31

 

 

默认显示的是当前月的日历,下面是今天(2010-02-05)的执行结果,其中5号进行了着重标记,表示是今天:

SQL> select * from table(show_calendar);

 

年

月

星期日

星期一

星期二

星期三

星期四

星期五

星期六

本月最后一日

2010

06

 

 

       1

       2

       3

       4

       5

30

2010

06

       6

       7

       8

       9

      10

      11

      12

30

2010

06

      13

      14

      15

      16

      17

      18

      19

30

2010

06

      20

      21

      22

      23

      24

      25

      26

30

2010

06

      27

      28

      29

      30

 

 

 

30

 

下面创建该函数所需要以来的类型

首先增加一个对象类型:

create or replace type typ_calendar as object(

    年 varchar2(8),

    月 varchar2(8),

    星期日 varchar2(8),

    星期一 varchar2(8),

    星期二 varchar2(8),

    星期三 varchar2(8),

    星期四 varchar2(8),

    星期五 varchar2(8),

    星期六 varchar2(8),

    本月最后一日 varchar2(2),

   

    member function format(

        curday date        := sysdate,

        fmtlen pls_integer := 8

    )return typ_calendar

)

创建该类型的类型体:

create or replace type body typ_calendar as

    member function format(

        curday date        := sysdate,

        fmtlen pls_integer := 8

    ) return typ_calendar as

        v_return   typ_calendar := typ_calendar('','','','','','','','','','');

        v_dd       varchar2(2)  := to_char(curday, 'dd');

       

        function fmt(

            fmtstr varchar2

        )return varchar2 as

        begin

            return lpad(fmtstr, fmtlen, ' ');

        end fmt;

    begin

        v_return.年 := 年;

        v_return.月 := 月;

        v_return.星期日 := fmt(星期日);

        v_return.星期一 := fmt(星期一);

        v_return.星期二 := fmt(星期二);

        v_return.星期三 := fmt(星期三);

        v_return.星期四 := fmt(星期四);

        v_return.星期五 := fmt(星期五);

        v_return.星期六 := fmt(星期六);

        v_return.本月最后一日 := 本月最后一日;

 

        if (年 || lpad(月, 2, '0') = to_char(curday, 'yyyymm')) then

            case v_dd

            when 星期日 then

                v_return.星期日 := fmt('【' || 星期日 || '】');

            when 星期一 then

                v_return.星期一 := fmt('【' || 星期一 || '】');

            when 星期二 then

                v_return.星期二 := fmt('【' || 星期二 || '】');

            when 星期三 then

                v_return.星期三 := fmt('【' || 星期三 || '】');

            when 星期四 then

                v_return.星期四 := fmt('【' || 星期四 || '】');

            when 星期五 then

                v_return.星期五 := fmt('【' || 星期五 || '】');

            when 星期六 then

                v_return.星期六 := fmt('【' || 星期六 || '】');

            else null;

            end case;

        end if;

       

        return v_return;

    end format;

end;

这个成员函数用于格式化输出的数据,加着重标识以及输出格式。

 

创建该对象类型的数组类型:

create or replace type tbl_calendar as table of typ_calendar

 

创建显示日历的函数show_calendar,默认日期为当前月:

create or replace function show_calendar(

    v_yermonth varchar2  := to_char(sysdate, 'yyyymm'))

return tbl_calendar as

    v_cal tbl_calendar;

    v_seg pls_integer := 6;

    v_len pls_integer := 8;

    v_yer varchar2(4) := substr(v_yermonth, 1, 4);

    v_mon varchar2(2) := lpad(substr(v_yermonth, 5, 2), 2, '0');

    v_ini date := to_date(v_yermonth || '01', 'yyyymmdd');

begin

    select typ_calendar(v_yer, v_mon,

           case when rn >= wkn - 1 and rn - wkn + 2 <= mxdays

           then  rn - wkn + 2 end,

           case when rn >= wkn - 2 and rn - wkn + 3 <= mxdays

           then  rn - wkn + 3 end,

           case when rn >= wkn - 3 and rn - wkn + 4 <= mxdays

           then  rn - wkn + 4 end,

           case when rn >= wkn - 4 and rn - wkn + 5 <= mxdays

           then  rn - wkn + 5 end,

           case when rn >= wkn - 5 and rn - wkn + 6 <= mxdays

           then  rn - wkn + 6 end,

           case when rn >= wkn - 6 and rn - wkn + 7 <= mxdays

           then  rn - wkn + 7 end,

           case when rn >= wkn - 7 and rn - wkn + 8 <= mxdays

           then  rn - wkn + v_len end,

           mxdays).format()

      bulk collect into v_cal

      from (select (rownum - 1)*7 rn,

                   to_number(to_char(trunc(v_ini, 'mm'), 'd')) wkn,

                   to_number(to_char(last_day(v_ini), 'dd')) mxdays

              from dual

            connect by rownum <= v_seg) b

     where rn - wkn + 2 <= mxdays; --过滤空行

    return v_cal;

end show_calendar;

 

 

    需要注意的是,在函数中调用外部定义的对象类型的时候,对使用的数组变量v_cal进行bulk collect into,要将所有字段用obj_calender对象类型进行包装,然后再bulk collect into。否则会报:

--ora-00947
--PL/SQL: SQL Statement ignored
--PL/SQL: ORA-00947: not enough values
这样的错误。这个地方与本地定义的类型有很明显的不同,本地定义的类型只要列出字段,直接bulk collect into就能正常实现数据载入。这个地方需要注意。

 

 

 

 

请使用浏览器的分享功能分享到微信等