Kotlin Module 2 – Values, types and control flow
Parse text and format Indian numbers
Split and parse text with toIntOrNull and readlnOrNull, check formats with Regex, and print paise with the lakh and crore grouping NumberFormat lacks.
What you will learn
- Split, trim and parse text with toIntOrNull, toLongOrNull and readlnOrNull without stopping on bad input
- Validate a simple format with Regex, and choose between matches, containsMatchIn and find
- Explain why NumberFormat for en-IN prints 123,456,789 on JDK 25
- Format paise with lakh and crore grouping for every Long, negative amounts included
- Parse amounts that people type into paise, and reject digits and formats the program cannot trust
Before you start
What you’ll learn · Before you start
What you will learn
- Split, trim and parse text with toIntOrNull, toLongOrNull and readlnOrNull without stopping on bad input
- Validate a simple format with Regex, and choose between matches, containsMatchIn and find
- Explain why NumberFormat for en-IN prints 123,456,789 on JDK 25
- Format paise with lakh and crore grouping for every Long, negative amounts included
- Parse amounts that people type into paise, and reject digits and formats the program cannot trust
Before you start
On this page
"12a".toInt() stops a program with a NumberFormatException, while "12a".toIntOrNull() returns null and lets it
carry on. Text from people and files is full of such values, and on JDK 25 even the formatter for English in India
prints 123,456,789 where readers in India write 12,34,56,789.
This lesson shows how to parse text without letting one bad value stop the program, how to check a format with
Regex and where regular expressions stop helping, and how to format Indian numbers, which the JDK’s own formatter
does not do.
Read text line by line, then parse it with the OrNull functions
Every parsing function has two forms. toInt() throws when the text is not a number; toIntOrNull() returns null
instead. This program feeds both the same seven texts:
// The same texts parsed two ways: toIntOrNull() returns null, toInt() throws.
fun main() {
for (text in listOf("42", "+42", " 42", "12a", "1,234", "2147483648", "")) {
val strict =
try {
text.toInt().toString()
} catch (e: NumberFormatException) {
"NumberFormatException"
}
println("\"$text\": toIntOrNull() = ${text.toIntOrNull()}, toInt() = $strict")
}
println("\" 42 \".trim().toIntOrNull() = ${" 42 ".trim().toIntOrNull()}")
println("\"2147483648\".toLongOrNull() = ${"2147483648".toLongOrNull()}")
} Output
"42": toIntOrNull() = 42, toInt() = 42 "+42": toIntOrNull() = 42, toInt() = 42 " 42": toIntOrNull() = null, toInt() = NumberFormatException "12a": toIntOrNull() = null, toInt() = NumberFormatException "1,234": toIntOrNull() = null, toInt() = NumberFormatException "2147483648": toIntOrNull() = null, toInt() = NumberFormatException "": toIntOrNull() = null, toInt() = NumberFormatException " 42 ".trim().toIntOrNull() = 42 "2147483648".toLongOrNull() = 2147483648
Recorded with Kotlin 2.4.20 on OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: kotlinc Parse.kt -include-runtime -d main.jar && java -jar main.jar
A leading + or - is accepted, but a space, a comma or a letter is not, and neither is a number beyond the type:
2147483648 does not fit in an Int, so toIntOrNull() gave null and toLongOrNull() gave the number. Trim first,
and choose the type by the largest value the text may hold.
The OrNull functions are the safe way to parse text that people type or that files contain. This program reads orders,
one per line, from its standard input, the file orders.txt:
// Reads orders as "item,quantity,price in paise", one per line, until the input ends.
fun main() {
var total = 0L
var lineNumber = 0
var accepted = 0
while (true) {
val line = readlnOrNull() ?: break
lineNumber++
val fields = line.split(",").map { it.trim() }
if (fields.size != 3) {
println("line $lineNumber rejected: ${fields.size} fields instead of 3")
continue
}
val (item, quantityText, priceText) = fields
val quantity = quantityText.toIntOrNull()
val price = priceText.toLongOrNull()
when {
quantity == null -> println("line $lineNumber rejected: quantity \"$quantityText\" is not a whole number")
price == null -> println("line $lineNumber rejected: price \"$priceText\" is not a whole number")
else -> {
total += quantity * price
accepted++
println("$item: $quantity × $price paise = ${quantity * price} paise")
}
}
}
println("Total: $total paise from $accepted of $lineNumber lines")
} Input (standard input)
chai,3,2500 samosa,two,2000 vada pav , 2 , 3000 lassi,1,
Output
chai: 3 × 2500 paise = 7500 paise line 2 rejected: quantity "two" is not a whole number vada pav: 2 × 3000 paise = 6000 paise line 4 rejected: price "" is not a whole number Total: 13500 paise from 2 of 4 lines
Recorded with Kotlin 2.4.20 on OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: kotlinc Totals.kt -include-runtime -d main.jar && java -jar main.jar
Step through what happened to each of the four lines:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Line 1. readlnOrNull() returns chai,3,2500. split(",") gives three fields, and toIntOrNull() and toLongOrNull() turn 3 and 2500 into numbers.
- Line 1 is added. 3 × 2500 is 7500 paise, which goes into the total, and the loop asks for the next line.
- Line 2: "two". samosa,two,2000 splits into three fields, but "two".toIntOrNull() is null. The line is rejected with its reason, and the program carries on instead of stopping.
- Line 3: spaces. The fields of " vada pav , 2 , 3000" carry spaces, which toIntOrNull() would refuse. trim() removes them first, so 2 × 3000 adds 6000 paise.
- Line 4: an empty price. lassi,1, ends with an empty field. "".toLongOrNull() is null, so this line is rejected too, with the reason the run printed.
- The end of the input. readlnOrNull() returns null when the input ends, and the loop stops. The program prints 13500 paise from 2 of 4 lines, where readln() would have thrown at the end instead.
readlnOrNull() returns one line, or null when the input has ended, which ends the loop; readln() would have
thrown an exception there instead. Each rejected line gets its reason and the rest carry on.
Checkpoint
Check: Parsing text with the OrNull functions
Regex checks a format, and matches tests the whole text
A regular expression: A pattern that describes a set of texts, such as [1-9][0-9]{5} for six digits not starting with 0. Kotlin's Regex compiles one and tests or searches text with it. describes the shape of valid text. This lesson’s own rule
for a postal code, six digits with a first digit that is not 0, is [1-9][0-9]{5}:
// This lesson's own rule for a postal code: six digits, the first one not 0.
val postalCode = Regex("[1-9][0-9]{5}")
fun reason(text: String): String =
when {
postalCode.matches(text) -> "ok"
text.length != 6 -> "needs 6 characters, has ${text.length}"
text.startsWith("0") -> "may not start with 0"
else -> "only the digits 0 to 9 are allowed"
}
fun main() {
for (text in listOf("560001", "56001", "060001", "56000A", "5600011")) {
println("$text: ${reason(text)}")
}
val address = "Deliver to 560001, Bengaluru"
println("matches: ${postalCode.matches(address)}")
println("containsMatchIn: ${postalCode.containsMatchIn(address)}")
val found = postalCode.find(address)
println("find: ${found?.value} at ${found?.range}")
} Output
560001: ok 56001: needs 6 characters, has 5 060001: may not start with 0 56000A: only the digits 0 to 9 are allowed 5600011: needs 6 characters, has 7 matches: false containsMatchIn: true find: 560001 at 11..16
Recorded with Kotlin 2.4.20 on OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: kotlinc PostalCode.kt -include-runtime -d main.jar && java -jar main.jar
matches asks whether the pattern covers the whole text, so "5600011" failed: it has one digit too many.
containsMatchIn asks whether the pattern occurs anywhere, which is true for the address, and find returns the first
occurrence with its position, 560001 at 11 to 16. Use matches to validate a field and find to pick a value out of
longer text. Write patterns as raw strings, so a backslash such as the one in \d needs no second backslash.
A regular expression checks shape, not meaning: 999999 passes this pattern whether or not any address uses it, and
a pattern of two digits, a slash and two digits accepts 99/99 as readily as a real value. Check the shape with Regex, then
convert and check the value in code.
Checkpoint
Check: Checking a format with Regex
Indian digit grouping needs a formatter of its own
Large numbers are written with separators between groups of digits, and this digit grouping: The separators placed between groups of digits in a large number. International grouping uses threes, 123,456,789; Indian grouping keeps the last three and pairs the rest, 12,34,56,789. is not in threes everywhere:
| Number | International grouping | Indian grouping |
|---|---|---|
100000 |
100,000 |
1,00,000 |
10000000 |
10,000,000 |
1,00,00,000 |
123456789 |
123,456,789 |
12,34,56,789 |
Lakh and crore
In India the last three digits stay together and the digits before them go in pairs. A lakh: One hundred thousand in the Indian numbering system, written 1,00,000. Ten lakh make a million., written 1,00,000, is a hundred thousand; a crore: Ten million in the Indian numbering system: a hundred lakh, written 1,00,00,000., written 1,00,00,000, is ten million. ₹12,34,567.50 therefore reads as twelve lakh, thirty-four thousand, five hundred and sixty-seven rupees and fifty paise.
The JDK has a locale: A language together with a region, such as en-IN for English as used in India. Formatting functions use it to choose separators, symbols and digit grouping. for English as used in India, en-IN. This program asks its formatters
for that grouping:
import java.text.NumberFormat
import java.util.Locale
// What the JDK's own formatters do with English as used in India.
fun main() {
val india = Locale.of("en", "IN")
println("number: ${NumberFormat.getNumberInstance(india).format(123456789)}")
println("currency: ${NumberFormat.getCurrencyInstance(india).format(1234567.5)}")
println("format: ${String.format(india, "%,d", 123456789)}")
println("en-US: ${NumberFormat.getNumberInstance(Locale.US).format(123456789)}")
} Output
number: 123,456,789 currency: ₹1,234,567.50 format: 123,456,789 en-US: 123,456,789
Recorded with Kotlin 2.4.20 on OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: kotlinc Grouping.kt -include-runtime -d main.jar && java -jar main.jar
All three printed groups of three. The reason is in the documentation of DecimalFormat, which NumberFormat uses:
it has one grouping size, and when a pattern contains several grouping separators, only the interval after the last
one counts, here 3. So to format Indian numbers correctly, a program needs one of two things:
- A formatter of its own: about ten lines of Kotlin that group the digits of whole paise as text.
- ICU4J, the Unicode project’s library, whose
DecimalFormathas a second grouping size:#,##,###prints 123456789 as 12,34,56,789.
Key idea
Parse with the OrNull functions and give every rejected value its reason, check shapes with Regex.matches, and do
not trust NumberFormat for Indian grouping: it printed 123,456,789 for en-IN.
Checkpoint
Check: Indian grouping and NumberFormat
A formatter for lakh and crore, tested on the edges Pro: opens with a Pro, Premium or Ultimate pass
Formatting whole paise the Indian way is a few lines of work on the digits as text. This part writes the grouping, reproduces the one amount that breaks the obvious version, and leaves you to fix it.
In this part:
- Grouping digits the Indian way, step by step
- The smallest Long breaks the obvious version, reproduced
- Formatting at the edge, and the inputs that guard it
- Practise: a formatter that is right for every Long
1 diagram · 2 runnable examples (Kotlin) · about 6 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Parsing amounts people type, and where Regex stops helping Pro: opens with a Pro, Premium or Ultimate pass
Amounts arrive as text such as 1,234.50, ₹ 99 or 12,34,567.5, and a payment that reads the wrong number is
worse than one that refuses. This part parses typed amounts into paise and shows two traps of the parsing functions
that a format check has to catch first.
In this part:
- Digits that parse but do not match, measured
- From typed text to paise, with the numbers
- Limits before patterns, and messages a person can act on
2 runnable examples (Kotlin) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Practise: predict what parses
Change Parse.kt and predict first
Copy Parse.kt and add "-0", "007" and "1_000" to its list. Write down what toIntOrNull() will give for
each before you run it with kotlinc Parse.kt -include-runtime -d parse.jar && java -jar parse.jar.
Finish with questions on parsing, patterns and grouping, asked weakest idea first, with the reason for every choice:
Practice that adapts to you
Practise this lesson
9 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
-
Fresher · Warm-up
What is the difference between toInt() and toIntOrNull() in Kotlin, and when do you use each?
toInt()parses a string into anIntand throws aNumberFormatExceptionwhen the text is not a valid number:"12a"," 42"with its space,"1,234", or a number beyond theIntrange such as"2147483648".toIntOrNull()accepts exactly the same texts but returnsnullinstead of throwing, so the caller decides what a bad value means. UsetoIntOrNull()for text that people type or files contain, where bad values are expected: trim it first, and report everynullwith its reason instead of stopping. KeeptoInt()for text that can only be wrong because of a bug, such as a constant in your own code, so the exception points at that bug. The same pair exists forLongandDouble, and theLongversion takes numbers past 2,147,483,647. -
Mid to senior
A payments form accepts amounts typed as
1,234.50,₹ 99or12,34,567.5. Design a function that turns the text into paise, say what it refuses and why, and explain why callingtoDouble()first is the wrong start. -
Fresher to mid
A finance report formatted with NumberFormat for en-IN shows 12,345,678 where the finance team expects 1,23,45,678. Explain why, and how you would produce Indian grouping reliably on every server, negative amounts included.
-
Mid to senior
A teammate wants one regular expression to validate the dates, amounts and postal codes typed into a form. When is a regular expression the right check, and when would you parse instead? Give an example where a pattern accepts text your program cannot use.
Key takeaways
toInt()throws on text that is not a number andtoIntOrNull()returnsnull, so parse text from people and files with the OrNull functions, aftertrim(), and give every rejected value its reason.readlnOrNull()returnsnullwhen the input ends, whilereadln()throws, so a loop over standard input stops cleanly onnull.Regex.matcheschecks the whole text,containsMatchInandfindlook inside it, and a pattern checks only the shape of a value, never its meaning.- On JDK 25,
NumberFormatforen-INprinted 123,456,789, becauseDecimalFormathas a single grouping size, so Indian grouping needs a formatter of its own or ICU4J.
Terms in this lesson
- crore
- Ten million in the Indian numbering system: a hundred lakh, written 1,00,00,000.
- digit grouping
- The separators placed between groups of digits in a large number. International grouping uses threes, 123,456,789; Indian grouping keeps the last three and pairs the rest, 12,34,56,789.
- lakh
- One hundred thousand in the Indian numbering system, written 1,00,000. Ten lakh make a million.
- locale
- A language together with a region, such as en-IN for English as used in India. Formatting functions use it to choose separators, symbols and digit grouping.
- regular expression
- A pattern that describes a set of texts, such as [1-9][0-9]{5} for six digits not starting with 0. Kotlin's Regex compiles one and tests or searches text with it.
References
- Read standard input (JetBrains (kotlinlang.org))
- kotlin.text: toIntOrNull (JetBrains (kotlinlang.org))
- kotlin.text.Regex (JetBrains (kotlinlang.org))
- Strings: multiline strings for patterns (JetBrains (kotlinlang.org))
- java.util.regex.Pattern (Java SE 25): predefined character classes (Oracle)
- java.text.DecimalFormat (Java SE 25): grouping size (Oracle)
- java.lang.Math (Java SE 25): the Exact methods (Oracle)
- OWASP Cheat Sheet Series: Input Validation (OWASP Foundation)
- ICU4J DecimalFormat: primary and secondary grouping sizes (Unicode Consortium (ICU))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress