web-dev-qa-db-ja.com

文字列に数字のみが含まれているかどうかを確認しようとしています

文字列に数値が含まれていないかどうかを確認する関数を作成しようとしています(つまり、検索対象の数値が「6」で、文字列が「7 + 16 + 2」である場合)この文字列の「6」は数値「16」の一部であるため、falseを返す必要があります)

私は以下の関数を書きました(時間がかかりますが、リファクタリングする前に最初にテストしました)

テストの際に、見つかった番号の最初のインスタンスのみをロジックで実行するバグを発見しました。したがって、この関数を '6'で'16 + 7 + 9 + 6 'に対して実行すると、最初の' 6 'がより大きな数の一部であると判断され、処理が停止するため、falseが返されます。

私はこれを回避するために 'haystack'文字列を短くするループを実装する必要があると考えました(そのため、例'16 + 7 + 9 + 6 'を使用すると、関数は削除した後も' + 7 + 9 + 6 'をチェックし続けます最初の '6')ですが、時間をかけて複雑な関数をさらに複雑にする前に、同じ目標を達成する簡単な方法があるかどうかを確認したいと思いましたか?

drop function dbo.runners_contain_runner
go
create function dbo.runners_contain_runner(@runner varchar(max), @runners varchar(max))
returns int
as 
begin

    /*
    eliminate the plus sign from @runners so that the 
    'isnumeric' function doesn't return false positives (it returns 1 for '+') 
    */
    set @runners = replace(@runners,'+','_' ) 


    declare @ret int;
    set @ret = 0;

    -- if the runner is the only runner return 1
    if @runners = @runner 
        set @ret = 1
    else
    begin
        declare @charindex int;
        set @charindex = charindex(@runner,@runners)
        if @charindex > 0
        begin

            -- if it is at the beginning then check the char after it 
            if @charindex = 1 
            begin
                if isnumeric(substring(@runners,@charindex + len(@runner),1)) = 0
                    set @ret = @charindex
            end

            -- if it is at the end then check the char before it
            else if @charindex = len(@runners) - (len(@runner) - 1) 
            begin
                if isnumeric(substring(@runners,@charindex - 1,1)) = 0
                    set @ret = @charindex
            end

            -- if it is in the middle check the chars either side of it
            else
            begin
                if isnumeric(substring(@runners,@charindex - 1,1)) + 
                isnumeric(substring(@runners,@charindex + len(@runner),1)) = 0
                    set @ret = @charindex 
            end
        end
    end
    return @ret
end
3
MrVimes

たぶん、あなたは数を求めることに集中しすぎて、これを過度に複雑にしています。少し前に戻ります。実際に必要なのは、両側に数字がない部分文字列です。数値がより大きい数値の一部になる唯一の方法は、その両側に少なくとも1桁あることです。したがって、数値のみを渡す限り、この定義では、どちらの側にも数字がない数値を生成する必要があります。

それを念頭に置いて、渡された値が左端、右端、または中央にある場合、3つのPATINDEX述部が必要です。うまくいくと思われるので、以下を試してください:

GO
CREATE PROCEDURE #TestFindRunner
(
  @Runner VARCHAR(10)
)
AS
SET NOCOUNT ON;

DECLARE @Data TABLE
(
  [ID] INT NOT NULL PRIMARY KEY,
  [Runners] VARCHAR(50) NULL
 );

INSERT INTO @Data ([ID], [Runners]) VALUES (1, '16+7+9+6');
INSERT INTO @Data ([ID], [Runners]) VALUES (2, '16+7+9+5');
INSERT INTO @Data ([ID], [Runners]) VALUES (3, '26+77+9+5');
INSERT INTO @Data ([ID], [Runners]) VALUES (4, '6+3+45');
INSERT INTO @Data ([ID], [Runners]) VALUES (5, '63,808,111,92');
INSERT INTO @Data ([ID], [Runners]) VALUES (6, '1-7-9,6');
INSERT INTO @Data ([ID], [Runners]) VALUES (7, '1-6-9,7');
INSERT INTO @Data ([ID], [Runners]) VALUES (8, '1-7-9,63');
INSERT INTO @Data ([ID], [Runners]) VALUES (9, '1-63-9,7');
INSERT INTO @Data ([ID], [Runners]) VALUES (10, NULL);
INSERT INTO @Data ([ID], [Runners]) VALUES (11, '6');

SELECT tmp.*
FROM   @Data tmp
WHERE  @Runner COLLATE Latin1_General_100_BIN2 = tmp.[Runners]
OR     PATINDEX('%[^0123456789]' + @Runner COLLATE Latin1_General_100_BIN2,
                tmp.[Runners]) > 0
OR     PATINDEX(@Runner + '[^0123456789]%' COLLATE Latin1_General_100_BIN2,
                tmp.[Runners]) > 0
OR     PATINDEX('%[^0123456789]' + @Runner + '[^0123456789]%'
                COLLATE Latin1_General_100_BIN2, tmp.[Runners]) > 0
GO

そして、次でテストします:

EXEC #TestFindRunner 0;
EXEC #TestFindRunner 2;
EXEC #TestFindRunner 4;
EXEC #TestFindRunner 8;
EXEC #TestFindRunner 11;
-- 0 rows

EXEC #TestFindRunner 3;   -- 4
EXEC #TestFindRunner 77;  -- 3
EXEC #TestFindRunner 111; -- 5
-- 1 row

EXEC #TestFindRunner 5; -- 2 and 3
-- 2 rows

EXEC #TestFindRunner 1; -- 6, 7, 8, and 9
-- 4 rows

EXEC #TestFindRunner 6; -- 1, 4, 6, 7, and 11
-- 5 rows

EXEC #TestFindRunner 7; -- 1, 2, 6, 7, 8, and 9
-- 6 rows

EXEC #TestFindRunner 9; -- 1, 2, 3, 6, 7, 8, and 9
-- 7 rows

PATINDEXの3つのバリエーションがある理由は、PATINDEXの検索パターンがnot正規表現(RegeEx)であるためです。 LIKEパターン)。 PATINDEXおよびLIKEパターンには数量詞がないため、[^0123456789]の単一文字の置換を「0以上」にするように指定することはできません。それは「唯一無二、それ以上でもそれ以下でもない」です。

バイナリ照合を強制すると(つまり、各COLLATE Latin1_General_100_BIN2参照の後の@Runner)、これらの10桁の10進数のみが処理され、同等と見なされる可能性のある他の文字は処理されません


上記のロジックをインラインテーブル値関数(TVF)に入れて、使いやすく(そして、同様に使いやすいスカラーUDFよりも効率的に)するには、次のことを試してください。

USE [tempdb];
GO
CREATE FUNCTION dbo.IsRunnerPresent
(
  @Runner VARCHAR(10),
  @Runners VARCHAR(8000)
)
RETURNS TABLE
WITH SCHEMABINDING
AS RETURN

  SELECT CONVERT(BIT,
    CASE WHEN @Runner COLLATE Latin1_General_100_BIN2 = @Runners
           OR PATINDEX('%[^0123456789]' + @Runner
                       COLLATE Latin1_General_100_BIN2, @Runners) > 0
           OR PATINDEX(@Runner + '[^0123456789]%'
                       COLLATE Latin1_General_100_BIN2, @Runners) > 0
           OR PATINDEX('%[^0123456789]' + @Runner + '[^0123456789]%'
                       COLLATE Latin1_General_100_BIN2, @Runners) > 0
           THEN 1
         ELSE 0
      END) AS [RunnerFound];
GO

そして、次でテストします:

DECLARE @Runner VARCHAR(10);
SET @Runner = '6';


DECLARE @Data TABLE
(
  [ID] INT NOT NULL PRIMARY KEY,
  [Runners] VARCHAR(50) NULL
 );

INSERT INTO @Data ([ID], [Runners]) VALUES (1, '16+7+9+6');
INSERT INTO @Data ([ID], [Runners]) VALUES (2, '16+7+9+5');
INSERT INTO @Data ([ID], [Runners]) VALUES (3, '26+77+9+5');
INSERT INTO @Data ([ID], [Runners]) VALUES (4, '6+3+45');
INSERT INTO @Data ([ID], [Runners]) VALUES (5, '63,808,111,92');
INSERT INTO @Data ([ID], [Runners]) VALUES (6, '1-7-9,6');
INSERT INTO @Data ([ID], [Runners]) VALUES (7, '1-6-9,7');
INSERT INTO @Data ([ID], [Runners]) VALUES (8, '1-7-9,63');
INSERT INTO @Data ([ID], [Runners]) VALUES (9, '1-63-9,7');
INSERT INTO @Data ([ID], [Runners]) VALUES (10, NULL);
INSERT INTO @Data ([ID], [Runners]) VALUES (11, '6');

SELECT tmp.[ID],
       tmp.[Runners],
       fnd.[RunnerFound]
FROM   @Data tmp
CROSS APPLY dbo.IsRunnerPresentTVF(@Runner, tmp.[Runners]) fnd;

どちらが戻ります:

ID    Runners          RunnerFound
1     16+7+9+6         1
2     16+7+9+5         0
3     26+77+9+5        0
4     6+3+45           1
5     63,808,111,92    0
6     1-7-9,6          1
7     1-6-9,7          1
8     1-7-9,63         0
9     1-63-9,7         0
10    NULL             0
11    6                1
5
Solomon Rutzky

以降、関数を次のように書き直しました。

編集:質問に対するSolomon Rutzkyの返信を見る前に、これを完了しました。彼の返事はうまくいき、私のものよりはましだ。しかし、私はこの答えを代替として残しておきます。

drop function dbo.runners_contain_runner
go
create function dbo.runners_contain_runner(@runner varchar(max), @runners varchar(max))
returns int
as 
begin
    declare @ret int

    if @runner = @runners
        set @ret = 1
    else if charindex(@runner,@runners) = 0
        set @ret = 0
    else
    begin
        declare @foundindex int
        declare @item varchar(max)
        declare @tmp varchar(max)

        declare @position int
        set @tmp = @runners
        set @position = 1
        set @foundindex = 0

        while @position > 0
        begin
            set @foundindex = @foundindex + @position 
            set @position = charindex('+',@tmp)

            if @position > 0
            begin
                set @item = substring(@tmp,1,@position - 1)
                set @tmp = substring(@tmp,@position + 1,len(@tmp) - @position)
            end
            else
                set @item = @tmp
            if @item = @runner
            begin
                set @ret = @foundindex
                break
            end
            else
                set @ret = 0
        end
    end
    return @ret
end
0
MrVimes