1 The Problem
We want a tool that takes some text — typed in or read from a file — and reports how many words, characters, and lines it has. It teaches the core string operations for breaking text into pieces and measuring them.
2 How to Think About It
Think about what “counting words” really means, before any code:
3 The Build — explained part by part
Here is the complete counter. Read each part’s note below — you should understand the whole thing from the notes alone.
/** Splits on any run of whitespace, mirroring how most languages' split() behaves. */
fun countWords(text: String): Int {
val trimmed = text.trim()
return if (trimmed.isEmpty()) 0 else trimmed.split(Regex("\\s+")).size
}
fun countChars(text: String): Int = text.length
fun main() {
println("Paste your text, then press Enter twice to finish:")
val lines = mutableListOf<String>()
while (true) {
val line = readLine() ?: break
if (line == "") break // an empty line means "done"
lines.add(line)
}
val text = lines.joinToString("\n")
println("Characters: ${countChars(text)}")
println("Words: ${countWords(text)}")
println("Lines: ${lines.size}")
}kotlinc on your own machine instead; the “Run It” section explains exactly how.Regex("\\s+"). Trimming first matters: splitting "" on whitespace gives [""] (one empty “word”), not zero, so we special-case the empty string.while (true) { val line = readLine() ?: break; ... } — reads one line at a time with Kotlin's plain, blocking
readLine(); when input runs out it returns null, and the Elvis operator ?: breaks the loop right there, with no async machinery of any kind involved.if (line == "") break — an empty line (the user pressing Enter twice) signals “done”, exactly as in the Python version.
lines.joinToString("\n") — reassembles the collected lines into one block of text, the same shape
countWords/countChars expect.
text.split(Regex("\\s+")) directly on an empty or all-whitespace string and reporting 1 word instead of 0."" on whitespace always yields [""], never an empty list.text.split(" ").\\s+ treats any run of spaces, tabs, or newlines as one separator.readLine() returning null and returning "" mean different things.null means the input stream itself has ended (e.g. a piped file ran out); an empty string means the user pressed Enter on a blank line. This program treats both as “stop reading”, but they are genuinely different situations worth telling apart in a program that cares.4 Test & Prove Each Part
How do we know this works? We pull the real logic into small, plain functions and check each one against cases we already know the answer to.
import kotlin.test.Test
import kotlin.test.assertEquals
class WordCountTest {
@Test
fun twoWords() {
assertEquals(2, countWords("hello world"))
}
@Test
fun empty() {
assertEquals(0, countWords(""))
}
@Test
fun extraSpaces() {
assertEquals(2, countWords(" hello world "))
}
@Test
fun charCount() {
assertEquals(3, countChars("abc"))
}
}Compile with kotlinc wordcount.kt wordcount_test.kt -include-runtime -d wordcount.jar and run with JUnit's own runner. countWords/countChars take a plain String, so the tests check known inputs directly, without any typed interaction at all.
5 The Interface
What it expects
The quick brown fox
jumps over the lazy dog
[blank line]What it returns
Characters: 43
Words: 9
Lines: 26 Run It & Automate It
Save the code as wordcount.kt and compile it with kotlinc wordcount.kt -include-runtime -d wordcount.jar.
kotlinc wordcount.kt -include-runtime -d wordcount.jar && java -jar wordcount.jarPaste in a block of text, then press Enter on an empty line to see the counts.
A CI tool like Jenkins compiles and tests automatically whenever the code changes — every line below has a plain explanation.
Paste your text, then press Enter twice to finish:
hello world
second line
Characters: 23
Words: 4
Lines: 2// Jenkinsfile — compiles and tests automatically every time the code changes.
pipeline {
agent any // run on any available machine
stages {
stage('Get the code') {
steps { checkout scm } // download the latest code
}
stage('Set up Kotlin') {
steps {
sh 'kotlinc -version' // confirm the compiler is installed
}
}
stage('Compile and test') {
steps {
sh 'kotlinc wordcount.kt wordcount_test.kt -include-runtime -d build.jar' // one real JVM jar, no build tool required
sh 'java -cp build.jar:kotlin-test-junit.jar:junit.jar org.junit.runner.JUnitCore WordCountTest'
}
}
}
post {
success { echo 'All tests passed.' }
failure { echo 'A test failed — look above.' }
}
}
You have a working word counter. Extend it:
- Read from a file argument instead. Accept a filename on the command line rather than typed text. (Teaches:
args: Array<String>andFile(args[0]).readText().) - Count the most frequent word. Build a frequency map and report the top entry. (Teaches:
groupingBy/eachCount, used again in the log analyser project.) - Ignore case and punctuation. Normalise before counting so “Hello” and “hello,” count as the same word. (Teaches:
lowercase()and a cleanup regex.) - Report reading time. Estimate minutes at an average reading speed. (Teaches: simple derived statistics.)
readLine() for line-at-a-time input with no async code anywhere, the Elvis operator ?: for a clean “stop here” when input ends, and Regex for whitespace-aware splitting. Related reference: Collections in Kotlin, Standard Library Deep Dive.