Ex1 : Students Grade
Ex1 : Students Grade
Lets write a program that calculates grades obtained by some students. Each student has a rollNo, name & marks (list of marks in 5 subjects). Given this data of some students, we need to print rollNo. name -> grade
for each student.
Example
Example
Input
Input
RollNo, Name, Marks
11, "Aman", [10, 9, 8, 10, 6]
20, "Yash", [5, 6, 7, 7, 8]
31, "John", [8, 8, 8, 10, 9]
50, "Juliet", [7, 7, 8, 6, 8]
Output
Output
11. Aman -> A
20. Yash -> C
31. John -> A
50. Juliet -> B
Normal approach
Normal approach
We need to save data of multiple students where each student has rollNo
, name
& marks
. rollNo
will have type Int
, so we need List<Int>
to save rollNums
of all students. Similarly we need List<String>
for names
and List<List<Int>>
for marks
:
val rollNums = listOf(11, 20, 31, 50)
val names = listOf("Aman", "Yash", "John", "Juliet")
val marks = listOf(
listOf(10, 9, 8, 10, 6),
listOf(5, 6, 7, 7, 8),
listOf(8, 8, 8, 10, 9),
listOf(7, 7, 8, 6, 8),
)
Now, lets calculate the average marks of each student :
val averageMarks = marks.map { it.average() }
To map average marks to grades, lets define a function gradeFor(marks: Double): String
which will return the grade for given marks :
fun gradeFor(marks: Double): String {
return when (marks) {
in 9.0..10.0 -> "A+"
in 8.0..9.0 -> "A"
in 7.0..8.0 -> "B"
in 6.0..7.0 -> "C"
in 5.0..6.0 -> "D"
else -> "E"
}
}
Finally, we use this function to calculate grades of each student :
val grades = averageMarks.map { gradeFor(it) }
Now, we can print rollNo. name -> grade
for each student :
repeat(rollNums.size) {
println("${rollNums[it]}. ${names[it]} -> ${grades[it]}")
}
Note that we have used separate lists to save each field / attribute of Student entity: rollNums
, names
, marks
. If there are more fields, we’ll need more lists.
Object Oriented approach
Object Oriented approach
Recall that Class is a user defined data type. In this example, Student is a user defined data type. Related to it, it has some data / attributes - rollNo
, name
and marks
.
Also, it has two related functions :
grade()
which calculates & returns the grade of the student.printWithGrade()
which prints the student info in the required format
The concept of Class enables us to encapsulate (group) this data and functions into a single entity - Student
class like this :
class Student(
val rollNo: Int,
val name: String,
val marks: List<Int>
) {
fun grade(): String {
return when (marks.average()) {
in 9.0..10.0 -> "A+"
in 8.0..9.0 -> "A"
in 7.0..8.0 -> "B"
in 6.0..7.0 -> "C"
in 5.0..6.0 -> "D"
else -> "E"
}
}
fun printWithGrade() {
println("$rollNo. $name -> ${grade()}")
}
}
This is only the definition of Student class and serves as a blueprint. To use this blueprint and actually save something, we need Objects. We create an object of the student class like this :
val aman = Student(11, "Aman", listOf(10, 9, 8, 10, 6))
So, to save information of multiple students, we now need only one list :
val students = listOf(
Student(11, "Aman", listOf(10, 9, 8, 10, 6)),
Student(20, "Yash", listOf(5, 6, 7, 7, 8)),
Student(31, "John", listOf(8, 8, 8, 10, 9)),
Student(50, "Juliet", listOf(7, 7, 8, 6, 8))
)
Finally, to print students info with their grades, we write :
students.forEach { it.printWithGrade() }
Comparison
Comparison
Encapsulation
Encapsulation
In the Normal approach, we needed to save the data of multiple students in separate lists. But in Object Oriented approach, we encapsulated the fields and defined the Student class. This way, we need only one list - List<Student>
.
Addionally, we were able to define the functions related to Student (grade()
& printWithGrade()
) within the same class.
Avoiding errors
Avoiding errors
While saving data in multiple lists, we can’t gaurantee that all lists will be of same size. For example, below code leads to error :
val rollNums = listOf(11, 20, 31, 50)
val names = listOf("Aman", "Yash")
repeat(rollNums.size) {
println("${rollNums[it]}. ${names[it]}")
}
Size of rollNums
is 4 but that of names
is only 2. When printing with size as 4, we get ArrayIndexOutOfBoundsException
when trying to access names[2]
and the program crashes.
Such error is easily avoided when following Object Oriented approach because each Student
object is sure to have all fields - rollNo
, name
& marks
.
Conclusion
Conclusion
Object Oriented approach helps us write code in a cleaner & organized way. We can define classes in separate files and maintain large codebase in an efficient manner.
Ex2 : Income Tax Calculator
Ex2 : Income Tax Calculator
Lets write a program to calculate income tax based on two different regimes (income tax slabs) of multiple persons given their name & income. The taxes of each regime should be printed along with person details in the format : $personName (Income = $income) → $tax1 vs $tax2
.
Example
Example
Tax Regimes
Tax Regimes
Regime 1 Tax Slabs
Income | Tax percentage |
---|---|
below Rs. 3L | exempted |
Rs. 3L - 6L | 5% |
Rs. 6L - 9L | 10% |
Rs. 9L - 12L | 15% |
Rs. 12L - 15L | 20% |
Rs. above 15L | 30% |
Regime 2 Tax Slabs
Income | Tax percentage |
---|---|
below Rs. 2.5L | exempted |
Rs. 2.5L - 5L | 5% |
Rs. 5L - 7.5L | 10% |
Rs. 7.5L - 10L | 15% |
Rs. 10L - 12.5L | 20% |
Rs. 12.5L - 15L | 25% |
above Rs. 15L | 30% |
- Note that
L
refers to Lakhs (100,000) andRs.
refers to the currency INR
Input
Input
Person, IncomeInLakhRupees
A, 20
B, 12
C, 7
D, 2
Output
Output
A (Income = 20.0L) -> Tax = 3.0L vs 3.375L
B (Income = 12.0L) -> Tax = 0.9L vs 1.15L
C (Income = 7.0L) -> Tax = 0.25L vs 0.325L
D (Income = 2.0L) -> Tax = 0.0L vs 0.0L
Tax Calculation
Tax Calculation
Following is an example of how tax is calculated for an income of Rs. 7L based on Regime 1 :
Income | Tax percentage | Taxable amount in slab | Tax |
---|---|---|---|
below Rs. 3L | exempted | 3L (left = 4L) | 0 |
Rs. 3L - 6L | 5% | 3L (left = 1L) | 15000 |
Rs. 6L - 9L | 10% | 1L | 10000 |
Rs. 9L - 12L | 15% | Nil | |
Rs. 12L - 15L | 20% | Nil | |
Rs. above 15L | 30% | Nil | |
Total Tax | 25000 (0.25L) |
Normal approach
Normal approach
Lets first see how to save each tax slab described with its lower limit, upper limit & tax percentage. We can use a Pair<Pair<Float, Float>, Int>
as (lower limit → upper limit) → taxPercentage
:
// For tax slab 3L - 6L (5%) :
val slab = (3f to 6f) to 5
Then, to represent a tax regime, we create list or map of such pairs :
val taxRegime = mapOf(
(0f to 3f) to 0,
(3f to 6f) to 5,
(6f to 9f) to 10,
(9f to 12f) to 15,
(12f to 15f) to 20,
(15f to Float.MAX_VALUE) to 30
)
To save multiple regimes, we create a list of regime :
val taxRegimes = listOf(
mapOf(
(0f to 3f) to 0,
(3f to 6f) to 5,
(6f to 9f) to 10,
(9f to 12f) to 15,
(12f to 15f) to 20,
(15f to Float.MAX_VALUE) to 30
),
mapOf(
(0f to 2.5f) to 0,
(2.5f to 5f) to 5,
(5f to 7.5f) to 10,
(7.5f to 10f) to 15,
(10f to 12.5f) to 20,
(12.5f to 15f) to 25,
(15f to Float.MAX_VALUE) to 30,
)
)
Now, lets focus on tax calculation function :
fun calculateTax(regime: Map<Pair<Float, Float>, Int>, incomeInLakhs: Float): Float {
var incomeLeft = incomeInLakhs
var tax = 0f
for (slab in regime) {
val taxable = minOf(incomeLeft, slab.key.second - slab.key.first)
tax += taxable * slab.value / 100
incomeLeft -= taxable
if (incomeLeft == 0f) break
}
return tax
}
- We simply iterate over each slab until the
incomeLeft > 0
- For each slab,
taxable
amount is calculated as minOf(incomeLeft, slab size) & deducted from theincomeLeft
tax
is calculated and added
To prepare the tax comparison text as $tax1 vs $tax2
, we define another function :
fun getTaxComparison(incomeInLakhs: Float): String {
return taxRegimes.map { regime -> calculateTax(regime, incomeInLakhs) }
.joinToString(" vs ") { "${it}L" }
}
To save details of persons, we need two lists - one for name (List<String>
) and another for their incomes (List<Float>
) :
val personNames = listOf("A", "B", "C", "D")
val personIncomesInLakhs = listOf(20f, 12f, 7f, 2f)
Finally, we iterate over the list and calculate & print tax comparison :
repeat(personNames.size) {
println("${personNames[it]} (Income = ${personIncomesInLakhs[it]}L) -> Tax = ${getTaxComparison(personIncomesInLakhs[it])}")
}
Object Oriented approach
Object Oriented approach
The entities that can be defined as user defined data type / Class are TaxSlab, TaxRegime and Person. Lets define a class for each of these first.
TaxSlab has 3 data fields - lowerLimit
, upperLimit
and taxPercentage
. Also, we can define a function - size()
for it, which will return the size of the slab :
class TaxSlab(
val lowerLimit: Float,
val upperLimit: Float,
val taxPercentage: Int
) {
fun size() = upperLimit - lowerLimit
}
A TaxSlab
object can then be defined as :
val taxSlab = TaxSlab(0f, 3f, 0)
TaxRegime has a single data field slabs: List<TaxSlab>
and can have a function calculateTax()
:
class TaxRegime(
val slabs: List<TaxSlab>
) {
fun calculateTax(incomeInLakhs: Float): Float {
var incomeLeft = incomeInLakhs
var tax = 0f
for (slab in slabs) {
val taxable = minOf(incomeLeft, slab.size())
tax += taxable * slab.taxPercentage / 100
incomeLeft -= taxable
if (incomeLeft == 0f) break
}
return tax
}
}
Multiple taxRegimes can then be saved in a List<TaxRegime>
:
private val taxRegimes = listOf(
TaxRegime(
listOf(
TaxSlab(0f, 3f, 0),
TaxSlab(3f, 6f, 5),
TaxSlab(6f, 9f, 10),
TaxSlab(9f, 12f, 15),
TaxSlab(12f, 15f, 20),
TaxSlab(15f, Float.MAX_VALUE, 30)
)
),
TaxRegime(
listOf(
TaxSlab(0f, 2.5f, 0),
TaxSlab(2.5f, 5f, 5),
TaxSlab(5f, 7.5f, 10),
TaxSlab(7.5f, 10f, 15),
TaxSlab(10f, 12.5f, 20),
TaxSlab(12.5f, 15f, 25),
TaxSlab(15f, Float.MAX_VALUE, 30)
)
)
)
Person entity has two data fields - name
& incomeInLakhs
. Also, we can define a function printTaxComparison()
to calculate and print the tax comparison :
class Person(
val name: String,
val incomeInLakhs: Float
) {
fun printTaxComparison() {
val taxes = taxRegimes.map { it.calculateTax(incomeInLakhs) }
.joinToString(" vs ") { "${it}L" }
println("$name (Income = ${incomeInLakhs}L) -> Tax = $taxes")
}
}
Persons info can then be saved in a List<Person>
:
val persons = listOf(
Person("A", 20f),
Person("B", 12f),
Person("C", 7f),
Person("D", 2f),
)
Finally, we iterate over the persons
list and invoke the printTaxComparison()
function :
persons.forEach { it.printTaxComparison() }
Comparison
Comparison
Encapsulation
Encapsulation
In the Normal approach, we needed to save the data of multiple persons in separate lists. But in Object Oriented approach, we encapsulated the fields and defined the Person class. This way, we need only one list - List<Person>
.
Addionally, we were able to define the function related to Person - printTaxComparison()
within the same class.
Code readability
Code readability
In Normal approach, we defined a slab as Pair<Pair<Float, Float>, Int>
and a tax regime as list of such pairs. So, to access its parameters in tax calculation, we wrote :
val taxable = minOf(incomeLeft, slab.key.second - slab.key.first)
tax += taxable * slab.value / 100
Note that the use of slab.key.second
, slab.key.first
& slab.value
decrease code readability.
But when we followed Object oriented approach, we were able to use TaxSlab
object as :
val taxable = minOf(incomeLeft, slab.size())
tax += taxable * slab.taxPercentage / 100
This improved code readability!
Avoiding errors
Avoiding errors
Similar to Ex1, Object oriented approach helps us avoid the ArrayIndexOutOfBoundsException
.
Conclusion
Conclusion
Object Oriented approach helps us write code in a cleaner, readable and organized way. We can define classes in separate files and maintain large codebase in an efficient manner.
Examples from Kotlin StdLib
Examples from Kotlin StdLib
Classes are almost everywhere in Kotlin. All primitive data types like Int
, Float
, String
etc are Classes. Creating a variable of these types creates an object of that class :
val x = 5 // Creates an Int object
val y = "abc" // Creates a String object
These primitive data type classes provide many useful functions like String.uppercase()
, String.substring()
, Int.coerceAtMost()
, Int.countOneBits()
etc :
println(y.uppercase()) // ABC
println(y.substring(1)) // bc
println(x.coerceAtMost(3)) // 3
println(x.countOneBits()) // 2
Collections like List
, Map
, Set
etc. are also classes. Using functions like listOf()
, mapOf()
, setOf()
, we create an object of such collection classes :
val list = listOf(1, 2, 3) // Creates an object of List<Int>
val map = mapOf("A+" to 10) // Creates an object of Map<String, Int>
val set = setOf('A', 'B') // Creates an object of Set<Char>
Note that List, Map and Set are Generic classes. Ex. - we are able to define List<Int>
, List<String>
etc. using the same List<T>
class where T
can be any class.