我正在尝试创建一个函数,该函数将返回两个日期之间经过的整个周期数,而不是跨越的边界数。
例如myFunc(小时,' 01:31',' 03:20')= 1(在" 03:31"将成为2)
我遇到了3个问题:
日期计算很复杂,我想保持我的代码整洁,尽可能简单,所以我想使用内置的日期函数,而不是重新发明格里高利历。
DateDiff接受一个datepart参数,该参数抵制了我作为变量传递的所有尝试,除了通过动态sql。
我需要在select语句中调用它,因此它不能成为存储过程,但SQL中的函数拒绝运行动态SQL或存储过程。
(由于我正在运行SSMS 2012,因此我遇到了一些超载DateDiff的问题因此没有DateDiff_BIG)
这是我的代码:
[dbo].[DDif]
(
@Start datetime
,@End datetime
,@Period varchar = null
)
RETURNS bigint
Declare @Dif bigint
if @Period is null set @Period = 'Y'
@p varchar(20) = case
when @Period = 'Y' then 'yy'
when @Period in('S','Q') then 'q'
when @Period = 'M' then 'm'
when @Period in('B','W') then 'ww'
when @Period = 'D' then 'd'
when @Period = 'h' then 'hh'
when @Period = 'm' then 'n'
when @Period = 's' then 's'
End
@sQry varchar(8000) = 'Set @Dif = DateDiff('+@p+',@Start,@End) + case when DateAdd('+@p+',DateDiff('+@p+',@Start,@End),@Start)>@End then -1 else 0 end'
execute @sQry
if @Period in ('S','B') return @Dif/2
return @Dif
我正在寻找3种解决方案中的任何一种:
将dateparts作为变量传递的非动态方式
从函数
用于计算用户指定期间长度的两个日期时间之间的持续时间的不同方法的简洁/简单实现。
注意:如果要删除堆栈溢出的代码中的一些小部分,那么我知道它不会像它出现的那样完全运行。
编辑: 感谢Mark的解决方案。这是我目前的实施:
FUNCTION [dbo].[DDif]
(
@Start datetime
,@End datetime
,@Period varchar = null
)
RETURNS bigint
AS
BEGIN
if @Period is null set @Period = 'Y'
declare @m int = DATEDIFF(Month,@Start,@End) + case when DATEADD(MONTH,DATEDIFF(Month,@Start,@End),@Start)>@End then -1 else 0 END
declare @s bigint = 86400*DATEDIFF(DAY, CAST(@Start as date),CAST(@End as date))+datediff(s,cast(@Start as time),cast(@End as time))
declare @num bigint = case
when @Period = 'Y' then 12
when @Period = 'S' then 6
when @Period = 'Q' then 3
when @Period = 'M' then 1
when @Period = 'B' then 1209600
when @Period = 'W' then 604800
when @Period = 'D' then 86400
when @Period = 'h' then 3600
when @Period = 'm' then 60
when @Period = 's' then 1
end
return case
when @Period in ('Y','S','Q','M') then @m
when @Period in ('B','W','D','h','m','s') then @s
end / @num
答案 0 :(得分:4)
您可以使用与时间戳中最小有意义精度对应的日期部分调用DateDiff
,然后除以以正确单位获取间隔。
例如,如果您将时间存储到第二个,要将小时数DateDiff
调用ss
,将日期部分设为date_diff
,然后再除以3600.想法是,如果您的时间只有精确到秒,然后“交叉1秒边界”意味着与所有可测量目的的“花费1秒钟”相同,然后你只需转换为你真正需要的单位。
如果你的间隔很长而你的时间戳是高精度的 - 例如如果你需要使用微秒精度一年的间隔 - 你可能不得不调用DateDiff_Big。
<强>更新强>
如果您需要更长的时间间隔的更高精度并且不能使用DateDiff_Big,那么您必须变得有点棘手。一种方法是:
首先将日期与时间分开。
然后date_diff
日期部分,以天为单位。 (这有几百万年的范围。)将结果乘以86,400,000(结果将需要64位存储。)
现在date_diff
ms精度的时间。
注意@import url('https://fonts.googleapis.com/css?family=Roboto');
进行了签名比较,因此后者差异可能是正面的,也可能是负面的。通过乘以日期部分的差异将其添加到您获得的部分,现在您可以准确计算ms。
除以得到你想要的单位。
更新2 - 我原先声称纳秒会起作用,但那是因为我无法正确移动小数点。
答案 1 :(得分:0)
试试这个,
你可以添加你需要的所有DATEPARTS。
它也可以作为独立查询工作,只有当你发现它有用时才真正需要功能。
如果您不需要纳秒精度,则可以使用datetime数据类型而不是datetime2,它可以更容易处理。
编辑:更正了边界行为
UPDATE2:大量语法简化
UPDATE3:恢复原始语法以避免闰年和不同月份的长度问题
DROP FUNCTION FN_CALC_DATE_DIFF;
GO
CREATE FUNCTION FN_CALC_DATE_DIFF(
@INTERVALTYPE AS INT = NULL,
@START AS datetime,
@END AS datetime
)
RETURNS BIGINT
AS
BEGIN
RETURN (SELECT CASE @INTERVALTYPE
WHEN 0 THEN DATEDIFF(NS, DATEADD(NS, DATEDIFF(NS, 0, @START), 0), DATEADD(NS, DATEDIFF(NS, 0, @START), 0) + @Start-@End)
WHEN 1 THEN DATEDIFF(MCS, DATEADD(MCS, DATEDIFF(MCS, 0, @START), 0), DATEADD(MCS, DATEDIFF(MCS, 0, @START), 0) + @END-@START)
WHEN 2 THEN DATEDIFF(MS, DATEADD(MS, DATEDIFF(MS, 0, @START), 0), DATEADD(MS, DATEDIFF(MS, 0, @START), 0) + @END-@START)
WHEN 3 THEN DATEDIFF(SS, DATEADD(SS, DATEDIFF(SS, 0, @START), 0), DATEADD(SS, DATEDIFF(SS, 0, @START), 0) + @END-@START)
WHEN 4 THEN DATEDIFF(MI, DATEADD(MI, DATEDIFF(MI, 0, @START), 0), DATEADD(MI, DATEDIFF(MI, 0, @START), 0) + @END-@START)
WHEN 5 THEN DATEDIFF(HH, DATEADD(HH, DATEDIFF(HH, 0, @START), 0), DATEADD(HH, DATEDIFF(HH, 0, @START), 0) + @END-@START)
WHEN 6 THEN DATEDIFF(DD, DATEADD(DD, DATEDIFF(DD, 0, @START), 0), DATEADD(DD, DATEDIFF(DD, 0, @START), 0) + @END-@START)
WHEN 7 THEN DATEDIFF(WK, DATEADD(WK, DATEDIFF(WK, 0, @START), 0), DATEADD(WK, DATEDIFF(WK, 0, @START), 0) + @END-@START)
WHEN 8 THEN DATEDIFF(MM, DATEADD(MM, DATEDIFF(MM, 0, @START), 0), DATEADD(MM, DATEDIFF(MM, 0, @START), 0) + @END-@START)
WHEN 9 THEN DATEDIFF(YY, DATEADD(YY, DATEDIFF(YY, 0, @START), 0), DATEADD(YY, DATEDIFF(YY, 0, @START), 0) + @END-@START)
ELSE
DATEDIFF(SS, DATEADD(SS, DATEDIFF(SS, 0, @START), 0), DATEADD(SS, DATEDIFF(SS, 0, @START), 0) + @END-@START) -- default to
END )
END
GO
这个测试
declare @d1 datetime = '31/12/2016'
declare @d2 datetime = '01/01/2017'
declare @h1 datetime = '01:31'
declare @h2 datetime = '03:20'
select
DBO.FN_CALC_DATE_DIFF(5, @h1, @h2) DIFF_HOURS,
DBO.FN_CALC_DATE_DIFF(9, @d1, @d2) DIFF_YEARS
将产生
DIFF_HOURS DIFF_YEARS
1 0
答案 2 :(得分:0)
考虑到所有的答案和评论,我终于找到了解决这个问题的方法。
我已经实现了一个函数来处理DATETIME2
数据类型的基本加/减,然后我在函数中使用它来获取两个日期之间的间隔。
所有计算的最大精确度均为DATETIME2
和BIGINT
,并应处理所有情况。
这是实现基本DATETIME2
算术的函数:
DROP FUNCTION FN_DATE2_MATH;
GO
CREATE FUNCTION FN_DATE2_MATH(
@D1 AS DATETIME2(7),
@D2 AS DATETIME2(7),
@OP AS INT=1, -- 1 = SUM, -1 = SUBTRACT
@OVERFLOW AS INT = NULL -- NULL = NULL VALUE, ELSE OVERFLOW ERROR
)
RETURNS DATETIME2(7)
AS
BEGIN
IF (@OP = -1) AND (@D1>@D2) BEGIN
DECLARE @DT DATETIME2(7) = @D1
SET @D1 = @D2
SET @D2 = @DT
END
DECLARE @B1 VARBINARY(8) = CONVERT(VARBINARY(8), REVERSE(SUBSTRING(CONVERT(VARBINARY(9), @D1),2,8)))
DECLARE @DD1 VARBINARY(8) = SUBSTRING(CONVERT(VARBINARY(8), @B1),1,3)
DECLARE @NS1 VARBINARY(8) = SUBSTRING(CONVERT(VARBINARY(8), @B1),4,5)
DECLARE @B2 VARBINARY(8) = CONVERT(VARBINARY(8), REVERSE(SUBSTRING(CONVERT(VARBINARY(9), @D2),2,8)))
DECLARE @DD2 VARBINARY(8) = SUBSTRING(CONVERT(VARBINARY(8), @B2),1,3)
DECLARE @NS2 VARBINARY(8) = SUBSTRING(CONVERT(VARBINARY(8), @B2),4,5)
DECLARE @DDR AS BIGINT
DECLARE @NSR AS BIGINT
IF @OP = 1 BEGIN
SET @NSR = CONVERT(BIGINT, @NS2) + CONVERT(BIGINT, @NS1)
IF @NSR>=864000000000 BEGIN
SET @NSR = @NSR - CONVERT(BIGINT, 864000000000)
SET @DD1 = CONVERT(VARBINARY(8), CONVERT(BIGINT, @DD1)-1)
END
SET @DDR = CONVERT(BIGINT, @DD2) + CONVERT(BIGINT, @DD1)
END ELSE
IF @OP = -1 BEGIN
SET @NSR = CONVERT(BIGINT, @NS2) - CONVERT(BIGINT, @NS1)
IF @NSR<0 BEGIN
SET @NSR = @NSR + CONVERT(BIGINT, 864000000000)
SET @DD1 = CONVERT(VARBINARY(8), CONVERT(BIGINT, @DD1)+1)
END
SET @DDR = CONVERT(BIGINT, @DD2) - CONVERT(BIGINT, @DD1)
END
-- CHECK OVERFLOW
IF @DDR NOT BETWEEN 0 AND 3652058 BEGIN
IF @OVERFLOW IS NULL
RETURN NULL
ELSE
RETURN DATEADD(DD, -1, CONVERT(DATETIME2(7), 0x070000000000000000)) -- GENERATE OVERFLOW
END
DECLARE @BR VARBINARY(8) = CONVERT(VARBINARY(3), @DDR)+CONVERT(VARBINARY(5), @NSR)
SET @BR = CONVERT(VARBINARY(8), REVERSE(@BR))
RETURN CONVERT(DATETIME2(7), 0x07+@BR)
END
GO
这是让时间段过去的功能:
DROP FUNCTION FN_DATE_DIFF2;
GO
CREATE FUNCTION FN_DATE_DIFF2(
@INTERVALTYPE AS VARCHAR(11),
@START AS DATETIME2(7),
@END AS DATETIME2(7)
)
RETURNS BIGINT
AS
BEGIN
DECLARE @DATEPART INT = CASE
WHEN @INTERVALTYPE IN ('0','nanosecond','ns') THEN 0
WHEN @INTERVALTYPE IN ('1','microsecond','mcs') THEN 1
WHEN @INTERVALTYPE IN ('2','millisecond','ms') THEN 2
WHEN @INTERVALTYPE IN ('3','second','ss','s') THEN 3
WHEN @INTERVALTYPE IN ('4','minute','mi','n') THEN 4
WHEN @INTERVALTYPE IN ('5','hour','hh') THEN 5
WHEN @INTERVALTYPE IN ('6','day','dd','d') THEN 6
WHEN @INTERVALTYPE IN ('7','week','wk','ww') THEN 7
WHEN @INTERVALTYPE IN ('8','month','mm','m') THEN 8
WHEN @INTERVALTYPE IN ('9','quarter','qq','q') THEN 9
WHEN @INTERVALTYPE IN ('10','year','yy','yyyy') THEN 10
ELSE
6 -- DEFAULT TO DAYS
END
DECLARE @BN0 VARBINARY(8) = 0x0000000000000000 -- 0001-01-01 00:00:00.0000000
DECLARE @DT0 AS DATETIME2(7) = CONVERT(DATETIME2(7), 0x07+@BN0) -- datetime2(7) = 0
--DECLARE @BNX VARBINARY(8) = 0xFFBF692AC9DAB937 -- 9999-12-31 23:59:59.9999999
--DECLARE @DTX AS DATETIME2(7) = CONVERT(DATETIME2(7), 0x07+@BNX) -- datetime2(7) = 0
DECLARE @DT1 AS DATETIME2(7)
DECLARE @DT2 AS DATETIME2(7)
DECLARE @DP AS DATETIME2(7)
DECLARE @VB1 VARBINARY(8) = CONVERT(VARBINARY(8), REVERSE(SUBSTRING(CONVERT(VARBINARY(9), @START),2,8)))
DECLARE @DD1 VARBINARY(8) = SUBSTRING(CONVERT(VARBINARY(8), @VB1),1,3) -- DAYS FROM 0 TO START
DECLARE @NS1 VARBINARY(8) = SUBSTRING(CONVERT(VARBINARY(8), @VB1),4,5) -- NS FROM 0 TO START
DECLARE @VB2 VARBINARY(8) = CONVERT(VARBINARY(8), REVERSE(SUBSTRING(CONVERT(VARBINARY(9), @END),2,8)))
DECLARE @DD2 VARBINARY(8) = SUBSTRING(CONVERT(VARBINARY(8), @VB2),1,3) -- DAYS FROM 0 TO END
DECLARE @NS2 VARBINARY(8) = SUBSTRING(CONVERT(VARBINARY(8), @VB2),4,5) -- NS FROM 0 TO END
DECLARE @NSR AS BIGINT = CONVERT(BIGINT, @NS2) - CONVERT(BIGINT, @NS1) -- NS RESULT NOT BIASED
IF @NSR<0 BEGIN
SET @NSR = @NSR + CONVERT(BIGINT, 864000000000) -- NS RESULT
SET @DD1 = CONVERT(VARBINARY(8), CONVERT(BIGINT, @DD1)+1) -- ADD CARRY
END
DECLARE @DDR AS BIGINT = CONVERT(BIGINT, @DD2) - CONVERT(BIGINT, @DD1) -- DAYS RESULT
DECLARE @RES BIGINT
SET @RES = CASE @DATEPART
WHEN 0 THEN @DDR*CONVERT(BIGINT, 864000000000)+ @NSR -- NS
WHEN 1 THEN @DDR*CONVERT(BIGINT, 86400000000) + @NSR/CONVERT(BIGINT, 10) -- MCS
WHEN 2 THEN @DDR*CONVERT(BIGINT, 86400000) + @NSR/CONVERT(BIGINT, 10000) -- MS
WHEN 3 THEN @DDR*CONVERT(BIGINT, 86400) + @NSR/CONVERT(BIGINT, 10000000) -- SS
WHEN 4 THEN @DDR*CONVERT(BIGINT, 1440) + @NSR/CONVERT(BIGINT, 600000000) -- MI
WHEN 5 THEN @DDR*CONVERT(BIGINT, 24) + @NSR/CONVERT(BIGINT, 36000000000) -- HH
WHEN 6 THEN @DDR -- DD
WHEN 7 THEN @DDR / 7 -- WK (BOTH INT, RES = INT)
END
IF @DATEPART IN (8,9,10) BEGIN
SET @DT1 = CASE @DATEPART
WHEN 8 THEN DATEADD(MM, DATEDIFF(MM, @DT0, @START), @DT0)
WHEN 9 THEN DATEADD(QQ, DATEDIFF(QQ, @DT0, @START), @DT0)
WHEN 10 THEN DATEADD(YY, DATEDIFF(YY, @DT0, @START), @DT0)
END
SET @DP = DBO.FN_DATE2_MATH(@START, @END, -1, 0) -- ELAPSED TIME (DIFF)
SET @DT2 = DBO.FN_DATE2_MATH(@DT1, @DP, 1, 0) -- SHIFT DATE (ADD)
SET @RES = CASE @DATEPART
WHEN 8 THEN DATEDIFF(MM, @DT1, @DT2)
WHEN 9 THEN DATEDIFF(QQ, @DT1, @DT2)
WHEN 10 THEN DATEDIFF(YY, @DT1, @DT2)
END
END
RETURN @RES
END
GO
您可以这样称呼它:
DECLARE @D1 DATETIME2(7)
DECLARE @D2 DATETIME2(7)
DECLARE @DP VARCHAR(20)
SET @D1 = '31/12/2016'
SET @D2 = '01/01/2017'
SET @DP = 'YY'
SELECT @D1 DATE_START, @D2 DATE_END, @DP DATE_PART, DBO.FN_DATE_DIFF2(@DP, @D1, @D2) INTERVALS
SET @D1 = '01:31'
SET @D2 = '03:20'
SET @DP = 'HH'
SELECT @D1 DATE_START, @D2 DATE_END, @DP DATE_PART, DBO.FN_DATE_DIFF2(@DP, @D1, @D2) INTERVALS
SET @D1 = '01/01/0001'
SET @D2 = '31/12/9999 23:59:59.9999999'
SET @DP = 'NS'
SELECT @D1 DATE_START, @D2 DATE_END, @DP DATE_PART, DBO.FN_DATE_DIFF2(@DP, @D1, @D2) INTERVALS
你会得到:
DATE_START DATE_END DATE_PART INTERVALS
2016-12-31 00:00:00.0000000 2017-01-01 00:00:00.0000000 YY 0
1900-01-01 01:31:00.0000000 1900-01-01 03:20:00.0000000 HH 1
0001-01-01 00:00:00.0000000 9999-12-31 23:59:59.9999999 NS 3155378975999999999
这应该有效