将 ISO 8601 持续时间转换为十进制时间 SQL 值,例如 PT7H30M 或 PT8H0M
Convert ISO 8601 duration to decimal time SQL values such as PT7H30M or PT8H0M
我需要将 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
恐怕没有built-in功能。
我刚刚写了一个,它是完全可内联的 ad-hoc 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
一些解释
遗憾的是,ISO 8601 周期可以使用 M
几个月和几分钟。如果字符串中有T
,则T
之后的M
就是分钟。我将其替换为 X
以便直接处理字符串。
中央代码是一个递归的 CTE,跟踪字符串 char-by-char,记住最后一个数字的开始位置并寻找 non-numbers。每当找到一个字母时,前面的数值就会相应地乘以并添加到前一个值 - 从而累积所有值。
以下代码演示了 ISO 8601 持续时间格式子集的穷人解析器。
- 它只处理时间字段:小时、分钟和秒。
- 它不处理日期字段:年、月、周和日。可以通过直接扩展代码来添加它们。
- 它支持小数,例如"PT12.5S".
- 它不会验证字段值(尤其是小数部分)的格式是否正确,例如“0,2.3”。 (CAST 可能会捕获一些错误,但解析器不会查找它们。)
- 不验证只有最小单位有小数。 (这可以作为最终 SELECT 中的检查来实现,以确保小数点(如果有)仅出现在最小的 non-empty 单元中。)
- 它不执行一般顺序的验证,即字段必须从大到小的单位。 (这可以通过添加一列来跟踪 "used" 字段来实现,例如作为位掩码。)
-- 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
)
Select 来自那个 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
我需要将 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
恐怕没有built-in功能。 我刚刚写了一个,它是完全可内联的 ad-hoc 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
一些解释
遗憾的是,ISO 8601 周期可以使用 M
几个月和几分钟。如果字符串中有T
,则T
之后的M
就是分钟。我将其替换为 X
以便直接处理字符串。
中央代码是一个递归的 CTE,跟踪字符串 char-by-char,记住最后一个数字的开始位置并寻找 non-numbers。每当找到一个字母时,前面的数值就会相应地乘以并添加到前一个值 - 从而累积所有值。
以下代码演示了 ISO 8601 持续时间格式子集的穷人解析器。
- 它只处理时间字段:小时、分钟和秒。
- 它不处理日期字段:年、月、周和日。可以通过直接扩展代码来添加它们。
- 它支持小数,例如"PT12.5S".
- 它不会验证字段值(尤其是小数部分)的格式是否正确,例如“0,2.3”。 (CAST 可能会捕获一些错误,但解析器不会查找它们。)
- 不验证只有最小单位有小数。 (这可以作为最终 SELECT 中的检查来实现,以确保小数点(如果有)仅出现在最小的 non-empty 单元中。)
- 它不执行一般顺序的验证,即字段必须从大到小的单位。 (这可以通过添加一列来跟踪 "used" 字段来实现,例如作为位掩码。)
-- 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
)
Select 来自那个 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