Skip to main content

Why Word Counters Disagree: Segmentation and Edge Cases

Two counters, same essay, different numbers — the gap lives in segmentation rules, not rounding.

Updated 6 min readMarkdown

Two counters read the same essay and report different word totals. Neither is broken. They are applying different rules for what counts as a word, and the difference concentrates in a handful of edge cases that ordinary prose hits constantly: apostrophes, hyphens, dashes, decimals, emoji and text that does not separate words with spaces.

Disclosure: I built a free browser word counter at https://toolkitloop.com/word-counter, and the examples below are the ones its own page documents. The tool runs entirely in the browser — pasted text is never sent to a server — which matters precisely because the text people count is usually unpublished.

A word is not "characters between spaces"

The oldest definition in code is also the weakest: split on whitespace, count the tokens. It survives because it is correct on clean, space-separated English prose. It fails the moment real writing shows up, and each failure is a small lesson in linguistics.

Take the apostrophe. don't is one word to every reader, and a whitespace split agrees. Take the hyphen: well-known is one compound to a reader, and the split agrees again. So far, so lucky — two of the hard cases happen to look like single tokens. The luck runs out at the em dash. hello—world is two words to a reader, but there is no space, so a whitespace split reports one. A reader sees a dash doing the work of a boundary; a regex sees a single token. That is not a rounding difference. It is a different theory of the text.

Decimals are the mirror image. 3.14 is one number, one word-like unit, and a split on whitespace gets it right only because the dots are not spaces. A segmenter that split on punctuation would get it wrong. The correct rule is not "split on punctuation" or "split on spaces" — it is "split the way the language splits", which is why the platform API exists.

Intl.Segmenter is the honest default

Browsers now ship Intl.Segmenter, a language-aware segmentation API that encodes the rules above instead of leaving each developer to re-derive them badly:

const segmenter = new Intl.Segmenter(undefined, { granularity: "word" });
let words = 0;
for (const { segment, isWordLike } of segmenter.segment(text)) {
  if (isWordLike) words++;
}

The isWordLike flag does the quiet, important work. It is what keeps an emoji out of the word count: an emoji is a character, it is visible, it carries meaning, and it is not a word. A counter that reports emoji as words is not just technically off; it contradicts what the user sees. Character counts need the same discipline at a different granularity: text.length counts UTF-16 code units, so an emoji outside the basic multilingual plane counts as two, and a compound emoji as several. Grapheme segmentation — granularity: "grapheme" — counts what the user sees. If your counter shows words and characters side by side, the two numbers should be computed with the same seriousness, or users will trust neither.

Locale is the other half of honesty. Passing undefined uses the browser's default locale, which is fine for a general tool, but segmentation is locale-sensitive, and a counter aimed at a specific language should name its locale and document it. Mixed text such as Chinese and English is the stress test: segmentation must not rely on spaces alone, because one half of the input does not use them. A tool that claims to handle mixed-language drafts and then splits on spaces is making a promise its implementation cannot keep.

Sentences and reading time: useful, but labelled as estimates

Word count gets the arguments; sentences and reading time get the shrugs. They deserve more precision about their limits.

Sentence segmentation has to survive abbreviations, decimals and ellipses. Dr. Smith arrived at 3.14 p.m. contains periods that are not sentence boundaries, and any splitter that treats every period as a terminator will invent sentences. Intl.Segmenter with granularity: "sentence" handles the common cases better than a hand-rolled regex, but abbreviation handling is genuinely hard and no segmenter resolves every case. The honest UI presents sentence counts as a structure signal, not a fact.

Reading time is a further step removed: it converts a word count through a words-per-minute assumption that varies by reader, purpose and difficulty. It answers "how long might this take to read", never "is this good". The ToolkitLoop page says exactly that, and it is the right line to copy: reading time estimates time, not quality or completeness.

Paragraphs are the most robust of the secondary numbers, with one decision to document: does a single line break end a paragraph, or only a blank line? Pasted text from different editors disagrees on this, and your answer should be visible somewhere, not buried in code.

Why the destination system always wins

Here is the practical consequence. When two counters disagree, the argument is unresolvable in the abstract, because each is faithful to its own rules. The only count that can decide acceptance is the count of the system that receives the text: the editor, the CMS, the application portal. That is not a cop-out; it is the correct mental model, and it changes how you use any counter.

Use the counter while editing, as a guardrail: paste the exact version you intend to submit, headings and bullets included, watch the range, cut repetition before padding, and add missing explanation rather than filler. Then, for a hard limit, verify in the destination system. A counter that presents itself as the final authority is over-claiming; a counter that tells you to check the destination is telling you the truth about its own limits.

This is also why the local-first shape matters. The input to a word counter is disproportionately unpublished work — essays, applications, client drafts, manuscripts. A server-side counter asks the user to trade privacy for arithmetic that runs perfectly well on their own device. The tool I built keeps counting, goal-setting and limits on the device, with no account and no upload. For this category, that architecture is not a marketing line; it is the difference between a tool a writer can safely paste a draft into and one they should not.

The edge cases worth testing before you ship

If you maintain a counter, run these through it and compare with what your users expect:

  • don't → 1 word; apostrophe stays inside the word.
  • well-known → 1 word; hyphenated compound stays together.
  • hello—world → 2 words; the em dash is a boundary.
  • 3.14 → 1 word; a decimal is one unit.
  • Emoji → 0 words, but counted as characters.
  • Chinese and English mixed text → segmented without relying on spaces alone.

If your implementation gets all six right, it is almost certainly using a real segmenter rather than a split. If it gets the em dash or the mixed-language case wrong, it is a whitespace split wearing a nicer UI.

Which edge case broke your counter first — the dash, the emoji, or the language without spaces? I have found that the answer predicts the next bug report better than any test suite does.

About the author

  • Founder of ToolkitLoop (toolkitloop.com) — 30 free online tools that run 100% in your browser. No sign-ups, no uploads, private by design.

No comments yet. Be the first to comment.