1

Quantos caracteres tem "Ação 👍🏽"? JavaScript diz 9, Python diz 7

Enquanto eu desenvolvia um contador de caracteres para o meu site, um teste meu falhou. Eu esperava que 'Ação 👍🏽'.length desse 8 no JavaScript, mas deu 9. Fui conferir em outras linguagens, e cada uma deu um número diferente:

Como conteiResultado
JavaScript, .length9
Python, len()7
PHP, mb_strlen()7
PHP, strlen()15
Como uma pessoa conta6

As cinco respostas estão certas: cada uma conta uma coisa diferente.

O que cada linguagem conta

  • Bytes (15): o strlen do PHP conta os bytes em UTF-8. "ç" e "ã" ocupam 2 bytes cada, e cada parte do emoji ocupa 4.
  • Unidades UTF-16 (9): o .length do JavaScript (e do Java) conta pedaços de 16 bits. Emojis ficam fora do espaço de 16 bits e ocupam dois pedaços cada.
  • Code points (7): o len do Python e o mb_strlen do PHP contam os caracteres Unicode.
  • Grafemas (6): o que aparece na tela. 👍🏽 são dois code points, o polegar e o tom de pele, que viram um desenho só.

Na prática, o problema aparece quando o front-end limita um campo a 280 caracteres com .length e o back-end valida com outra regra. Para a pessoa, o texto cabe; para o servidor, não.

Fica pior

O mesmo "Ação", digitado no Mac ou colado de um PDF, pode vir decomposto: o "ã" vira "a" + til. As duas versões aparecem iguais na tela, mas não são iguais no código:

const a = 'Ação 👍🏽';
const b = a.normalize('NFD');
console.log(a === b);              // false
console.log(a.length, b.length);   // 9 11

E cortar o texto também cobra o preço. Em JavaScript, 'Ação 👍🏽'.slice(0, 6) corta o emoji no meio e deixa um caractere quebrado no final. Em Python, s[:6] não quebra nada, mas perde o tom de pele e devolve um 👍 amarelo.

Como resolvi

Para contar como uma pessoa conta, uso o Intl.Segmenter, que separa o texto em grafemas e já vem nos navegadores modernos e no Node:

const seg = new Intl.Segmenter('pt-BR', { granularity: 'grapheme' });
const caracteres = (s) => [...seg.segment(s)].length;

caracteres('Ação 👍🏽');                    // 6
caracteres('Ação 👍🏽'.normalize('NFD'));   // 6
caracteres('👨‍👩‍👧');                        // 1 (são 8 no .length)

No contador, mostro as três medidas lado a lado: os caracteres como a pessoa vê, os bytes em UTF-8 (o que pesa no banco e na rede) e o .length, para quem precisa bater com um maxlength do HTML.

O que aprendi

  1. Antes de limitar um campo, decida o que é "caractere" e use a mesma regra no front-end e no back-end.
  2. Normalize o texto (normalize('NFC')) antes de comparar ou salvar, para "Ação" ser sempre igual a "Ação".
  3. Para cortar texto sem quebrar emoji, corte por grafemas, não com slice.
  4. Limite de banco e de API geralmente é em bytes ou em code points, não no que a pessoa vê. Confira na documentação.

Você já tomou um erro desses em produção? Conta nos comentários.

Carregando publicação patrocinada...