Text Statistics & Readability — TypeScript source
Count words, sentences, paragraphs, characters, lines, and reading time, plus Flesch Reading Ease and Flesch-Kincaid grade-level readability scores.
This is the TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Pure text-statistics & readability logic - no React, no DOM, deterministic.
// Counts characters, words, sentences, paragraphs, lines, syllables; computes
// reading/speaking time and Flesch readability scores. Never throws.
export interface TextStats {
characters: number;
charactersNoSpaces: number;
words: number;
sentences: number;
paragraphs: number;
lines: number;
syllables: number;
readingTimeMs: number; // words / 200 wpm
speakingTimeMs: number; // words / 130 wpm
fleschReadingEase: number | null;
fleschKincaidGrade: number | null;
readabilityLabel: string | null;
}
/** Count syllables in a single word via vowel-group heuristic. */
export function countSyllables(word: string): number {
const w = word.toLowerCase().replace(/[^a-z]/g, '');
if (!w) return 0;
if (w.length <= 3) return 1;
// Drop silent trailing e, but keep words ending in 'le' after a consonant.
let s = w.replace(/(?:[^laeiouy]es|ed|[^laeiouy]e)$/, '');
s = s.replace(/^y/, '');
const groups = s.match(/[aeiouy]{1,}/g);
const count = groups ? groups.length : 1;
return Math.max(1, count);
}
function labelForFlesch(f: number): string {
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';
}
export function analyzeText(input: string): TextStats {
const text = input ?? '';
const characters = text.length;
const charactersNoSpaces = text.replace(/\s/g, '').length;
const wordList = text.match(/[A-Za-z0-9''-]+/g) ?? [];
const words = wordList.length;
const sentences = words === 0 ? 0 : Math.max(1, (text.match(/[.!?]+(?:\s|$)/g) ?? []).length);
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);
let fleschReadingEase: number | null = null;
let fleschKincaidGrade: number | null = null;
let readabilityLabel: string | null = 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 →