Executando verificação de segurança...
3

[Dúvida] Filtrar um array de strings de forma condicional no Javascript

Existe um padrão comum pra filtrar um array de strings de forma condicional?

Por exemplo, dado a lista e os filtros a seguir:

const alphabet = 'abcde123456!@#$%'.split('')
const filteredAlphabet = alphabet
  .filter(removeLetters)
  .filter(removeNumbers)
  .filter(removeSpecial)

Existe um padrão pra poder ativar/desativar esses filtros de forma condicional, usando propriedades de uma função, por exemplo?

type Options: {
    letters: boolean,
    numbers: boolean,
    special: boolean
}

function filterList(options: Options) {
    const alphabet = 'abcde123456!@#$%'.split('')
    const filteredAlphabet = alphabet
      .filter(removeLetters) // Filtro deve ser aplicado apenas se `options.letters` for `false`
      .filter(removeNumbers) // Filtro deve ser aplicado apenas se `options.numbers` for `false`
      .filter(removeSpecial) // Filtro deve ser aplicado apenas se `options.special` for `false`
    
    return filteredAlphabet.join('')
}

filterList({ letter: true, number: true, special: false }) // abcde123456
filterList({ letter: true, number: false, special: true }) // abcde!@#$%
filterList({ letter: false, number: true, special: true }) // 123456!@#$%

Não se atente à implementação das funções removeLetters, removeNumbers e removeSpecial em si, minha dúvida é só se existe um padrão comum pra aplicar essas funções de forma condicional.

Carregando publicação patrocinada...
8

Eu não usaria filter desta forma. Isso porque cada chamada de filter percorre o array e retorna outro. Mesmo que o filtro remova alguns elementos, ainda sim na prática vc está percorrendo várias vezes o array (no pior caso, todos os elementos), e retornando outros arrays intermediários no meio do processo.

Além disso, o nome não está bom. Eu entendi que se letter for true, vc não quer remover as letras, e sim mantê-las. Por isso o remove no início dos nomes dos filtros não me parece uma boa.

Enfim, minha sugestão é percorrer os caracteres apenas uma vez, e para cada um, vc aplica todos os filtros:

function filterChars(text, options) {
    var result = '';
    for (const char of text) {
        if ((options.letter && /[a-z]/i.test(char))
             || (options.number && /[0-9]/.test(char))
             || (options.special && /[!@#$%]/.test(char))) {
            result += char;
        }
    }
    return result;
}

const alphabet = 'abcde123456!@#$%';
console.log(filterChars(alphabet, { letter: true, number: true, special: false })); // abcde123456
console.log(filterChars(alphabet, { letter: true, number: false, special: true })); // abcde!@#$%
console.log(filterChars(alphabet, { letter: false, number: true, special: true })); // 123456!@#$%

Repare que fiz o texto ser um parâmetro da função (da forma que vc fez, ela sempre verifica o mesmo texto). Assim fica mais flexível, pois vc pode passar qualquer outra string para a função.


Mas tem um detalhe: repare que a função ficou responsável por definir o que é uma letra, número ou caractere especial.

Uma forma mais flexível seria a função receber uma lista de filtros a serem aplicados. Aí para cada elemento, vc verifica se ele satisfaz algum deles:

function filterChars(text, filters) {
    var result = '';
    for (const char of text) { // para cada caractere do texto
        // verifica se ele satisfaz algum filtro
        for (const filter of filters) {
            if (filter(char)) {
                result += char;
                break; // se já satisfaz um dos filtros, não preciso verificar os outros
            }
        }
    }
    return result;
}

// cada filtro é uma função que recebe um caractere e verifica se ele satisfaz determinada condição
function isLetter(char) {
    return /[a-z]/i.test(char);
}
function isNumber(char) {
    return /[0-9]/.test(char);
}
function isSpecial(char) {
    return /[!@#$%]/.test(char);
}

const alphabet = 'abcde123456!@#$%';
console.log(filterChars(alphabet, [ isLetter, isNumber ])); // abcde123456
console.log(filterChars(alphabet, [ isLetter, isSpecial ])); // abcde!@#$%
console.log(filterChars(alphabet, [ isNumber, isSpecial ])); // 123456!@#$%

Assim fica mais flexível, pois as definições dos filtros ficam fora da função, e ela funcionará para quaisquer critérios que vc passar. E as funções dos filtros podem ser tão complexas quanto vc precisar, além de não se limitar a apenas um número fixo de opções.

E apesar de ter um loop dentro de outro, ainda é melhor do que chamar filter várias vezes. Isso porque só percorremos os caracteres apenas uma vez e não criamos os arrays intermediários. E para cada caractere, percorremos o array de filtros, mas assim que um é satisfeito, interrompemos o loop interno e ele não verifica os demais.

2

Não tinha pensado desse modo, ótima sua implementação! Eu reescrevi ela de uma forma mais concisa, mas acho que não perderia muita coisa de performance:

const isLetter = char => /[a-z]/i.test(char)
const isNumber = char => /[0-9]/.test(char)
const isSpecial = char => /[!@#$%]/.test(char)

function filterChars(text, filters) {
  const result = text.split('').filter(char => {
    for (const filter of filters) {
      if (filter(char)) return true
    }

    return false
  })

  return result.join('')
}

const alphabet = 'abcde123456!@#$%'

console.log(filterChars(alphabet, [isLetter, isNumber])) // abcde123456
console.log(filterChars(alphabet, [isLetter, isSpecial])) // abcde!@#$%
console.log(filterChars(alphabet, [isNumber, isSpecial])) // 123456!@#$%

tipado:

type FilterFn = (char: string) => boolean

const isLetter: FilterFn = (char: string) => /[a-z]/i.test(char)
const isNumber: FilterFn = (char: string) => /[0-9]/.test(char)
const isSpecial: FilterFn = (char: string) => /[!@#$%]/.test(char)

function filterChars(text: string, filters: FilterFn[]) {
  const result = text.split('').filter(char => {
    for (const filter of filters) {
      if (filter(char)) return true
    }

    return false
  })

  return result.join('')
}

O que você acha?


Além disso, o nome não está bom. Eu entendi que se letter for true, vc não quer remover as letras, e sim mantê-las. Por isso o remove no início dos nomes dos filtros não me parece uma boa.

Aqui foi erro meu na hora de digitar, o certo seria:

.filter(removeLetters) // Filtro deve ser aplicado apenas se `options.letters` for `false`
2

Eu só não vejo a necessidade de se fazer o split (que transforma a string em um array), para depois usar o filter (que cria outro array) e por fim o join (que junta tudo em uma string). Acho uma volta muito grande, sendo que dá pra fazer apenas com um loop simples pelos caracteres.

Tem outra diferença importante, caso a string tenha caracteres "diferentões", como emojis, veja:

function filterComSplit(text, filters) {
    const result = text.split('').filter(char => {
            for (const filter of filters) {
                if (filter(char))
                    return true;
            }
            return false;
        });
    return result.join('');
}

function filterSemSplit(text, filters) {
    var result = '';
    for (const char of text) { // para cada caractere do texto
        // verifica se ele satisfaz algum filtro
        for (const filter of filters) {
            if (filter(char)) {
                result += char;
                break; // se já satisfaz um dos filtros, não preciso verificar os outros
            }
        }
    }
    return result;
}

// verifica se é um dos emojis: 💩 ou 😀
function isEmoji(char) {
    const codepoint = char.codePointAt(0);
    return codepoint == 0x1f4a9 || codepoint == 0x1f600;
}

// sim, pode colocar emoji direto na string (se o editor suportar, claro)
const alphabet = 'olá 💩 etc 😀!';

// usando "for..of" funciona
console.log(filterSemSplit(alphabet, [ isEmoji ])); // 💩😀

// usando split, não funciona
console.log(filterComSplit(alphabet, [ isEmoji ])); // string vazia (não imprime nada)

Se ficou curioso, tem uma explicação detalhada sobre este problema aqui. Claro que se a string só tiver texto "normal", isso não ocorre. Mas vale lembrar que há vários caracteres de outros idiomas que podem dar este mesmo problema.

Por fim, fiz alguns testes com o Benchmark.js e a versão com split ficou cerca de 40% mais lenta. Isso porque o filter recebe como parâmetro uma função de callback que é executada para cada um dos caracteres. Claro que para poucas strings pequenas, a diferença será imperceptível (o Benchmark.js roda milhões de casos para ter uma comparação melhor). Mas enfim, o problema nem era "deixar as funções mais concisas", e sim o fato de criar um array, filtrá-lo (criando outro array) e depois juntar tudo de novo.


Quanto a trocar function por arrow function, a questão nem é "ficar mais conciso". Tem que considerar as diferenças entre uma e outra (ver aqui). Tem casos que não faz diferença, mas tem casos que faz, e esse deveria ser o critério (ser "mais curto" é mero detalhe, e o menos importante neste caso).

2

Sobre os caracteres diferentes e emojis, pro meu caso de uso, não seria um problema já que os caracteres são conhecidos e definidos hardcoded ([a-z][A-Z][0-1][!@#$%&*]), mas entendi seu ponto, não sabia que o split() não pegava esses caracteres diferentes.

Também não cheguei a fazer um benchmark com .split() e .join(), fiz de cabeça mesmo kk, mas de novo, os caracteres usados são bem definidos e o tamanho desse alfabeto não vai ser grande a esse ponto. Vou refazer esse benchmark com meu caso de uso pra ver como fica.

Sobre as arrow functions eu não fazia a mínima ideia que tinha diferenças significativas além da sintaxe, vou da uma lida no post linkado. Obrigado!

4

Boa tarde.

Bom, não creio que exista um padrão. Contudo, eu faria assim:

chars.filter(char => {
    return (
        (letters && isLetter(char)) ||
        (numbers && isNumber(char)) ||
        (special && isSpecial(char))
    )
})

Se uma flag for verdadeira, ela permite que sua respectiva função de verificação seja executada para testar o caractere em questão. Se a função retornar verdade, o caractere é incluído no novo array. Como são 3 condições distintas que podem incluir ou não o caractere no novo array, o || foi utilizado para representar essa independência.

1
1