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 →