Dev.to WebDev πŸ›  Dev πŸ‘ 0 πŸ“– 2 min read

Why "πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦".length Is 11: Counting Characters the Way Users See Them

Our word counter had a bug that no unit test caught: it said the family emoji πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ is 11 characters. Users see one. If you build anything with a character limit (captions, bios, SMS, form fields), you probably have th

Our word counter had a bug that no unit test caught: it said the family emoji πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ is 11 characters. Users see one. If you build anything with a character limit (captions, bios, SMS, form fields), you probably have the same bug, because the default way to count a string in JavaScript counts something users never see.

The test

We ran seven strings through four ways of counting:

Text .length (UTF-16) [...s].length (code points) Graphemes (what users see) UTF-8 bytes
Free tools for photos and PDF 29 29 29 29
Photo ka size kam kaise karein 30 30 30 30
Ψͺءویر Ϊ©Ψ§ Ψ³Ψ§Ψ¦Ψ² Ϊ©Ω… Ϊ©ΫŒΨ³Ϋ’ کریں (Urdu) 26 26 26 47
Eid Mubarak πŸŒ™βœ¨πŸŽ‰ 17 15 15 23
Great job πŸ‘πŸ½πŸ‘πŸ½ 18 14 12 26
Family time πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ 23 19 13 37
Pakistan πŸ‡΅πŸ‡° zindabad 22 20 19 26

Plain English and Roman Urdu are identical whichever way you count. Everything else drifts.

What is going on

  • .length counts UTF-16 code units. Emoji outside the Basic Multilingual Plane take two units each (a surrogate pair), so πŸŒ™ is 2.
  • [...s] counts code points. Better, but many visible characters are built from several code points:
    • πŸ‘πŸ½ = πŸ‘ + a skin-tone modifier (2 code points)
    • πŸ‡΅πŸ‡° = two "regional indicator" letters, P and K
    • πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ = four people joined by three invisible zero-width joiners: 7 code points, 11 UTF-16 units
  • Graphemes are what a person would call "one character". Unicode calls them extended grapheme clusters.
  • Bytes matter for storage and some limits. Urdu letters take 2 bytes each in UTF-8, so 26 characters became 47 bytes.

The fix: Intl.Segmenter

Every current browser and Node.js has a grapheme segmenter built in:

const seg = 'Segmenter' in Intl ? new Intl.Segmenter() : null;
const chars = s => seg ? [...seg.segment(s)].length : [...s].length;

chars('Family time πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦'); // 13
chars('Great job πŸ‘πŸ½πŸ‘πŸ½');      // 12

The fallback to code points keeps very old browsers usable without bringing in a library.

Two details from our version:

  • Characters without spaces: run the same function on s.replace(/\s/g, ''), so emoji are still counted once.
  • Words: count with a Unicode-aware regex such as /[\p{L}\p{N}][\p{L}\p{M}\p{N}]*/gu. The \p{M} part keeps Urdu and Hindi combining marks inside the word instead of splitting it.

Which number should you show?

  • Users typing against a limit you control: graphemes. That is what they can see and edit.
  • A third-party limit: match how that platform counts. For example, X weights most emoji as 2, and SMS switches from 160 to 70 characters per message as soon as one non-GSM character (such as Urdu) appears.
  • Database columns and byte limits (Amazon's 249-byte search terms, some APIs): UTF-8 bytes via new TextEncoder().encode(s).length.

Try it

The fixed counter is free and runs entirely in the browser: word counter with characters, reading and speaking time. There is also a plain character counter, and for byte-based limits the Amazon character counter shows both characters and bytes.

πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.