在Opengauss/MogDB中实现months_between函数

2023年 9月 25日 34.9k 0

Oracle有个函数,months_between, Opengauss中并没有实现。

Oracle实现逻辑

研究一下, Oracle的实现逻辑如下:
1,提取两个时间的年/月/日数值,年差值*12 + 月差值作为结果的基础部分。

2,如果正好两个日期都是月末的最后一天,会直接返回上一步的结果。而如果不是,则返回年差值*12 + 月差值+日差值/31,同时,需要注意的是,日差值部分,包括了时分秒。

在这种特殊的月末的规则下,会出现一些比较奇怪的的现象,特别是在2月月底的时候。
举几个例子体会一下:

  • months_between(‘20210301’,‘20210228’)返回的值不是大家想象中的1/31, 而是4/31.
  • months_between(‘20210331’,‘20210228’) 和months_between(‘20210331’,‘20210228’)都返回1
  • 但months_between(‘20210330’,‘20210228’)反而会返回1+2/31, months_between(‘20210329’,‘20210228’)返回1+1/31.
  • months_between(‘20210531’,‘20210430’)
    和 months_between(‘20210530’,‘20210430’) 都返回1.

Opengauss(MogDB)实现

知道了规则后,可以通过在Opengauss(MogDB)上创建自定义函数实现兼容。

提取年月日

首先,通过date_part函数(或者兼容Oracle的extract函数)取出年月日。下面t1代表入参的时间参数。

y1 := date_part('year',t1);
m1 := date_part('month',t1);
d1 := date_part('day',t1);

或者

y1 := extract(year from t1);
m1 := extract(month from t1);
d1 := extract(day from t1);

值得注意的是,由于Oracle函数的返回值里面,是包含了时分秒的时分秒的,因此,这里取出来的d1/d2不能直接用结果的返回。需要提取的是包含时分秒信息的日数值,可以通过入参减去月初来实现。

(t1-to_date(to_char(t1,'yyyymm'),'yyyymm'))

很遗憾的是,Opengauss没有实现trunc(时间列)的功能,否则,下面会是更简便的写法。

t1-trunc(t1,'MM')

月末判断

这里面有个比较麻烦的是,是闰年的2月,需要特殊判断。

不涉及闰年(2月)的月末判断

最简单的写法,是拼接年和月,然后判断是否在1/31、3/31、4/30 … 12/31里面

m1||d1 in ('131','331','430','531','630','731','831','930','1031','1130','1231')

这个代码看起来似乎会有那么一丁点的问题,比如说,你也许会想,1月11号和 11月1号拼出来不就一样了吗?但其实没关系,因为1月份的日期,我们只关心1/31, 并不会造成混淆。
当然,如果你很严谨,也可以这样来判断

m1*100 + d1 in (131,331,430,531,630,731,831,930,1031,1130,1231)

2月的月末判断

2月判断拗口一点,需要了解闰年规则,闰年规则如下:
年能被4整除,不能被100整除,但被400整除又可以。
说起来很拗口,但其实也不复杂

(mod(y1,4)=0 and mod(y1,100)!=0 or mod(y1,400)=0)

由于这个是和判断月末一起的,所以,可以连2月是否28日/29日一起判断。

/*leap year*/
( m1 = 2 and d1=29 and (mod(y1,4)=0 and mod(y1,100)!=0 or mod(y1,400)=0) )
or /*non-leap year*/
( m1 = 2 and d1=28 and not (mod(y1,4)=0 and mod(y1,100)!=0 or mod(y1,400)=0) )

结合2月和非2月的完整判断

(m1||d1 in ('131','331','430','531','630','731','831','930','1031','1130','1231')
or /*leap year*/
( m1 = 2 and d1=29 and (mod(y1,4)=0 and mod(y1,100)!=0 or mod(y1,400)=0) )
or /*non-leap year*/
( m1 = 2 and d1=28 and not (mod(y1,4)=0 and mod(y1,100)!=0 or mod(y1,400)=0) )
)
and
(m2||d2 in ('131','331','430','531','630','731','831','930','1031','1130','1231')
or /*leap year*/
( m2 = 2 and d2=29 and (mod(y2,4)=0 and mod(y2,100)!=0 or mod(y2,400)=0) )
or /*non-leap year*/
( m2 = 2 and d2=28 and not (mod(y2,4)=0 and mod(y2,100)!=0 or mod(y2,400)=0) )
)

Opengauss(MogDB)完整实现

结合前面的逻辑,可以写出最终的代码

create or replace function months_between(t1 date ,t2 date)
returns number
-- months_between implementation in MogDB, by LuoHaixiong@Enmotech
as $$
declare
y1 int;
y2 int;
m1 int;
m2 int;
d1 int;
d2 int;
begin
y1 := date_part('year', t1);
y2 := date_part('year', t2);
m1 := date_part('month', t1);
m2 := date_part('month', t2);
d1 := date_part('day', t1);
d2 := date_part('day', t2);
if --Both dates are end of month
( (m1||d1 in ('131','331','430','531','630','731','831','930','1031','1130','1231')
or /*leap year*/
( m1 = 2 and d1=29 and (mod(y1,4)=0 and mod(y1,100)!=0 or mod(y1,400)=0) )
or /*non-leap year*/
( m1 = 2 and d1=28 and not (mod(y1,4)=0 and mod(y1,100)!=0 or mod(y1,400)=0) )
)
and
(m2||d2 in ('131','331','430','531','630','731','831','930','1031','1130','1231')
or /*leap year*/
( m2 = 2 and d2=29 and (mod(y2,4)=0 and mod(y2,100)!=0 or mod(y2,400)=0) )
or /*non-leap year*/
( m2 = 2 and d2=28 and not (mod(y2,4)=0 and mod(y2,100)!=0 or mod(y2,400)=0) )
)
) then
return (y1-y2)*12+(m1-m2);
else --Normal days
return (y1-y2)*12+(m1-m2)
+ (
(t1-to_date(to_char(t1,'yyyymm'),'yyyymm'))
- (t2-to_date(to_char(t2,'yyyymm'),'yyyymm'))
)/31;
end if;
end;
$$
LANGUAGE plpgsql;

注:本功能未经严格测试,仅限内部使用或者外部POC评测使用。

相关文章

Oracle如何使用授予和撤销权限的语法和示例
Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
社区版oceanbase安装
Oracle 导出CSV工具-sqluldr2
ETL数据集成丨快速将MySQL数据迁移至Doris数据库

发布评论