Skip to content

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 →