Count Words in a String
Word counting drives reading-time estimates, character-budget warnings, and SEO meta-checks. The naive `str.split(' ').length` overcounts double spaces, miscounts empty input, and ignores non-Latin scripts entirely. This snippet starts with a regex-based whitespace split, hardens it against empty and whitespace-only input, then upgrades to `Intl.Segmenter` for locale-aware counting in Chinese, Japanese, and Thai where there are no spaces.
827 views
22
function wordCount(str) {
if (typeof str !== 'string') return 0;
return str.trim().split(/\s+/).filter(Boolean).length;
}
console.log(wordCount('Hello world')); // 2
console.log(wordCount(' Many spaces between ')); // 3
console.log(wordCount('')); // 0
console.log(wordCount(' ')); // 0Splitting on /\s+/ collapses any run of whitespace (spaces, tabs, newlines) into a single delimiter, which fixes the naive split(' ') overcount on double spaces. The leading trim() strips surrounding whitespace so the split does not produce a phantom empty leading entry, and the filter(Boolean) is a belt-and-braces against the rare case where split still emits an empty string. Empty and whitespace-only input correctly return 0 because trim() reduces them to '' and ''.split(/\s+/) yields [''], which the filter removes. Use this for English-style text where words are space-separated.
function wordCountStrict(str) {
if (typeof str !== 'string') return 0;
const matches = str.match(/[\p{L}\p{N}]+(?:['\u2019\-][\p{L}\p{N}]+)*/gu);
return matches ? matches.length : 0;
}
console.log(wordCountStrict('Hello, world!')); // 2
console.log(wordCountStrict("don't can't won't")); // 3 (apostrophes kept as one word)
console.log(wordCountStrict('state-of-the-art design')); // 2 (hyphenated as one)
console.log(wordCountStrict('Naïve résumé café 2025')); // 4When the input has heavy punctuation, the whitespace split can include trailing commas or quotes inside its tokens; a stricter approach matches what a word actually is. The pattern [\p{L}\p{N}]+(?:['\u2019\-][\p{L}\p{N}]+)* with the u flag uses Unicode property classes: \p{L} for any letter (Latin, Cyrillic, Greek, Hebrew), \p{N} for any digit. The non-capturing group lets 'don\u2019t' and 'state-of-the-art' count as one word each, which matches user intuition. Reach for this when accuracy matters more than simplicity, like word-count limits in a CMS.
function wordCountLocale(str, locale = 'en') {
if (typeof str !== 'string' || str.length === 0) return 0;
const seg = new Intl.Segmenter(locale, { granularity: 'word' });
let count = 0;
for (const { isWordLike } of seg.segment(str)) {
if (isWordLike) count++;
}
return count;
}
console.log(wordCountLocale('Hello, world!')); // 2
// Japanese: '私は東京に住んでいます' has no spaces
console.log(wordCountLocale('私は東京に住んでいます', 'ja')); // 8 on Node 22 (varies by runtime: segmenter rules evolve)
// Chinese: spaces also absent
console.log(wordCountLocale('我喜欢编程', 'zh')); // 4 on Node 22 (varies by runtime)
console.log(wordCountLocale(' ', 'en')); // 0Intl.Segmenter with granularity: 'word' does what the regex versions cannot: it segments scripts that do not delimit words with whitespace. Japanese, Chinese, and Thai sentences are written as continuous strings, so any whitespace-based count reports 1 (or 0) regardless of how many words are actually present. The isWordLike flag filters out the punctuation and whitespace segments that the segmenter also returns, leaving only word entries. The exact count for a given Japanese sentence depends on the locale's segmentation rules, which is why the comments say ~6 rather than an exact number. Use this whenever your app accepts CJK or Thai input.
