将 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

You can see a live demo on rextester.