Programação Por Equipe DevBras

Capa do artigo: Vírgula sobrando no fim da lista: use join em vez de cortar o último caractere

Vírgula sobrando no fim da lista: use join em vez de cortar o último caractere

Cortar o último caractere para tirar a vírgula quebra acento, falha com lista vazia e esconde o bug. Veja como juntar com separador em PHP, JavaScript e SQL.

O e-mail de confirmação do pedido chegou assim: "Itens: Café, Pão de queijo, Maçã,". A vírgula sobrando no fim veio de um laço que acrescenta ", " depois de cada item, seguido de um substr($texto, 0, -1) que deveria limpar o fim. Só que o separador tem dois caracteres, a vírgula e o espaço, e o corte tirou só o espaço. A vírgula ficou.

A resposta curta: não ponha o separador para depois tirar; junte os itens com o separador entre eles. No PHP, implode(', ', $itens); no JavaScript, itens.join(', '); no SQL, GROUP_CONCAT ou STRING_AGG. Se você realmente precisa remover um fim, confira que ele está lá com str_ends_with e corte exatamente o tamanho dele.

Por que cortar o último caractere quase sempre é o conserto errado

Remover o fim trata o sintoma. O problema real é que o código monta o texto errado e depois tenta adivinhar o que sobrou. Essa adivinhação falha de vários jeitos, e cada um aparece num dia diferente:

  • O separador muda de tamanho. Alguém troca ", " por " | " e o corte de 2 caracteres passa a deixar um espaço, ou a comer uma letra.
  • O separador não está lá. Se uma regra pula a vírgula no último item, o corte apaga a última letra do item. "Maçã" vira "Maç".
  • O caractere tem mais de um byte. substr do PHP anda em byte. Cortar 1 byte de "Maçã" tira só metade do "ã", e o e-mail mostra um losango. A explicação de por que isso acontece está em strlen não conta letra.
  • A lista está vazia. O laço não roda, o texto é vazio, e o corte tenta tirar de onde não há nada. No PHP 8, substr devolve string vazia; em versões antigas devolvia false, que vazava para o resto do código.

Juntar com separador elimina todos esses casos de uma vez: com zero itens, o resultado é vazio; com um, é o item sozinho; com vários, o separador fica só entre eles.

Como montar a lista do jeito certo

  1. Guarde os itens numa lista (array) em vez de concatenar texto dentro do laço.
  2. Tire da lista os itens vazios ou só com espaços, se eles puderem aparecer.
  3. Junte com a função da linguagem, passando o separador.
  4. Se o formato é estruturado (CSV, JSON, query string), use a função específica do formato, não a junção manual.
  5. Teste com lista vazia, com um item e com um item que termina em acento.

O pedido em PHP, antes e depois

$itens = ['Café', 'Pão de queijo', 'Maçã'];

// antes
$texto = '';
foreach ($itens as $item) {
    $texto .= $item . ', ';
}
$texto = substr($texto, 0, -1);
// "Café, Pão de queijo, Maçã,"

// depois
$texto = implode(', ', $itens);
// "Café, Pão de queijo, Maçã"

Quando os itens vêm do banco ou de um formulário, alguns podem estar vazios. Filtre antes de juntar, para não gerar ", ," no meio:

$limpos = array_filter(array_map('trim', $itens), fn ($item) => $item !== '');
$texto = implode(', ', $limpos);

O array_filter com a função explícita evita um detalhe do PHP: sem ela, o item '0' seria considerado falso e sumiria da lista.

Quando você precisa mesmo tirar um fim

Às vezes o texto vem pronto de outro sistema, com uma barra no fim da URL ou um ponto e vírgula sobrando. Nesse caso, confira o fim e corte pelo tamanho dele:

$fim = ', ';
if (str_ends_with($texto, $fim)) {
    $texto = substr($texto, 0, -strlen($fim));
}

Aqui o substr é seguro, porque você sabe exatamente quais bytes está removendo: os do sufixo que acabou de conferir. A função str_ends_with existe a partir do PHP 8.0. Se o que você quer é remover a última letra visível de um texto qualquer, use mb_substr($texto, 0, -1, 'UTF-8'), que conta caracteres e não bytes.

Cuidado com rtrim($texto, ', '). Ele não remove o texto ", ", e sim qualquer combinação de vírgulas e espaços no fim, quantas houver. Às vezes é o que você quer; às vezes apaga mais do que devia. E, na lista de caracteres do rtrim, dois pontos seguidos (..) indicam um intervalo, então não use a função com sufixos que tenham pontos sem ler a documentação.

Em JavaScript

const itens = ['Café', 'Pão de queijo', 'Maçã'];

itens.join(', ');
// "Café, Pão de queijo, Maçã"

new Intl.ListFormat('pt-BR', { style: 'long', type: 'conjunction' }).format(itens);
// "Café, Pão de queijo e Maçã"

O Intl.ListFormat monta a lista como se escreve em português, com "e" antes do último item, e cuida dos casos de um e dois itens. No JavaScript, texto.slice(0, -1) remove uma unidade de UTF-16, o que parte emoji ao meio. Para cortar texto visível com segurança, o post cortar texto sem partir a letra mostra o Intl.Segmenter.

No banco de dados

-- MySQL
SELECT p.id, GROUP_CONCAT(i.nome ORDER BY i.nome SEPARATOR ', ') AS itens
FROM pedidos p
JOIN itens_pedido i ON i.pedido_id = p.id
GROUP BY p.id;

-- PostgreSQL e SQL Server
SELECT p.id, STRING_AGG(i.nome, ', ') AS itens
FROM pedidos p
JOIN itens_pedido i ON i.pedido_id = p.id
GROUP BY p.id;

No MySQL, GROUP_CONCAT tem um limite de tamanho controlado pela variável group_concat_max_len, que por padrão é 1024. Acima disso, o resultado é cortado com apenas um aviso, e o pedido com muitos itens perde os últimos sem erro nenhum. Se a lista pode ser longa, aumente o limite na sessão ou junte no código da aplicação.

Formatos que já têm função própria

Lista separada por vírgula às vezes é um formato, não um texto para gente ler. Nesses casos, montar à mão é pedir problema com aspas, vírgulas dentro do valor e caracteres especiais:

  • CSV: fputcsv no PHP coloca aspas onde precisa e escapa as que já existem.
  • Query string: http_build_query no PHP e URLSearchParams no JavaScript codificam espaço, acento e "&".
  • JSON: json_encode e JSON.stringify, nunca concatenação com vírgula.
  • SQL com lista de ids: parâmetros, não um texto "1, 2, 3" colado na consulta. O motivo está em Node.js e MySQL com parâmetro.

Erros comuns

  • Acrescentar o separador em todo item e cortar depois. Qualquer mudança no separador deixa sobra ou come uma letra.
  • Cortar com substr sem conferir o fim. Quando o separador não está lá, o último item perde o último caractere.
  • Cortar byte em texto com acento. O "ã" partido vira losango no e-mail do cliente.
  • Não testar lista vazia. Em PHP antigo, o false vaza; em qualquer versão, o texto pode sair com um rótulo sem itens.
  • Confiar no GROUP_CONCAT sem pensar no limite. Pedidos grandes chegam com a lista cortada, sem erro.
  • Montar CSV ou query string na mão. O primeiro valor com vírgula ou "&" quebra o formato.

Checklist

  • Nenhum laço acrescenta separador depois de cada item para cortar depois.
  • Listas são juntadas com implode, join ou equivalente.
  • Itens vazios são filtrados antes da junção.
  • Todo corte de fim confere o sufixo antes e corta pelo tamanho dele.
  • Corte de letra visível usa função de caractere, não de byte.
  • Há teste com zero itens, um item e item terminando em acento.
  • CSV, JSON e query string usam a função do formato.
  • Consultas com GROUP_CONCAT consideram o limite de tamanho.

Procure no seu código por substr( seguido de -1 e por slice(0, -1). Cada ocorrência está removendo algo que você conferiu antes, ou está apagando o último caractere e torcendo para ser a vírgula?

Avalie este artigo

Referência: http://www.ctasoftware.com.br/blog/remover-ultimo-caractere-php/