Skip to content

Text Statistics & Readability — Kotlin source

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

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

// text-stats — text statistics & readability: chars, words, sentences, syllables, Flesch. Language: Kotlin (JVM 1.9+). 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.Locale

object TextStatsTool {
    private val WORD_RE = Regex("[A-Za-z0-9'’-]+") // U+2019 keeps contractions whole
    private val SENTENCE_RE = Regex("[.!?]+(\\s|$)")
    private val PARA_SEP_RE = Regex("\\n{2,}")

    /** Full analysis — readability stays null when words or sentences are too few. */
    data class TextStats(
        val characters: Int, val charactersNoSpaces: Int, val words: Int, val sentences: Int,
        val paragraphs: Int, val lines: Int, val syllables: Int, val readingTimeMs: Int, val speakingTimeMs: Int,
        val fleschReadingEase: Double? = null, val fleschKincaidGrade: Double? = null, val readabilityLabel: String? = null)

    /** Syllables in one word via the vowel-group heuristic. */
    fun countSyllables(word: String): Int {
        val w = word.lowercase(Locale.ROOT).filter { it in 'a'..'z' }
        if (w.isEmpty()) return 0
        if (w.length <= 3) return 1
        var s = w.replace(Regex("(?:[^laeiouy]es|ed|[^laeiouy]e)$"), "") // silent trailing e ('~le' keeps its syllable)
        s = s.replaceFirst(Regex("^y"), "")
        return maxOf(1, Regex("[aeiouy]+").findAll(s).count())
    }

    fun analyze(input: String?): TextStats {
        val text = input ?: ""
        val words = WORD_RE.findAll(text).map { it.value }.toList()
        val sentences = if (words.isEmpty()) 0 else maxOf(1, SENTENCE_RE.findAll(text).count())
        val paragraphs = if (text.isBlank()) 0 else PARA_SEP_RE.split(text).count { it.isNotBlank() }
        val lines = if (text.isEmpty()) 0 else text.split('\n').size
        val syllables = words.sumOf { countSyllables(it) }
        val readingMs = Math.round(words.size / 200.0 * 60000).toInt()  // 200 wpm
        val speakingMs = Math.round(words.size / 130.0 * 60000).toInt() // 130 wpm
        var fre: Double? = null; var fkg: Double? = null; var label: String? = null
        if (words.isNotEmpty() && sentences > 0) {
            val wps = words.size.toDouble() / sentences
            val spw = syllables.toDouble() / words.size
            val f1 = Math.round((206.835 - 1.015 * wps - 84.6 * spw) * 10) / 10.0
            val f2 = Math.round((0.39 * wps + 11.8 * spw - 15.59) * 10) / 10.0
            fre = f1; fkg = f2
            label = when {
                f1 >= 80 -> "Very Easy"; f1 >= 70 -> "Easy"; f1 >= 60 -> "Standard"
                f1 >= 50 -> "Fairly Hard"; f1 >= 30 -> "Hard"; else -> "Very Hard"
            }
        }
        return TextStats(text.length, Regex("\\s").replace(text, "").length, words.size, sentences,
            paragraphs, lines, syllables, readingMs, speakingMs, fre, fkg, label)
    }
}

fun main() {
    for (text in listOf("The quick brown fox jumps over the lazy dog.",
                        "Hi.\n\nMy name is Inigo Montoya. You killed my father; prepare to die!")) {
        val s = TextStatsTool.analyze(text)
        val flesch = s.readabilityLabel?.let {
            String.format(Locale.ROOT, "%.1f (%s)", s.fleschReadingEase, it) } ?: "n/a"
        val grade = s.fleschKincaidGrade?.let { String.format(Locale.ROOT, "%.1f", it) } ?: "n/a"
        println("chars=${s.characters} nospace=${s.charactersNoSpaces} words=${s.words} " +
                "sentences=${s.sentences} paragraphs=${s.paragraphs} lines=${s.lines} " +
                "syllables=${s.syllables} reading=${s.readingTimeMs}ms speaking=${s.speakingTimeMs}ms " +
                "flesch=$flesch grade=$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 →