Cómo encontrar todas las posiciones de una cadena dentro de otra cadena

11

¿Cómo puedo encontrar todas las posiciones patindexen una tabla o variable?

declare @name nvarchar(max)
set @name ='ali reza dar yek shabe barani ba yek  '
  + 'dokhtare khoshkel be disco raft va ali baraye'
  + ' 1 saat anja bud va sepas... ali...'
select patindex('%ali%',@name) as pos 

Esto vuelve 1pero quiero todos los resultados, por ejemplo:

pos
===
  1
 74
113
coditori
fuente

Respuestas:

9
declare @name nvarchar(max)
set @name ='ali reza dar yek shabe barani ba yek  dokhtare khoshkel be disco raft va ali baraye 1 saat anja bud va sepas... ali...'

Declare @a table (pos int)
Declare @pos int
Declare @oldpos int
Select @oldpos=0
select @pos=patindex('%ali%',@name) 
while @pos > 0 and @oldpos<>@pos
 begin
   insert into @a Values (@pos)
   Select @oldpos=@pos
   select @pos=patindex('%ali%',Substring(@name,@pos + 1,len(@name))) + @pos
end

Select * from @a

Para hacerlo reutilizable, puede usarlo en una función de tabla para llamarlo así:

Select * from  dbo.F_CountPats ('ali reza dar yek shabe barani ba yek  dokhtare khoshkel be disco raft va ali baraye 1 saat anja bud va sepas... ali...','%ali%')

La función podría verse así

Create FUNCTION [dbo].[F_CountPats] 
(
@txt varchar(max),
@Pat varchar(max)
)
RETURNS 
@tab TABLE 
(
 ID int
)
AS
BEGIN
Declare @pos int
Declare @oldpos int
Select @oldpos=0
select @pos=patindex(@pat,@txt) 
while @pos > 0 and @oldpos<>@pos
 begin
   insert into @tab Values (@pos)
   Select @oldpos=@pos
   select @pos=patindex(@pat,Substring(@txt,@pos + 1,len(@txt))) + @pos
end

RETURN 
END

GO
bummi
fuente
Sé que esta es una vieja pregunta, pero tengo una pregunta sobre el rendimiento. He creado dos funciones que buscan 1en una cadena que contiene solo ceros y unos. He usado Su solución y @ aaron-bertrand, pero obtuve los mismos resultados y el mismo rendimiento. ¿Qué solución sería mejor?
Misiu
2
@Misiu, como se esperaba, las soluciones de Aaron Bertrands no solo son más elegantes sino incluso mucho más rápidas que las mías y deberían ser la solución aceptada. Puede probar esto fácilmente con una entrada más grande, utilizando su ejemplo simplemente agregue SET @ name = Replicar (@ name, 5000) antes de la llamada SELECT pos FROM dbo.FindPatternLocation (@name, 'ali'); e intente lo mismo con mi procedimiento lento.
bummi
15

Creo que esto será un poco más eficiente que el método de bucle que ha elegido ( alguna evidencia aquí ), y definitivamente más eficiente que el CTE recursivo:

CREATE FUNCTION dbo.FindPatternLocation
(
    @string NVARCHAR(MAX),
    @term   NVARCHAR(255)
)
RETURNS TABLE
AS
    RETURN 
    (
      SELECT pos = Number - LEN(@term) 
      FROM (SELECT Number, Item = LTRIM(RTRIM(SUBSTRING(@string, Number, 
      CHARINDEX(@term, @string + @term, Number) - Number)))
      FROM (SELECT ROW_NUMBER() OVER (ORDER BY [object_id])
      FROM sys.all_objects) AS n(Number)
      WHERE Number > 1 AND Number <= CONVERT(INT, LEN(@string)+1)
      AND SUBSTRING(@term + @string, Number, LEN(@term)) = @term
    ) AS y);

Uso de la muestra:

DECLARE @name NVARCHAR(MAX);

SET @name = N'ali reza dar yek shabe barani ba yek'
    + '  dokhtare khoshkel be disco raft va ali baraye '
    + '1 saat anja bud va sepas... ali...';

SELECT pos FROM dbo.FindPatternLocation(@name, 'ali');

Resultados:

pos
---
  1
 74
113

Si sus cadenas serán más largas que 2K, use sys.all_columns en lugar de sys.all_objects. Si tiene más de 8K, agregue una unión cruzada.

Aaron Bertrand
fuente
2

- CTE recursivo

with cte as
(select 'ali reza dar yek shabe barani ba yek  dokhtare khoshkel be disco raft va ali baraye 1 saat anja bud va sepas... ali...' as name
), 
pos as
(select patindex('%ali%',name) pos, name from cte
union all
select pos+patindex('%ali%',substring(name, pos+1, len(name))) pos, name from pos
where patindex('%ali%',substring(name, pos+1, len(name)))>0
)
select pos from pos
msi77
fuente
0

Me encanta la respuesta de Aaron Bertrand. Aunque no lo entiendo completamente, se ve muy elegante.

En el pasado me he encontrado con problemas con los permisos al usar sys.objects. Combinado con la necesidad de solucionar el código, se me ocurrió una variación del código de Aaron y lo agregué a continuación.

Este es mi procedimiento:

CREATE PROCEDURE dbo.FindPatternLocations
-- Params
@TextToSearch nvarchar (max),
@TextToFind nvarchar (255)

AS
BEGIN

    declare @Length int
    set @Length = (Select LEN(@TextToSearch))

    declare @LengthSearchString int
    set @LengthSearchString = (select LEN (@TextToFind))

    declare @Index int
    set @Index=1

    create table #Positions (
    [POSID] [int] IDENTITY(0,1) NOT FOR REPLICATION NOT NULL,
    POS int
    )

    insert into #Positions (POS) select 0 -- to return a row even if no findings occur

        set @Index = (select charindex(@TextToFind, @TextToSearch, @Index))
                    if @Index = 0 goto Ende -- TextToFind is not in TextToSearch

        insert into #Positions (POS) select @Index


        set @Index = @Index + @LengthSearchString

while @Index <= @Length - @LengthSearchString   
    Begin
            set @Index = (select charindex(@TextToFind, @TextToSearch, @Index) )
            if @Index = 0 goto Ende -- no findings anymore
            insert into #Positions (POS) select @Index
            set @Index = @Index + @LengthSearchString
    end
Ende:
if (select MAX(posid) from #Positions) > 0 delete from #Positions where POSID = 0 -- row is not needed if TextToFind occurs
select * from #Positions
END
GO

El MAX(posid)valor también es el número de coincidencias encontradas.

Nils
fuente
Para ser pedante, eso no se parece en nada a una variación de mi código. En absoluto. :-) Este es exactamente el tipo de bucle de fuerza bruta contra el que defiendo (y he demostrado que es más lento ).
Aaron Bertrand
0

Este es un código simple basado en la respuesta de Aaron que:

  • No se limita al tamaño de sys.all_objects
  • No te pierdas la última 'X'

CÓDIGO:

DECLARE @termToFind CHAR(1) = 'X'
DECLARE @string VARCHAR(40) = 'XX XXX  X   XX'

SET @string += '.' --Add any data here (different from the one searched) to get the position of the last character

DECLARE @stringLength BIGINT = len(@string)

SELECT pos = Number - LEN(@termToFind)
FROM (
    SELECT Number
        , Item = LTRIM(RTRIM(SUBSTRING(@string, Number, CHARINDEX(@termToFind, @string + @termToFind, Number) - Number)))
    FROM (
        --All numbers between 1 and the lengh of @string. Better than use sys.all_objects
        SELECT TOP (@stringLength) row_number() OVER (
                ORDER BY t1.number
                ) AS N
        FROM master..spt_values t1
        CROSS JOIN master..spt_values t2
        ) AS n(Number)
    WHERE Number > 1
        AND Number <= CONVERT(INT, LEN(@string))
        AND SUBSTRING(@termToFind + @string, Number, LEN(@termToFind)) = @termToFind
    ) AS y

RESULTADO

pos
--------------------
1
2
4
5
6
9
13
14

(8 row(s) affected)
JotaPardo
fuente
Creo que abordé el tamaño de sys.all_columns(puede usar cualquier fuente siempre que cubra la longitud de su cadena más larga), y también lo he vuelto a probar y no veo dónde extraño la última 'X'. .
Aaron Bertrand
0

Perdón por venir tan tarde, pero me gustaría hacer las cosas más fáciles para las personas que quieren expandirse en esto. Estaba mirando cada una de estas implementaciones, tomé la que me pareció mejor (Aaron Bertrand), la simplifiqué y aquí tienes, tienes la "plantilla". Úsalo con sabiduría.

CREATE FUNCTION dbo.CHARINDICES (
    @search_expression NVARCHAR(4000),
    @expression_to_be_searched NVARCHAR(MAX)
) RETURNS TABLE AS RETURN (
    WITH tally AS (
        SELECT Number = ROW_NUMBER() OVER (ORDER BY [object_id])
        FROM sys.all_objects)
    SELECT DISTINCT n = subIdx -- (4) if we don't perform distinct we'll get result for each searched substring, and we don't want that
    FROM 
        tally 
        CROSS APPLY (SELECT subIdx = CHARINDEX(@search_expression, @expression_to_be_searched, Number)) x -- (2) subIdx is found in the rest of the substring 
    WHERE 
        Number BETWEEN 1 AND LEN(@expression_to_be_searched) -- (1) run for each substring once
        AND SubIdx != 0  -- (3) we care only about the indexes we've found, 0 stands for "not found"
)

SELECT CHARINDEX('C', 'BACBABCBABBCBACBBABC')
SELECT * FROM dbo.CHARINDICES('C', 'BACBABCBABBCBACBBABC')

Solo como referencia: puede derivar otros comportamientos de esto, como expandir en PATINDEX ():

CREATE FUNCTION dbo.PATINDICES (
    @search_expression NVARCHAR(4000) = '%[cS]%',
    @expression_to_be_searched NVARCHAR(MAX) = 'W3Schools.com'
) RETURNS TABLE AS RETURN (
    WITH tally AS (
        SELECT num = ROW_NUMBER() OVER (ORDER BY [object_id])
        FROM sys.all_objects)
    SELECT DISTINCT n = subIdx + num - 1
    FROM 
        tally 
        CROSS APPLY (SELECT numRev = LEN(@expression_to_be_searched) - num + 1) x
        CROSS APPLY (SELECT subExp = RIGHT(@expression_to_be_searched, numRev)) y
        CROSS APPLY (SELECT subIdx = PATINDEX(@search_expression, subExp)) z
    WHERE 
        num BETWEEN 1 AND LEN(@expression_to_be_searched)
        AND SubIdx != 0
)

SELECT PATINDEX('%[cS]%', 'W3Schools.com')
SELECT * FROM dbo.PATINDICES('%[cS]%', 'W3Schools.com')
OzrenTkalcecKrznaric
fuente
0
Declare @search varchar(5)
    sET @search='a'
    Declare @name varchar(40)
    Set @name='AmitabhBachan'
    Declare @init int
    Set @init=1
    Declare @hold int
    Declare @table table (POSITION Int)
    While( @init<= LEn(@name))
    Begin
   Set @hold=(Select CHARINDEX(@search,@name,@init))
   If (@hold!=0)
   BEgin 
   --Print @hold
   Insert into @table
   Select @hold
   Set @init=@hold+1
   End 
   Else
   If (@hold=0)
   BEgin
   Break
   End
  End
  Select * from @table
Raghu
fuente
Esto se beneficiaría enormemente de una muesca y cubierta consistentes. Unas pocas palabras para explicar el enfoque y la implementación también ayudarían mucho.
Michael Green