簡體   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