繁体   English   中英

Function 通过 len 和 char 将字符串拆分为子字符串

[英]Function that splits string into substrings by len and char

我正在努力解决以下主题。

我必须根据定义的长度将字符串拆分为子部分。 额外的障碍是,在拆分时,我必须考虑最后出现的逗号,它是值的除数。 这是一个示例输入字符串:

4065431,4025075,4045490,4061895,4064846,4069323,3761852,3963407

假设我想进行拆分,以便子字符串不超过 26 个字符。 我期望得到的是以下内容:

4065431,4025075,4045490

4061895,4064846,4069323

3761852,3963407

基于已经找到的主题,我创建了以下 function:

ALTER FUNCTION [dbo].[fnRSplitString_byLen] (

        @stringToSplit nvarchar(max),
        @splitLength int
        )
returns
@returnList Table ([Name][nvarchar](max) )
as
BEGIN
    DECLARE @NAME NVARCHAR (max)
    declare @pos int
        while LEN(@stringToSplit) > 0
            BEGIN
            select @pos = len(REVERSE(left(reverse(@stringToSplit),@splitLength-CHARINDEX(',',reverse(@stringToSplit)))))
            select @name = SUBSTRING(@stringToSplit, 1, @pos-1)
            insert into @returnList
            select @name

            select @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, len(@stringToSplit) -@pos)

            END
            insert into @returnList
            select @stringToSplit
    RETURN
END

但是我有一个问题 - 由于未知原因(我认为它取决于@splitLength 值和原始字符串的总长度) function 有时会停止按预期工作,我会遇到一些随机问题,例如:

  1. 拆分是在随机位置完成的,导致子字符串以逗号开头或结尾;
  2. 当上面发生时,下一个 substring 不会从下一个字符索引开始

这是发生问题时通常的样子:

4065431,4025075,4045490

4061895,4064846,

9323,3761852,3963407

你会这么好心并指导我如何解决这个问题吗?

请注意,不幸的是 function 必须在旧版本的 SQL 上运行(2014 (SP3-CU4) (KB4500181) - 12.0.6329.1)。

请尝试以下解决方案。

它使用 XML 和 SQL 服务器的 XQuery 功能。

值得注意的点:

  • 将输入的标记字符串转换为 XML 数据类型以进行标记化
  • 根据令牌的运行总长度计算分组的系列值,并在此过程中使用mod运算符。
  • 分组系列以获取 XML 中的_start_end令牌位置。
  • 使用 XQuery .query().value()方法获得最终结果。

第一次 CTE 的结果

+----------------+-----+---------+-----------------------------------------------------------------+-------------------+-----------+--------+
|   token_xml    | pos | str_len |                        str_running_total                        | len_running_total | mod_chunk | series |
+----------------+-----+---------+-----------------------------------------------------------------+-------------------+-----------+--------+
| <r>4065431</r> |   1 |       7 |                                                         4065431 |                 7 |         7 |      0 |
| <r>4025075</r> |   2 |       7 |                                                 4065431 4025075 |                15 |        15 |      0 |
| <r>4045490</r> |   3 |       7 |                                         4065431 4025075 4045490 |                23 |        23 |      0 |
| <r>4061895</r> |   4 |       7 |                                 4065431 4025075 4045490 4061895 |                31 |         5 |     26 |
| <r>4064846</r> |   5 |       7 |                         4065431 4025075 4045490 4061895 4064846 |                39 |        13 |     26 |
| <r>4069323</r> |   6 |       7 |                 4065431 4025075 4045490 4061895 4064846 4069323 |                47 |        21 |     26 |
| <r>3761852</r> |   7 |       7 |         4065431 4025075 4045490 4061895 4064846 4069323 3761852 |                55 |         3 |     52 |
| <r>3963407</r> |   8 |       7 | 4065431 4025075 4045490 4061895 4064846 4069323 3761852 3963407 |                63 |        11 |     52 |
+----------------+-----+---------+-----------------------------------------------------------------+-------------------+-----------+--------+

SQL

DECLARE @string VARCHAR(MAX) = '4065431,4025075,4045490,4061895,4064846,4069323,3761852,3963407'
    , @separator CHAR(1) = ','
    , @tokens_max_len INT = 26;

DECLARE @xmldata XML = TRY_CAST('<root><r><![CDATA[' + 
      REPLACE(@string, @separator, ']]></r><r><![CDATA[') + 
      ']]></r></root>' AS XML);

SELECT @xmldata;

;WITH rs AS
(
    SELECT c.query('.') AS token_xml
        , pos, str_len, str_running_total, len_running_total
        , mod_chunk = len_running_total % @tokens_max_len
        , series = len_running_total - (len_running_total % @tokens_max_len)
    FROM @xmldata.nodes('/root/r') AS t(c)
       CROSS APPLY (SELECT t.c.value('let $n := . return count(/root/*[. << $n[1]]) + 1','INT') AS pos
             ) AS seq
       CROSS APPLY (SELECT t.c.value('string-length((/root/r[sql:column("pos")]/text())[1])','INT') AS str_len
             ) AS x
       CROSS APPLY (SELECT t.c.query('data(/root/r[position() le sql:column("pos")]/text())') AS str_running_total
             ) AS y
       CROSS APPLY (SELECT LEN(t.c.query('data(/root/r[position() le sql:column("pos")]/text())')
            .value('.', 'VARCHAR(MAX)')) AS len_running_total
             ) AS z
), rs2 AS
(
    SELECT series
        , _start = MIN(pos), _end = MAX(pos)
    FROM rs
    GROUP BY series
)
SELECT * 
    , result = REPLACE(@xmldata.query('data(/root/r[position() ge sql:column("_start") 
                and position() le sql:column("_end")]/text())').value('.', 'VARCHAR(100)')
                , SPACE(1), @separator)
FROM rs2;

Output

+--------+--------+------+-------------------------+
| series | _start | _end |         result          |
+--------+--------+------+-------------------------+
|      0 |      1 |    3 | 4065431,4025075,4045490 |
|     26 |      4 |    6 | 4061895,4064846,4069323 |
|     52 |      7 |    8 |         3761852,3963407 |
+--------+--------+------+-------------------------+

主要问题在于@Pos计算,它定位字符串中的最后一个逗号,然后使用它来计算从字符串开头提取的长度。

以下表达式可用于定位字符串的第一个 @splitLength + 1 个字符中的最后一个逗号,

SELECT @Pos = @splitLength + 2 - NULLIF(CHARINDEX(',',reverse(SUBSTRING(D.StringToSplit, 1, @splitLength+1))), 0)

但是,有几个潜在的边缘情况需要处理:

  1. 剩余的字符串已经比@splitLength 短。
  2. 第一个逗号出现在多个 @splitLength 前导字符之后。
  3. 该字符串不包含逗号。

以下应该能够处理所有这些情况:

        select @pos = CASE WHEN LEN(@stringToSplit) <= @splitLength
            THEN LEN(@stringToSplit) + 1  -- Take it all
            ELSE COALESCE(
                @splitLength + 2 - NULLIF(CHARINDEX(',',reverse(SUBSTRING(@stringToSplit, 1, @splitLength+1))), 0),  -- Normal case
                NULLIF(CHARINDEX(',', @stringToSplit), 0), -- Overlength
                LEN(@stringToSplit) + 1  -- No comma, might also be overlength
            )
        END

对于可能超长的值,该值将在单独的结果记录中保持不变。 这种情况可以用不同的方式处理,例如在没有分隔符的地方强制中断。 在这种情况下,剩余的字符串表达式需要调整,因为没有逗号可以跳过。

循环底部剩余文本的重新计算也需要调整以处理终端情况。 (这也使用STUFF而不是SUBSTRING来删除第一个 @pos 字符。)

        select @stringToSplit =
            CASE WHEN @Pos <= LEN(@stringToSplit)
            THEN ''
            ELSE STUFF(@stringToSplit, 1, @pos, '')
            END

最后,似乎 post loop insert 将始终插入一个空字符串,因此似乎没有必要。

更新后的 function 将类似于:

CREATE FUNCTION [dbo].[fnRSplitString_byLen] (

        @stringToSplit nvarchar(max),
        @splitLength int
        )
returns
@returnList Table ([Name][nvarchar](max) )
as
BEGIN
    DECLARE @NAME NVARCHAR (max)
    declare @pos int
    while LEN(@stringToSplit) > 0
    BEGIN
        select @pos = CASE WHEN LEN(@stringToSplit) <= @splitLength
            THEN LEN(@stringToSplit) + 1  -- Take it all
            ELSE COALESCE(
                @splitLength + 2 - NULLIF(CHARINDEX(',',reverse(SUBSTRING(@stringToSplit, 1, @splitLength+1))), 0),  -- Normal case
                NULLIF(CHARINDEX(',', @stringToSplit), 0), -- Overlength
                LEN(@stringToSplit) + 1  -- No comma, might also be overlength
            )
        END

        select @name = SUBSTRING(@stringToSplit, 1, @pos-1)
        insert into @returnList
        select @name

        select @stringToSplit =
            CASE WHEN @Pos > LEN(@stringToSplit)
            THEN ''
            ELSE STUFF(@stringToSplit, 1, @pos, '')
            END

    END
    --insert into @returnList
    --select @stringToSplit
    RETURN
END

可能还有其他需要改进的地方,但我相信这应该处理一般情况。

有关包含各种测试数据的工作演示,请参阅此 db<>fiddle

Yitzhak Khabinsky 提出的基于 XML 的解决方案可能是更好的方法,但它是作为一种紧密遵循原始逻辑的方法提供的。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM