首字母大写,例外

Joe*_*Joe 5 sql-server sql-server-2014 regex

我需要正确格式化一些欧洲地址。其中一个步骤是将第一个字母大写,但要避免一些特定的词,例如“on”、“on”、“von”、“van”、“di”、“in”、“sul”。因此,虽然我的技能稀缺,但我认为使用基于 RegEx 的函数是个好主意。

经过一番谷歌搜索后,我在这里找到了这个:

CREATE FUNCTION InitialCap
(
    @String nvarchar(max)
)
    RETURNS nvarchar(max)
AS
    BEGIN 
        DECLARE @Position INT;

        SELECT 
            @String   = STUFF(LOWER(@String),1,1,UPPER(LEFT(@String,1))) COLLATE Latin1_General_Bin,
            @Position = PATINDEX('%[^A-Za-z][a-z]%',@String COLLATE Latin1_General_Bin);

        WHILE @Position > 0
            SELECT 
                @String   = STUFF(@String,@Position,2,UPPER(SUBSTRING(@String,@Position,2))) COLLATE Latin1_General_Bin,
                @Position = PATINDEX('%[^A-Za-z][a-z]%',@String COLLATE Latin1_General_Bin);

        RETURN @String;
    END
Run Code Online (Sandbox Code Playgroud)

这似乎是在寻找一个“非字母”+一个小写“字母”的序列

[^A-Za-z][a-z]
Run Code Online (Sandbox Code Playgroud)

好的,我想我已经了解它是如何工作的,并且我对其进行了修改以最好地满足我的需求。

我认为最好搜索一个空格或 ' 或 - 和一个小写字母,因此我将其更改为

[\s'-][\w]
Run Code Online (Sandbox Code Playgroud)

然后,经过多次尝试,我在 regexr.com 上构建了这个 RegEx,它似乎捕获了所需的序列:

[\s](?!di\s|in\s|sul\s|on\s|upon\s|von\s|uber\s|ueber\s)[\w]
Run Code Online (Sandbox Code Playgroud)

但是当我把它放到上面的函数中时,结果并不像预期的那样。

怎么了?

Sol*_*zky 10

SQL Server 内部不支持正则表达式。LIKE并且PATINDEX两者都支持非常有限的通配符,包括与 RegEx 语法相似的单字符范围匹配[...]或排除[^...],并且在某种程度上功能相似,但肯定不是 RegEx。

如果您想要/需要 SQL Server 中的正则表达式,则需要使用 SQLCLR。您可以自己编写代码,也可以使用预先构建的函数,例如SQL#(我编写的)中可用的函数。大多数 RegEx 函数在免费版本中可用。我认为您可以使用RegEx_Matches返回不在排除列表中的单词结果集,然后将其与String_ToTitleCase4k函数(在免费版本中也可用)相结合来执行 InitCap。

例如:

DECLARE @Input NVARCHAR(MAX) =
               N'santacroce sull''arno o''sullivan suLL sUlLiVan gsantacroce',
        @Expression NVARCHAR(4000) =
               N'[\s](?!di\s|in\s|sull\s|on\s|upon\s|von\s|uber\s|ueber\s)[\w]';

-- show matches for debugging
SELECT word.[StartPos],
       word.[EndPos],
       word.[Value] AS [Original],
       SQL#.String_ToTitleCase4k(word.[Value], N'') AS [TitleCased]
FROM   SQL#.RegEx_Matches(@Input, @Expression, 1, N'ignorecase') word;


SELECT @Input = STUFF(@Input,
                      word.[StartPos],
                      ((word.[EndPos] - word.[StartPos]) + 1),
                      SQL#.String_ToTitleCase4k(word.[Value], N'')
                                                COLLATE Latin1_General_100_BIN2)
FROM SQL#.RegEx_Matches(@Input, @Expression, 1, N'ignorecase') word;


SELECT @Input AS [Fixed];
Run Code Online (Sandbox Code Playgroud)

返回:

DECLARE @Input NVARCHAR(MAX) =
               N'santacroce sull''arno o''sullivan suLL sUlLiVan gsantacroce',
        @Expression NVARCHAR(4000) =
               N'[\s](?!di\s|in\s|sull\s|on\s|upon\s|von\s|uber\s|ueber\s)[\w]';

-- show matches for debugging
SELECT word.[StartPos],
       word.[EndPos],
       word.[Value] AS [Original],
       SQL#.String_ToTitleCase4k(word.[Value], N'') AS [TitleCased]
FROM   SQL#.RegEx_Matches(@Input, @Expression, 1, N'ignorecase') word;


SELECT @Input = STUFF(@Input,
                      word.[StartPos],
                      ((word.[EndPos] - word.[StartPos]) + 1),
                      SQL#.String_ToTitleCase4k(word.[Value], N'')
                                                COLLATE Latin1_General_100_BIN2)
FROM SQL#.RegEx_Matches(@Input, @Expression, 1, N'ignorecase') word;


SELECT @Input AS [Fixed];
Run Code Online (Sandbox Code Playgroud)

它不能完全正确工作的原因是由于您的正则表达式不正确:

  1. 它只匹配一个字母。
  2. 如果它们位于字符串的末尾,它不会排除任何片段,但如果在实际使用中从未发生过,那可能没问题。
  3. 它不会包含字符串的第一个单词(由于左侧需要空格),但如果在实际使用中从未出现过,那可能没问题。

更新:
我能够通过将其更改为如下来修复您的正则表达式:

StartPos    EndPos    Original    TitleCased
--------    ------    --------    ----------
11          12        s           S
21          22        o           O
32          33        s           S
37          38        s           S
46          47        g           G


Fixed
-------------------------
santacroce Sull'arno O'sullivan SuLL SUlLiVan Gsantacroce
Run Code Online (Sandbox Code Playgroud)

与原版的主要区别:

  1. 我使用\b(字边界)而不是\s(空白),因为它处理行/字符串的开头和结尾。它也不会捕获空格,\s如果它在视觉上不明显,上面的每个匹配的字符串都以匹配的空格为前缀。虽然该空格不会影响替换,因为它仍然是一个空格,但它确实阻止了组中的第一个单词匹配,除非整个字符串前面有一些空格。在与地址一起使用的情况下,如果它们总是以数字开头,那么可能总会有前面的空格,但最好不要将其包含在匹配中。
  2. 我将+(一个或多个)量词添加到 the 中,\w以便它不仅可以获取第一个字符
  3. 我通过将\b每个片段末尾的公共移到新的内部非捕获组之外来简化排除列表。这是非功能性差异。它只是让阅读和处理变得更容易。

新输出:

\b(?!(?:di|in|sull|on|upon|von|uber|ueber)\b)\w+
Run Code Online (Sandbox Code Playgroud)

更新 2:

如果希望有一个可更新的排除词列表,而无需更新包含正则表达式的函数,那么通过执行以下操作很容易实现:

  1. 创建一个表来保存排除词:

    StartPos    EndPos    Original      TitleCased
    --------    ------    --------      ----------
    1           10        santacroce    Santacroce
    17          20        arno          Arno
    22          22        o             O
    24          31        sullivan      Sullivan
    38          45        sUlLiVan      Sullivan
    47          57        gsantacroce   Gsantacroce
    
    Fixed
    -------------------------
    Santacroce sull'Arno O'Sullivan suLL Sullivan Gsantacroce
    
    Run Code Online (Sandbox Code Playgroud)
  2. 填充/管理该表中的单词:

    CREATE TABLE #ExcludeWords ([Word] NVARCHAR(50) NOT NULL);
    
    Run Code Online (Sandbox Code Playgroud)
  3. 在您执行此数据清理的任何代码中,从要排除的单词表中动态构建正则表达式:

    INSERT INTO #ExcludeWords ([Word]) VALUES
       (N'di'), (N'in'), (N'sull'), (N'on'), (N'upon'), (N'von'), (N'uber'), (N'ueber');
    
    Run Code Online (Sandbox Code Playgroud)

更新 3:

最初没有提供样本数据,所以两个答案都用一个简单的单词列表进行测试,其中一些是排除词。但是现在已经提供了一些测试用例,并且在使用其中一个测试用例进行测试时,我发现我的实现存在一些问题:

  1. 我忘记为 RegEx 函数指定“不区分大小写”选项
  2. 我正在替换原始字符串中出现的所有匹配项。当整个字符串中没有匹配项是其他字符串的子字符串时,这可以正常工作。但是当包括“O'Sullivan”时,“O”是几个项目的子串,因此产生了错误的结果。

所以,我已经调整了上面的代码和测试用例以及结果来解决这些问题。主要区别是:

  1. 添加了 RegEx 选项 N'ignorecase'
  2. 切换REPLACE与功能STUFF,让我用每场比赛的起点和终点位置只替换一个项目

请注意:

  1. 问题中的排除列表与问题评论中提供的示例数据之间存在细微差异:问题使用sul而评论使用sull。我已经调整了我的答案以使用sull评论中提供的测试用例中显示的(两个“L”)。
  2. 这就是为什么拥有和/或提供实际测试数据至关重要的原因编号 5,235,948,567 ;-)。


小智 1

恕我直言,我认为上述 UDF 无法满足您的要求。

我的方法是,

  1. 创建分割字符串 UDF
  2. 创建排除单词词典表。

它会像这样工作。我有一个拆分字符串 UDF。您可以创建或下载自己的分割字符串函数。

        declare @Excludeword table(word varchar(50))--this should be permanenet table
insert into @Excludeword VALUES ('on'), ('upon'),('von'),('van'),('di'),('in'),('sul')

    DECLARE @String nvarchar(max)='santacroce sull''arno'--'caT  UPON the wet floor'

SELECT stuff((
            SELECT ' ' + CASE 
                    WHEN ca.word = item
                        THEN ca.word
                    WHEN charindex('''', item) > 0
                        THEN STUFF(LOWER(item), charindex('''', item) + 1, 1, UPPER(substring(item, charindex('''', item) + 1, 1))) COLLATE Latin1_General_Bin
                    ELSE STUFF(LOWER(item), 1, 1, UPPER(LEFT(item, 1))) COLLATE Latin1_General_Bin
                    END
            FROM dbo.DelimitedSplit8K(@String, ' ')
            OUTER APPLY (
                SELECT word
                FROM @Excludeword E
                WHERE e.word = item
                ) ca
            WHERE item <> ''
            FOR XML PATH('')
            ), 1, 1, '') Item
Run Code Online (Sandbox Code Playgroud)

输出, Cat on The Wet FloorSantacroce sull'ArnoCat upon The Wet Floor

您应该在问题本身中用适当的示例提及所有业务规则。

另外您打算如何使用它?就像您会传递一个变量还是您将处理整个表一样。

笔记 :

这只是粗略的想法。

如果这是您正在寻找的,那么可以根据您的要求定制 Split String,并且可以将一些内容封装在 UDf 本身中。

下载[dbo].[DelimitedSplit8K]

  • 我已经删除了这篇文章的许多评论。如果您认为需要访问那些已删除的评论,请向版主发送消息。或者在这里回复我,我回复后也会被删除。 (2认同)