[英]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.