我需要将在varchar
字段中具有ISO 8601持续时间的字段转换为表示以小时为单位的持续时间的十进制值。
我如何使用以下数据进行SELECT,因此结果返回时,持续时间字段的行值为8.0(PT8H0M
),7.5(PT7H30M
)和1.0(PT1H0M
)?
CREATE TABLE [dbo].[timetracking](
[qbsql_id] [int] IDENTITY(1,1) NOT NULL,
[username_id] [int] NULL,
[TxnDate] [datetime2](0) NULL,
[Duration] [varchar](50) NULL,
PRIMARY KEY CLUSTERED ([qbsql_id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
SET IDENTITY_INSERT [dbo].[timetracking] ON
GO
INSERT [dbo].[timetracking] ([qbsql_id], [username_id], [TxnDate], [Duration]) VALUES (1, 1, CAST(N'2018-02-02T00:00:00.0000000' AS DateTime2), N'PT8H0M')
INSERT [dbo].[timetracking] ([qbsql_id], [username_id], [TxnDate], [Duration]) VALUES (2, 2, CAST(N'2018-02-01T00:00:00.0000000' AS DateTime2), N'PT7H30M')
INSERT [dbo].[timetracking] ([qbsql_id], [username_id], [TxnDate], [Duration]) VALUES (3, 1, CAST(N'2018-02-01T00:00:00.0000000' AS DateTime2), N'PT1H0M')
GO
SET IDENTITY_INSERT [dbo].[timetracking] OFF
我担心没有内置功能。我只写了一个,它是完全可内联的临时SQL - 但它不会很快......
你可以试试这个:
CREATE FUNCTION dbo.ConvertISO8601Periode2Seconds(@periode VARCHAR(100))
RETURNS TABLE
AS
RETURN
WITH Variables AS
(
SELECT CASE WHEN CHARINDEX('T',@Periode)>0 THEN CHARINDEX('M',@periode,CHARINDEX('T',@Periode))-1 ELSE -1 END AS posMinute
,REPLACE(SUBSTRING(@periode,2,LEN(@periode)),'T','0') AS Original
)
,SwitchMinute AS
(
SELECT CASE WHEN posMinute>0 THEN STUFF(Original,posMinute,1,'X') ELSE Original END AS WorkWith
FROM Variables
)
,recCTE AS
(
SELECT CAST(0 AS FLOAT) AS Seconds
,1 AS StartPos
,2 AS nextPos
,WorkWith
FROM SwitchMinute
UNION ALL
SELECT CASE SUBSTRING(r.WorkWith,r.nextPos,1)
WHEN 'Y' THEN CAST(SUBSTRING(r.WorkWith,r.StartPos,r.nextPos-r.StartPos) AS FLOAT) * 365 * 24 * 60 * 60
WHEN 'M' THEN CAST(SUBSTRING(r.WorkWith,r.StartPos,r.nextPos-r.StartPos) AS FLOAT) * 30 * 24 * 60 * 60
WHEN 'W' THEN CAST(SUBSTRING(r.WorkWith,r.StartPos,r.nextPos-r.StartPos) AS FLOAT) * 7 * 24 * 60 * 60
WHEN 'D' THEN CAST(SUBSTRING(r.WorkWith,r.StartPos,r.nextPos-r.StartPos) AS FLOAT) * 24 * 60 * 60
WHEN 'H' THEN CAST(SUBSTRING(r.WorkWith,r.StartPos,r.nextPos-r.StartPos) AS FLOAT) * 60 * 60
WHEN 'X' THEN CAST(SUBSTRING(r.WorkWith,r.StartPos,r.nextPos-r.StartPos) AS FLOAT) * 60
WHEN 'S' THEN CAST(SUBSTRING(r.WorkWith,r.StartPos,r.nextPos-r.StartPos) AS FLOAT) * 1
ELSE 0
END + r.Seconds
,CASE WHEN SUBSTRING(r.WorkWith,r.nextPos,1) IN('Y','M','W','D','H','X','S') THEN r.nextPos+1 ELSE r.StartPos END
,r.nextPos + 1
,r.WorkWith
FROM recCTE AS r
WHERE r.nextPos<=LEN(r.WorkWith)
)
SELECT @periode AS ISO8601Periode
,MAX(Seconds) AS Seconds
FROM recCTE;
GO
- 你这样称呼它
DECLARE @SomePeriodes TABLE(p VARCHAR(100));
INSERT INTO @SomePeriodes VALUES('P3Y6M4DT12H30M5S'),('PT8H0M'),('PT7H30M'),('PT1H0M');
SELECT ISO2Sec.ISO8601Periode
,ISO2Sec.Seconds
,ISO2Sec.Seconds/(60*60) Hrs
FROM @SomePeriodes AS p
CROSS APPLY dbo.ConvertISO8601Periode2Seconds(p.p) AS ISO2Sec;
GO
- 清理
DROP FUNCTION dbo.ConvertISO8601Periode2Seconds;
结果
ISO8601Periode Seconds Hrs
P3Y6M4DT12H30M5S 110550605 30708,5013888889
PT8H0M 28800 8
PT7H30M 27000 7,5
PT1H0M 3600 1
令人遗憾的是,ISO8601 periodes可以使用M
几个月以及几分钟。如果字符串中有T
,那么M
之后的T
就是分钟。我用X
替换它,以便直接通过字符串。
中央代码是一个递归CTE,它跟踪字符串char-by-char,记住最后一个数字的开始位置并寻找非数字。每当找到一个字母时,之前的数值相应地相乘并加到前一个值 - 从而累计所有值。
以下代码演示了ISO 8601持续时间格式子集的穷人解析器。
-- Sample input.
declare @Period as VarChar(128) = 'PT12H56.7S';
-- Parser.
with PeriodFields as (
select Cast( '' as VarChar(128) ) as Hours, Cast( '' as VarChar(128) ) as Minutes, Cast( '' as VarChar(128) ) as Seconds,
Cast( '' as VarChar(128) ) as Field,
Substring( @Period, 2, 1 ) as Character, -- The next character to be processed.
Substring( @Period, 2, 128 ) as Remainder -- The remainder of the input string.
where LEFT( @Period, 2 ) = 'PT' -- Handle only periods (P) that consist only of a time (T) without years, months, weeks or days.
union all
select
-- Save the accumulated field value when there is a field identifier, i.e. 'H', 'M' or 'S'.
case when Character = 'H' then Field else Hours end,
case when Character = 'M' then Field else Minutes end,
case when Character = 'S' then Field else Seconds end,
-- Accumulate characters in Field until there is a field identifier, i.e. 'H', 'M' or 'S'.
Cast( case when Character like '[0-9.,]' then Field + Character else '' end as VarChar(128) ),
Substring( Remainder, 2, 1 ), Substring( Remainder, 2, 128 )
from PeriodFields
where Remainder != '' )
select *,
-- Assemble the field values into an instance of Time .
Cast( DateAdd( millisecond, Cast( Seconds as Float ) * 1000.0,
DateAdd( second, Cast( Hours as Float ) * 3600 + Cast( Minutes as Float ) * 60, 0 ) ) as Time ) as Period
from PeriodFields
where Remainder = ''; -- Comment out this line to see the intermediate results.
与当前的答案不同,我想建议一个不基于递归CTE的解决方案。
我的解决方案根本不验证值,只尝试解析它们。我只实现了解析时间部分(小时,分钟和秒),但也很容易扩展以支持其他部分。
话虽这么说,我不确定这是最好的持续时间。一旦指定了几个月,您就无法使用确定性函数将ISO8601持续时间转换为代表任何时间单位的数字,因为一个月可以有28到31天之间的任何时间。
我看到它的方式,这样的持续时间只能用于通过从另一个DateTime值添加或减去持续时间来计算DateTime值。
现在,足够的谈话,让我们看看一些代码!
使用cte获取持续时间的所有部分的位置,并使用另一个cte来处理缺失值(即PT30M
):
;WITH CTE1 AS
(
SELECT [qbsql_id],
[username_id],
[TxnDate],
[Duration],
CHARINDEX('P', Duration) As Ppos,
NULLIF(CHARINDEX('Y', Duration), 0) As Ypos,
NULLIF(CHARINDEX('M', Duration), 0) As Monpos,
NULLIF(CHARINDEX('D', Duration), 0) As Dpos,
NULLIF(CHARINDEX('T', Duration), 0) As Tpos,
NULLIF(CHARINDEX('H', Duration), 0) As Hpos,
NULLIF(CHARINDEX('M', Duration, CHARINDEX('T', Duration)), 0) As Minpos,
NULLIF(CHARINDEX('S', Duration), 0) As Spos
FROM timetracking
), CTE2 AS
(
SELECT [qbsql_id],
[username_id],
[TxnDate],
[Duration],
Ppos,
COALESCE(Ypos, Ppos) AS Ypos,
COALESCE(Monpos, Ypos, Ppos) AS Monpos,
COALESCE(Dpos, Monpos, Ypos, Ppos) AS Dpos,
COALESCE(Tpos, Dpos, Monpos, Ypos, Ppos) AS Tpos,
COALESCE(Hpos, Tpos, Dpos, Monpos, Ypos, Ppos) AS Hpos,
COALESCE(Minpos, Hpos, Tpos, Dpos, Monpos, Ypos, Ppos) AS Minpos,
COALESCE(Spos, Minpos, Hpos, Tpos, Dpos, Monpos, Ypos, Ppos) AS Spos
FROM CTE1
)
从该cte中选择,计算它表示为浮点值的小时数:
SELECT [qbsql_id],
[username_id],
[TxnDate],
[Duration],
0.0 +
CASE WHEN Ppos = 1 AND Tpos > 0 THEN -- a period containing a time part
CASE WHEN Hpos > Tpos THEN
ISNULL(CAST(SUBSTRING([Duration], Tpos+1, Hpos - Tpos-1) as float), 0)
ELSE
0
END
+
CASE WHEN Minpos > Hpos THEN
ISNULL(CAST(SUBSTRING([Duration], Hpos+1, Minpos - Hpos-1) as float), 0) / 60.0
ELSE
0
END
+
CASE WHEN Spos > Minpos THEN
ISNULL(CAST(SUBSTRING([Duration], Minpos+1, Spos - Minpos-1) as float), 0) / 60.0 / 60.0
ELSE
0
END
END AS DurationInHours
FROM CTE1
结果:
qbsql_id username_id TxnDate Duration DurationInHours
1 1 02.02.2018 00:00:00 PT8H0M 8
2 2 01.02.2018 00:00:00 PT7H30M 7,5
3 1 01.02.2018 00:00:00 PT1H0M 1