Skip to content

Text Statistics & Readability — Java source

Count words, sentences, paragraphs, characters, lines, and reading time, plus Flesch Reading Ease and Flesch-Kincaid grade-level readability scores.

This is the Java implementation — the same logic the interactive tool runs, in a shareable, citable form.

// text-stats — text statistics & readability: chars, words, sentences, syllables, Flesch. Language: Java (17+). Port of src/lib/textStats.ts — String.length() counts UTF-16 code units exactly like the TS reference (\s is ASCII-only in Java regex).
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/** Text statistics & readability scoring, ported from the canonical TS lib. */
public final class TextStatsTool {
    /** Full analysis — readability fields stay null when words or sentences are too few. */
    public record TextStats(int characters, int charactersNoSpaces, int words, int sentences, int paragraphs,
                            int lines, int syllables, int readingTimeMs, int speakingTimeMs,
                            Double fleschReadingEase, Double fleschKincaidGrade, String readabilityLabel) {}

    static final Pattern WORD_RE = Pattern.compile("[A-Za-z0-9'’-]+"); // U+2019 keeps contractions whole
    static final Pattern SENTENCE_RE = Pattern.compile("[.!?]+(\\s|$)");
    static final Pattern PARA_SEP_RE = Pattern.compile("\\n{2,}");

    /** Syllables in one word via the vowel-group heuristic. */
    public static int countSyllables(String word) {
        StringBuilder b = new StringBuilder();
        for (char c : word.toLowerCase(Locale.ROOT).toCharArray())
            if (c >= 'a' && c <= 'z') b.append(c);
        String w = b.toString();
        if (w.isEmpty()) return 0;
        if (w.length() <= 3) return 1;
        String s = w.replaceAll("(?:[^laeiouy]es|ed|[^laeiouy]e)$", ""); // silent trailing e ('~le' keeps its syllable)
        s = s.replaceFirst("^y", "");
        int groups = 0;
        for (Matcher m = Pattern.compile("[aeiouy]+").matcher(s); m.find(); ) groups++;
        return Math.max(1, groups);
    }

    public static TextStats analyze(String input) {
        String text = input == null ? "" : input;
        List<String> words = new ArrayList<>();
        Matcher m = WORD_RE.matcher(text);
        while (m.find()) words.add(m.group());
        int sentences = words.isEmpty() ? 0
            : Math.max(1, (int) SENTENCE_RE.matcher(text).results().count());
        int paragraphs = text.strip().isEmpty() ? 0
            : (int) Arrays.stream(PARA_SEP_RE.split(text)).map(String::strip).filter(p -> !p.isEmpty()).count();
        int lines = text.isEmpty() ? 0 : text.split("\n", -1).length;
        int syllables = 0;
        for (String w : words) syllables += countSyllables(w);
        int readingMs = (int) Math.round(words.size() / 200.0 * 60000);  // 200 wpm
        int speakingMs = (int) Math.round(words.size() / 130.0 * 60000); // 130 wpm
        Double fre = null, fkg = null;
        String label = null;
        if (!words.isEmpty() && sentences > 0) {
            double wps = (double) words.size() / sentences, spw = (double) syllables / words.size();
            fre = Math.round((206.835 - 1.015 * wps - 84.6 * spw) * 10) / 10.0;
            fkg = Math.round((0.39 * wps + 11.8 * spw - 15.59) * 10) / 10.0;
            label = fre >= 80 ? "Very Easy" : fre >= 70 ? "Easy" : fre >= 60 ? "Standard"
                : fre >= 50 ? "Fairly Hard" : fre >= 30 ? "Hard" : "Very Hard";
        }
        return new TextStats(text.length(), text.replaceAll("\\s", "").length(), words.size(), sentences,
            paragraphs, lines, syllables, readingMs, speakingMs, fre, fkg, label);
    }

    public static void main(String[] args) {
        for (String text : new String[] {
                "The quick brown fox jumps over the lazy dog.",
                "Hi.\n\nMy name is Inigo Montoya. You killed my father; prepare to die!"}) {
            TextStats s = analyze(text);
            String flesch = s.readabilityLabel() != null
                ? String.format(Locale.ROOT, "%.1f (%s)", s.fleschReadingEase(), s.readabilityLabel()) : "n/a";
            String grade = s.fleschKincaidGrade() != null
                ? String.format(Locale.ROOT, "%.1f", s.fleschKincaidGrade()) : "n/a";
            System.out.printf(Locale.ROOT,
                "chars=%d nospace=%d words=%d sentences=%d paragraphs=%d lines=%d syllables=%d " +
                "reading=%dms speaking=%dms flesch=%s grade=%s%n",
                s.characters(), s.charactersNoSpaces(), s.words(), s.sentences(), s.paragraphs(),
                s.lines(), s.syllables(), s.readingTimeMs(), s.speakingTimeMs(), flesch, grade);
        }
    }
}

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 →