[英]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 有時會停止按預期工作,我會遇到一些隨機問題,例如:
這是發生問題時通常的樣子:
4065431,4025075,4045490
4061895,4064846,
9323,3761852,3963407
你會這么好心並指導我如何解決這個問題嗎?
請注意,不幸的是 function 必須在舊版本的 SQL 上運行(2014 (SP3-CU4) (KB4500181) - 12.0.6329.1)。
請嘗試以下解決方案。
它使用 XML 和 SQL 服務器的 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)
但是,有幾個潛在的邊緣情況需要處理:
以下應該能夠處理所有這些情況:
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.