1 The Problem
We want a small program that converts a temperature from Celsius to Fahrenheit, or the other way around. The user picks which direction, types a number, and the program does the maths and shows the answer. Simple — but it teaches the core shape of every calculator-style tool: ask, calculate, show.
2 How to Think About It
Before any code, think about the two things the program needs to know and the two formulas it might use. You do not need Kotlin yet — just the plan:
3 The Build — explained part by part
Here is the complete converter. Read each part’s note below — you should understand the whole thing from the notes alone.
import java.io.PrintStream
/** Converts Celsius to Fahrenheit. */
fun cToF(celsius: Double): Double = celsius * 9 / 5 + 32
/** Converts Fahrenheit to Celsius. */
fun fToC(fahrenheit: Double): Double = (fahrenheit - 32) * 5 / 9
fun main() {
// Without this, a JVM started under a non-UTF-8 locale (common on bare
// Linux servers and minimal Docker images) silently turns "°" into "?".
System.setOut(PrintStream(System.out, true, "UTF-8"))
println("Temperature Converter")
println("1. Celsius to Fahrenheit")
println("2. Fahrenheit to Celsius")
print("Choose 1 or 2: ")
val choice = readLine()?.trim()
when (choice) {
"1" -> {
print("Enter temperature in Celsius: ")
val celsius = readLine()?.trim()?.toDoubleOrNull()
if (celsius == null) {
println("Please type a number.")
} else {
println("${celsius}°C is ${cToF(celsius)}°F")
}
}
"2" -> {
print("Enter temperature in Fahrenheit: ")
val fahrenheit = readLine()?.trim()?.toDoubleOrNull()
if (fahrenheit == null) {
println("Please type a number.")
} else {
println("${fahrenheit}°F is ${fToC(fahrenheit)}°C")
}
}
else -> println("Please choose 1 or 2.")
}
}kotlinc on your own machine instead; the “Run It” section explains exactly how.fun cToF(celsius: Double): Double says “takes a Double, returns a Double” — the compiler checks every call site against that promise, and a single-expression body (= celsius * 9 / 5 + 32) skips the braces entirely.readLine()?.trim() — Kotlin’s
readLine() is a plain, blocking call built into the standard library: no callback, no async iterator, no event to wire up. It returns String? (a nullable string) because input can run out, which is exactly why the ? is there.?.toDoubleOrNull() — converts the typed text to a number the same way Python’s
float() does, except a bad string never throws: it quietly becomes null instead, chained safely through the ?. from the nullable readLine() result.when (choice) { "1" -> ... } — Kotlin’s
when is a more capable switch: each branch can be a whole block, and else plays the role of default.System.setOut(PrintStream(System.out, true, "UTF-8")) — a defensive line explained fully in “If it breaks” below: without it, the
° symbol silently turns into ? on a JVM started under a non-UTF-8 locale.
readLine() can never return null, and calling .trim() on it directly.readLine(): String? is nullable by design — input can run out (end of file). The ?. safe-call operator short-circuits to null instead of crashing when that happens."abc".toDouble() to behave like a safe parse.toDouble() throws NumberFormatException on bad input. Use toDoubleOrNull(), which returns null instead, and check for that before using the value.° symbol (or any non-ASCII character) and trusting it will always show up correctly.?. Setting System.out to an explicit UTF-8 PrintStream at the start of main fixes this regardless of the host's locale — see “If it breaks” below for the real, reproduced bytes.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
import kotlin.test.assertTrue
class ConverterTest {
@Test
fun freezingPoint() {
assertEquals(32.0, cToF(0.0))
}
@Test
fun boilingPoint() {
assertEquals(212.0, cToF(100.0))
}
@Test
fun reverseFreezing() {
assertEquals(0.0, fToC(32.0))
}
@Test
fun roundTrip() {
assertTrue(Math.abs(fToC(cToF(25.0)) - 25.0) < 0.00001)
}
}Compile with kotlinc converter.kt converter_test.kt -include-runtime -d converter.jar (adding kotlin-test.jar and kotlin-test-junit.jar to the classpath) and run with JUnit’s own runner. We pulled the formulas into small, independently-typed functions (cToF, fToC) so the tests can call them directly, without any menu or typed input in the way.
5 The Interface
The program’s interface — what it asks for and what it gives back — documented plainly.
What it expects
Choose 1 or 2: 1
Enter temperature in Celsius: 100What it returns
100.0°C is 212.0°F6 Run It & Automate It
Save the code as converter.kt and compile it with kotlinc converter.kt -include-runtime -d converter.jar, which bundles the Kotlin runtime so the result runs with a plain java -jar.
kotlinc converter.kt -include-runtime -d converter.jar && java -jar converter.jarPick a direction, type a temperature, and see the conversion.
A CI tool like Jenkins compiles and tests automatically whenever the code changes — every line below has a plain explanation.
Temperature Converter
1. Celsius to Fahrenheit
2. Fahrenheit to Celsius
Choose 1 or 2: 1
Enter temperature in Celsius: 100
100.0°C is 212.0°F100.0?C is 212.0?F — the ° symbol turns into a question marklocale in the same shell, and if it prints POSIX or C instead of something ending in UTF-8, the JVM's default console charset cannot represent ° and silently substitutes ? — confirmed on this exact sandbox by inspecting the raw output bytes (0x3F, a literal question mark, not a display artifact). System.setOut(PrintStream(System.out, true, "UTF-8")) at the top of main, as this program does, fixes it for good regardless of the host's locale." 100 ".toDoubleOrNull() is fine once trimmed, but "100cm".toDoubleOrNull() is null.// 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 converter.kt converter_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 ConverterTest'
}
}
}
post {
success { echo 'All tests passed.' }
failure { echo 'A test failed — look above.' }
}
}
You have a working temperature converter. Extend it:
- Add Kelvin. Offer Celsius→Kelvin too (Kelvin = Celsius + 273.15). (Teaches: a third
whenbranch.) - Round differently. Use
"%.1f".format(value)for a fixed one-decimal answer. (Teaches: Kotlin's string formatting.) - Keep converting. Wrap the whole thing in a loop so the user can do many conversions without restarting. (Teaches: an outer
while (true)loop.) - Guard bad input harder. Instead of giving up on a bad number, loop back and ask again. (Teaches: retry logic with
while.)
String?, ?., toDoubleOrNull()) for input that might be missing or malformed, and why console output needs an explicit encoding on some hosts. Related reference: Null Safety & Types, Variables & Types.