Text Statistics & Readability — JavaScript source
Count words, sentences, paragraphs, characters, lines, and reading time, plus Flesch Reading Ease and Flesch-Kincaid grade-level readability scores.
This is the JavaScript implementation — the same logic the interactive tool runs, in a shareable, citable form.
// text-stats - JavaScript port
// Language: JavaScript (ES2020+, runs unchanged in Node and modern browsers)
// CosmoDev polyglot showcase. Ported from src/lib/textStats.ts.
// Display source - part of CosmoDev's polyglot tool pages.
//
// Pure text-statistics & readability logic: counts characters, words, sentences,
// paragraphs, lines and syllables, then derives reading/speaking time and the
// Flesch readability scores. Deterministic; never throws.
// NOTE on character counting: this file mirrors src/lib/textStats.ts exactly,
// including JavaScript's UTF-16 code-unit semantics for `string.length`. For
// text outside the Basic Multilingual Plane (e.g. emoji) the count therefore
// differs from a code-point count in the Go/Rust/Python/PHP ports. Real-world
// prose input is unaffected.
/**
* @typedef {Object} TextStats
* @property {number} characters total UTF-16 code units
* @property {number} charactersNoSpaces code units excluding all whitespace
* @property {number} words tokens matched by the word regex
* @property {number} sentences terminal-punctuation boundaries
* @property {number} paragraphs blocks separated by blank lines
* @property {number} lines newline-separated rows
* @property {number} syllables estimated via vowel-group heuristic
* @property {number} readingTimeMs words / 200 wpm
* @property {number} speakingTimeMs words / 130 wpm
* @property {?number} fleschReadingEase null when no words/sentences
* @property {?number} fleschKincaidGrade null when no words/sentences
* @property {?string} readabilityLabel null when no words/sentences
*/
/**
* Count syllables in a single word via a vowel-group heuristic.
*
* True syllabification needs a dictionary; this heuristic is cheap and accurate
* enough to feed the Flesch formulas. It works on the ASCII letters only.
*/
function countSyllables(word) {
// Normalise to lowercase ASCII letters, discarding digits, apostrophes and
// hyphens so "don't" / "well-being" are scored on their letter cores.
const w = word.toLowerCase().replace(/[^a-z]/g, '');
if (!w) return 0;
if (w.length <= 3) return 1;
// Drop a silent trailing 'e', 'es' or 'ed'. The consonant class excludes
// 'l' so that "...le" endings (apple, table) keep their final syllable.
let s = w.replace(/(?:[^laeiouy]es|ed|[^laeiouy]e)$/, '');
// A leading 'y' is a consonant ("yellow"), not a vowel - strip before grouping.
s = s.replace(/^y/, '');
// Each maximal run of vowels counts as one syllable nucleus.
const groups = s.match(/[aeiouy]{1,}/g);
const count = groups ? groups.length : 1;
return Math.max(1, count);
}
/**
* Map a Flesch reading-ease score onto a qualitative label.
* Buckets follow the original Flesch interpretation bands.
*/
function labelForFlesch(f) {
if (f >= 80) return 'Very Easy';
if (f >= 70) return 'Easy';
if (f >= 60) return 'Standard';
if (f >= 50) return 'Fairly Hard';
if (f >= 30) return 'Hard';
return 'Very Hard';
}
/**
* Analyse a string and return its statistics.
*
* Accepts null/undefined (treated as the empty string) and never throws.
* The reading/speaking times use the conventional 200 wpm / 130 wpm rates.
*
* @param {string} [input]
* @returns {TextStats}
*/
function analyzeText(input) {
const text = input ?? '';
const characters = text.length;
const charactersNoSpaces = text.replace(/\s/g, '').length;
// Words are runs of ASCII letters/digits plus apostrophes (both ' and the
// typographic ') and hyphens, so contractions and hyphenated terms stay whole.
const wordList = text.match(/[A-Za-z0-9''-]+/g) ?? [];
const words = wordList.length;
// A sentence ends at terminal punctuation followed by whitespace or EOF.
// With no words there is nothing to terminate; otherwise clamp to >= 1 so a
// word block without any closing punctuation still counts as one sentence.
const sentences =
words === 0 ? 0 : Math.max(1, (text.match(/[.!?]+(?:\s|$)/g) ?? []).length);
// Paragraphs are separated by two or more line breaks. Whitespace-only input
// has zero paragraphs; otherwise trim each block and drop empties.
const paragraphs =
text.trim().length === 0
? 0
: text
.split(/\n{2,}/)
.map((p) => p.trim())
.filter(Boolean).length;
const lines = text === '' ? 0 : text.split('\n').length;
const syllables = wordList.reduce((sum, w) => sum + countSyllables(w), 0);
const readingTimeMs = Math.round((words / 200) * 60000);
const speakingTimeMs = Math.round((words / 130) * 60000);
// Readability needs at least one word and one sentence to be defined.
let fleschReadingEase = null;
let fleschKincaidGrade = null;
let readabilityLabel = null;
if (words > 0 && sentences > 0) {
const wordsPerSentence = words / sentences;
const syllablesPerWord = syllables / words;
fleschReadingEase =
Math.round((206.835 - 1.015 * wordsPerSentence - 84.6 * syllablesPerWord) * 10) / 10;
fleschKincaidGrade =
Math.round((0.39 * wordsPerSentence + 11.8 * syllablesPerWord - 15.59) * 10) / 10;
readabilityLabel = labelForFlesch(fleschReadingEase);
}
return {
characters,
charactersNoSpaces,
words,
sentences,
paragraphs,
lines,
syllables,
readingTimeMs,
speakingTimeMs,
fleschReadingEase,
fleschKincaidGrade,
readabilityLabel,
};
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →