Como iterar caracteres individuais em uma string Lua?

87

Eu tenho uma string em Lua e desejo iterar caracteres individuais nela. Mas nenhum código que tentei funciona e o manual oficial apenas mostra como encontrar e substituir substrings :(

str = "abcd"
for char in str do -- error
  print( char )
end

for i = 1, str:len() do
  print( str[ i ] ) -- nil
end
Grigoryvp
fonte

Respostas:

123

Em lua 5.1, você pode iterar os caracteres de uma string de algumas maneiras.

O loop básico seria:

para i = 1, #str faça
    local c = str: sub (i, i)
    - faça algo com c
fim

Mas pode ser mais eficiente usar um padrão string.gmatch()para obter um iterador sobre os caracteres:

para c em str: gmatch "." Faz
    - faça algo com c
fim

Ou ainda para usar string.gsub()para chamar uma função para cada char:

str: gsub (".", função (c)
    - faça algo com c
fim)

Em todos os itens acima, aproveitei o fato de que o stringmódulo é definido como uma metatabela para todos os valores de string, portanto, suas funções podem ser chamadas como membros usando a :notação. Eu também usei o (novo no 5.1, IIRC) #para obter o comprimento da string.

A melhor resposta para sua aplicação depende de muitos fatores, e os benchmarks são seus amigos se o desempenho for importante.

Você pode querer avaliar por que precisa iterar sobre os caracteres e olhar para um dos módulos de expressão regular que foram vinculados a Lua, ou para uma abordagem moderna, uma olhada no módulo lpeg de Roberto que implementa Parsing Expression Grammers para Lua.

RBerteig
fonte
Obrigado. Sobre o módulo lpeg que você mencionou - ele salva as posições dos tokens no texto original após a tokenização? A tarefa que eu preciso realizar é realçar a sintaxe de uma linguagem simples específica no scite via lua (sem nenhum analisador c ++ compilado). Além disso, como instalar o lpeg? Parece que tem fonte .c na distribuição - ele precisa ser compilado junto com lua?
grigoryvp
Construir lpeg produzirá uma DLL (ou .so) que deve ser armazenada onde o require puder encontrá-la. (isto é, em algum lugar identificado pelo conteúdo do package.cpath global em sua instalação lua.) Você também precisa instalar seu módulo complementar re.lua se quiser usar sua sintaxe simplificada. A partir de uma gramática lpeg, você pode obter retornos de chamada e capturar texto de várias maneiras, e certamente é possível usar capturas para simplesmente armazenar a localização da correspondência para uso posterior. Se o destaque da sintaxe for o objetivo, um PEG não é uma escolha ruim de ferramenta.
RBerteig
3
Sem mencionar que os lançamentos mais recentes do SciTE (desde 2.22) incluem o Scintillua, um lexer baseado em LPEG, o que significa que ele pode funcionar imediatamente, sem necessidade de recompilação.
Stuart P. Bentley
11

Se você estiver usando Lua 5, tente:

for i = 1, string.len(str) do
    print( string.sub(str, i, i) )
end
Aaron Saarela
fonte
9

Dependendo da tarefa em mãos, pode ser mais fácil de usar string.byte. É também a maneira mais rápida porque evita a criação de uma nova substring que parece ser muito cara em Lua graças ao hash de cada nova string e à verificação se ela já é conhecida. Você pode pré-calcular o código de símbolos que procura para string.bytemanter a legibilidade e a portabilidade.

local str = "ab/cd/ef"
local target = string.byte("/")
for idx = 1, #str do
   if str:byte(idx) == target then
      print("Target found at:", idx)
   end
end
Oleg V. Volkov
fonte
4

Já existem muitas boas abordagens nas respostas fornecidas ( aqui , aqui e aqui ). Se velocidade é o que você está procurando principalmente , definitivamente deve considerar fazer o trabalho por meio da API C de Lua, que é muitas vezes mais rápida do que o código Lua bruto. Ao trabalhar com blocos pré-carregados (por exemplo, função de carregamento ), a diferença não é tão grande, mas ainda é considerável.

Quanto às soluções Lua pura , deixe-me compartilhar este pequeno benchmark que fiz. Ele cobre todas as respostas fornecidas até esta data e adiciona algumas otimizações. Ainda assim, o básico a considerar é:

Quantas vezes você precisará iterar sobre os caracteres da string?

  • Se a resposta for "uma vez", então você deve procurar a primeira parte do banchmark ("velocidade bruta").
  • Caso contrário, a segunda parte fornecerá uma estimativa mais precisa, porque analisa a string na tabela, o que é muito mais rápido para iterar. Você também deve pensar em escrever uma função simples para isso, como sugeriu @Jarriz.

Aqui está o código completo:

-- Setup locals
local str = "Hello World!"
local attempts = 5000000
local reuses = 10 -- For the second part of benchmark: Table values are reused 10 times. Change this according to your needs.
local x, c, elapsed, tbl
-- "Localize" funcs to minimize lookup overhead
local stringbyte, stringchar, stringsub, stringgsub, stringgmatch = string.byte, string.char, string.sub, string.gsub, string.gmatch

print("-----------------------")
print("Raw speed:")
print("-----------------------")

-- Version 1 - string.sub in loop
x = os.clock()
for j = 1, attempts do
    for i = 1, #str do
        c = stringsub(str, i)
    end
end
elapsed = os.clock() - x
print(string.format("V1: elapsed time: %.3f", elapsed))

-- Version 2 - string.gmatch loop
x = os.clock()
for j = 1, attempts do
    for c in stringgmatch(str, ".") do end
end
elapsed = os.clock() - x
print(string.format("V2: elapsed time: %.3f", elapsed))

-- Version 3 - string.gsub callback
x = os.clock()
for j = 1, attempts do
    stringgsub(str, ".", function(c) end)
end
elapsed = os.clock() - x
print(string.format("V3: elapsed time: %.3f", elapsed))

-- For version 4
local str2table = function(str)
    local ret = {}
    for i = 1, #str do
        ret[i] = stringsub(str, i) -- Note: This is a lot faster than using table.insert
    end
    return ret
end

-- Version 4 - function str2table
x = os.clock()
for j = 1, attempts do
    tbl = str2table(str)
    for i = 1, #tbl do -- Note: This type of loop is a lot faster than "pairs" loop.
        c = tbl[i]
    end
end
elapsed = os.clock() - x
print(string.format("V4: elapsed time: %.3f", elapsed))

-- Version 5 - string.byte
x = os.clock()
for j = 1, attempts do
    tbl = {stringbyte(str, 1, #str)} -- Note: This is about 15% faster than calling string.byte for every character.
    for i = 1, #tbl do
        c = tbl[i] -- Note: produces char codes instead of chars.
    end
end
elapsed = os.clock() - x
print(string.format("V5: elapsed time: %.3f", elapsed))

-- Version 5b - string.byte + conversion back to chars
x = os.clock()
for j = 1, attempts do
    tbl = {stringbyte(str, 1, #str)} -- Note: This is about 15% faster than calling string.byte for every character.
    for i = 1, #tbl do
        c = stringchar(tbl[i])
    end
end
elapsed = os.clock() - x
print(string.format("V5b: elapsed time: %.3f", elapsed))

print("-----------------------")
print("Creating cache table ("..reuses.." reuses):")
print("-----------------------")

-- Version 1 - string.sub in loop
x = os.clock()
for k = 1, attempts do
    tbl = {}
    for i = 1, #str do
        tbl[i] = stringsub(str, i) -- Note: This is a lot faster than using table.insert
    end
    for j = 1, reuses do
        for i = 1, #tbl do
            c = tbl[i]
        end
    end
end
elapsed = os.clock() - x
print(string.format("V1: elapsed time: %.3f", elapsed))

-- Version 2 - string.gmatch loop
x = os.clock()
for k = 1, attempts do
    tbl = {}
    local tblc = 1 -- Note: This is faster than table.insert
    for c in stringgmatch(str, ".") do
        tbl[tblc] = c
        tblc = tblc + 1
    end
    for j = 1, reuses do
        for i = 1, #tbl do
            c = tbl[i]
        end
    end
end
elapsed = os.clock() - x
print(string.format("V2: elapsed time: %.3f", elapsed))

-- Version 3 - string.gsub callback
x = os.clock()
for k = 1, attempts do
    tbl = {}
    local tblc = 1 -- Note: This is faster than table.insert
    stringgsub(str, ".", function(c)
        tbl[tblc] = c
        tblc = tblc + 1
    end)
    for j = 1, reuses do
        for i = 1, #tbl do
            c = tbl[i]
        end
    end
end
elapsed = os.clock() - x
print(string.format("V3: elapsed time: %.3f", elapsed))

-- Version 4 - str2table func before loop
x = os.clock()
for k = 1, attempts do
    tbl = str2table(str)
    for j = 1, reuses do
        for i = 1, #tbl do -- Note: This type of loop is a lot faster than "pairs" loop.
            c = tbl[i]
        end
    end
end
elapsed = os.clock() - x
print(string.format("V4: elapsed time: %.3f", elapsed))

-- Version 5 - string.byte to create table
x = os.clock()
for k = 1, attempts do
    tbl = {stringbyte(str,1,#str)}
    for j = 1, reuses do
        for i = 1, #tbl do
            c = tbl[i]
        end
    end
end
elapsed = os.clock() - x
print(string.format("V5: elapsed time: %.3f", elapsed))

-- Version 5b - string.byte to create table + string.char loop to convert bytes to chars
x = os.clock()
for k = 1, attempts do
    tbl = {stringbyte(str, 1, #str)}
    for i = 1, #tbl do
        tbl[i] = stringchar(tbl[i])
    end
    for j = 1, reuses do
        for i = 1, #tbl do
            c = tbl[i]
        end
    end
end
elapsed = os.clock() - x
print(string.format("V5b: elapsed time: %.3f", elapsed))

Saída de exemplo (Lua 5.3.4, Windows) :

-----------------------
Raw speed:
-----------------------
V1: elapsed time: 3.713
V2: elapsed time: 5.089
V3: elapsed time: 5.222
V4: elapsed time: 4.066
V5: elapsed time: 2.627
V5b: elapsed time: 3.627
-----------------------
Creating cache table (10 reuses):
-----------------------
V1: elapsed time: 20.381
V2: elapsed time: 23.913
V3: elapsed time: 25.221
V4: elapsed time: 20.551
V5: elapsed time: 13.473
V5b: elapsed time: 18.046

Resultado:

No meu caso, os string.bytee string.subforam os mais rápidos em termos de velocidade bruta. Ao usar a tabela de cache e reutilizá-la 10 vezes por loop, a string.byteversão foi mais rápida mesmo ao converter charcodes de volta para chars (o que nem sempre é necessário e depende do uso).

Como você provavelmente notou, fiz algumas suposições com base em meus benchmarks anteriores e as apliquei ao código:

  1. As funções de biblioteca devem ser sempre localizadas se usadas dentro de loops, porque é muito mais rápido.
  2. Inserir um novo elemento na tabela lua é muito mais rápido usando do tbl[idx] = valueque table.insert(tbl, value).
  3. O loop através da tabela usando for i = 1, #tblé um pouco mais rápido do que for k, v in pairs(tbl).
  4. Prefira sempre a versão com menos chamadas de função, pois a chamada em si adiciona um pouco ao tempo de execução.

Espero que ajude.

Electrix
fonte
0

Todas as pessoas sugerem um método menos ideal

Será o melhor:

    function chars(str)
        strc = {}
        for i = 1, #str do
            table.insert(strc, string.sub(str, i, i))
        end
        return strc
    end

    str = "Hello world!"
    char = chars(str)
    print("Char 2: "..char[2]) -- prints the char 'e'
    print("-------------------\n")
    for i = 1, #str do -- testing printing all the chars
        if (char[i] == " ") then
            print("Char "..i..": [[space]]")
        else
            print("Char "..i..": "..char[i])
        end
    end
Jarriz
fonte
"Menos ideal" para que tarefa? "Melhor" para que tarefa?
Oleg V. Volkov