Kotlin Training Program

DOWNLOAD APP

FEEDBACK

String

Recall that String data type is used to store text. Text is made up of characters. A character is simply an integer (aka code) which has a graphical representation (aka glyph). For instance, ‘A’ is a character with integer code 65.

Possible characters are letters (‘A’..‘Z’, ‘a’..‘z’), digits (‘0’..‘9’), symbols (+, -, *, /, %, $, #, !, @, ^, & etc.) and some special characters (\n, \t etc).

String is simply an array of character. For instance, “APPLE” is a String made up of 5 characters.

Char

Code access

We can access the code of any character using the Char#code field.

 fun main() {
    println('A'.code) // Prints "65"
    println('A'.code..'Z'.code) // Prints "65..90"
    println('0'.code..'9'.code) // Prints "48..57"
    println('\n'.code) // Prints "10"
    println('#'.code)  // Prints "35"
}
 

Type check

We have several built-in functions like isLetter(), isDigit(), isLowerCase(), isUppercase() etc. :

 fun main() {
    println('A'.isLetter())     // true
    println('z'.isLetter())     // true
    println('0'.isLetter())     // false
    println('0'.isDigit())      // true
    println('A'.isUpperCase())  // true
    println('z'.isUpperCase())  // false
    println('x'.isLowerCase())  // true
}
 

Case conversions

We can convert a character from uppercase to lowercase and vice-versa using the lowercaseChar() & uppercaseChar() functions respectively :

 fun main() {
    println('A'.lowercaseChar()) // a
		println('z'.uppercaseChar()) // Z
}
 

Parse Digit

If a character is digit, then we can parse it to an Int. For example - ‘5’ → 5. We use the digitToInt() function for this :

 fun main() {
    listOf('0', '2', '5', '7', '8').forEach {
        println(it.digitToInt())
    }
    println('a'.digitToInt()) // throws IllegalArgumentException
}

/* Output :

0
2
5
7
8
Exception in thread "main" java.lang.IllegalArgumentException: Char a is not a decimal digit
 */
 

To avoid exception being thrown in case of non-digit characters, we can use the safe function - toIntOrNull() :

 fun main() {
    println('a'.digitToIntOrNull()) // Prints null instead
}
 

Equality check

To check whether two characters are equal, we can use the equals() function :

 fun Char.equals(
		other: Char, 
		ignoreCase: Boolean = false // Whether to ignore case
): Boolean
 

Example :

 fun main() {
    println('A'.equals('a')) // false
		println('A'.equals('a', ignoreCase = true)) // true
}
 

Basics

Concatenation

Concatenation refers to joining strings or a string and other data type. We can perform concatenation using the + operator :

 fun main() {
    println("A = " + 5)
    println("Hello" + " " + "World!")
}
 

Length

String length can be accessed using the length field of String class :

 /* String */.length
 

Example :

 fun main() {
		println("KOTLIN".length) // Prints "6"
}
 

Index Access

Similar to Arrays & Lists, we can access the character at a given index (position) of string using the indexing operator [] or the get() function :

 fun main() {
    println("APPLE"[0]) // A
    println("APPLE".get(3)) // L
    println("APPLE"[5]) // throws IndexOutOfBoundsException
}
 

Modification

String is immutable i.e. we can not modify it once it is defined.

 fun main() {
		val string = "HIMMLAYAS"
		string[3] = 'A' // ERROR! Invalid, not allowed
}
 

To modify a string, we need to first convert it to a character array, apply the modifications and then convert it back to string.

To convert a string to character array, we use the toCharArray() function :

 val string = "HIMMLAYAS"
val array: CharArray = string.toCharArray()
 

We can then apply the modifications using [] operator or the set() function :

 array[3] = 'A' 
// OR 
array.set(3, 'A')
 

Finally, for converting character array to string, we use the String constructor or the concatToString() function :

 val newString = String(array)
println(newString) // HIMALAYAS

// OR

val newString = array.concatToString()
println(newString) // HIMALAYAS
 

Final program :

 fun main() {
		val string = "HIMMLAYAS"
		val array: CharArray = string.toCharArray()
		array[3] = 'A'
		val newString = array.concatToString()
		println(newString) // HIMALAYAS
}
 

String templates

In Kotlin, we can perform string formatting very easile using String templates. Formatting refers to substituting values of a variable or expression into a string. We can use the dollar sign $ to embed values into the string. Example :

 fun main() {
		val a = 5
		val b = 10
		print("$a + $b = ${a + b}") // Prints: 5 + 10 = 15
}
 

We can use curly braces {} to enclose expressions. However, they are not required for embedding variable.

Special Characters

When it comes to strings, we have some special characters that serve a special purpose. Some of them are :

  • Newline character - \n : It represents a line break.
  • Tab character - \t : It represents a tab.

Example :

 fun main() {
		println("Hello\n\tWorld!")
}

/* Output :

Hello
    World!
*/
 

Escape Character \

Recall that strings are always enclosed in double quotes "". How do we write a string that itself contains double quotes? For example, a string like - It's a "Deadlock"!. It contains double quotes. If we try to write it like this :

 val message = "It's a "Deadlock"!" // Won't work!
 

It won’t work because double quotes have a special purpose of enclosing strings. So, to write double quotes inside of a string, we’ll have to prepend the qoutes with escape character - \. So, following will work :

 val message = "It's a \"Deadlock\"!" // Works!
 

Escape character can also be used to escape Newline & Tab character.

Dollar sign $ also has a special purpose in String templates. Suppose, we need to write a string - $hello$, without refering to the variable named hello (may or may not exist). Here also, we need to escape the dollar sign using escape character like this :

 val message = "$hello@CONTENTquot; // Won't work!
val message = "\$hello\@CONTENTquot; // Works!
 

StringBuilder

StringBuilder is a class which can be used for modifying strings without creating new ones. We can append, insert or delete characters from the string using the StringBuilder class. It is especially useful when we need to make a lot of modifications to a large string.

Overview

 fun main() {
		val builder = StringBuilder("Hello, World!")
		builder.append(" Goodbye!")
		builder.insert(5, " there,")
		builder.delete(5, 13)
		val newString = builder.toString()
		println(newString) // Hello World! Goodbye!
}
 

To create an instance of StringBuilder class, we use the StringBuilder(string: String) constructor.

 val builder = StringBuilder() // Passing a string is optional
 

We can then modify the string using the append(), insert(), and delete() functions.

We can append to the string anything that we want - Int, Float, Char, String or instance of any other class :

 builder.append("Pi = ") // Appending String
builder.append(3.14f) // Appending Float
builder.append('!') // Appending Char
 

To insert at a particular index, use the insert() function :

 builder.insert(0, "Approximate value of ") // Inserts String at index 0
 

To delete some characters from a given index, we can use the delete() function :

 /* builder */.delete(/* startIndexInclusive, endIndexExclusive */)

// Example :
builder.delete(30, 31) // Removes substring at index 30..31 i.e. '!'
 

Finally, we can convert the StringBuilder object to a string using the toString() method.

 val string = builder.toString()
println(string) // Prints "Approximate value of Pi = 3.14"
 

Need

We can easily create new string from several smaller strings using concatenation +. But when concatenation is performed a large number of times, it takes more time & isn’t memory efficient. Concatenation creates new string on each iteration which becomes an overhead when performed large number of times. In such cases, StringBuilder should be used. It is fast & memory efficient as compared to concatenation.

Lets see a practical comparison between the performance of the two approaches :

 /* Output :

Time taken by concatenationApproach = 11763300 ns
Time taken by stringBuilderApproach = 90100 ns
 */

fun main() {
    val t1 = measureNanoTime {
        concatenationApproach()
    }
    println("Time taken by concatenationApproach = $t1 ns")

    val t2 = measureNanoTime {
        stringBuilderApproach()
    }
    println("Time taken by stringBuilderApproach = $t2 ns")
}

fun concatenationApproach() {
    var string = ""
    repeat(1000) {
        string += "ABCD"
    }
}

fun stringBuilderApproach() {
    val builder = StringBuilder()
    repeat(1000) {
        builder.append("ABCD")
    }
    val string = builder.toString()
}
 

It is clear from the above example that StringBuilder is faster than concatenation approach by a factor of 130x! However, this may differ with number of iterations.

Note that we have used the built-in function measureNanoTime() which returns the time taken to execute a code block.

buildString() function

To avoid writing builder i.e. name of the StringBuilder instance repeatedly, we can make the code more concise by using the buildString() function.

 buildString {
		/* StringBuilder scope */
		/* Here, you can call the StringBuilder functions directly */
}
 

Example :

 fun main() {
    /* Normal approach */
    val builder = StringBuilder()
    repeat(1000) {
        builder.append("ABCD")
    }
    val string = builder.toString()
    println(string)
    

    /* buildString approach */
    val newString = buildString { 
        repeat(1000) { append("ABCD") }
    }
    println(newString)
}
 

Notice that we are not required to call the toString() function as buildString() returns the final string directly.

Example : Map#customToString()

The default toString() function of Map prints the enties in a single line :

 fun main() {
    val statueOfUnityInfo = mapOf(
        "name" to "Statue of Unity",
        "location" to "Kevadia, Gujarat, India",
        "height" to "182 meters (597 ft)",
        "weight" to "approx. 1700 tonnes",
        "builder" to "Larsen & Toubro",
        "inaugurated" to "31 October 2018"
    )

    println(statueOfUnityInfo)
}

/* Output :

{name=Statue of Unity, location=Kevadia, Gujarat, India, height=182 meters (597 ft), weight=approx. 1700 tonnes, builder=Larsen & Toubro, inaugurated=31 October 2018}
 */
 

Lets write a custom toCustomString() function to print the entries in the following syntax :

 // Default :
{key1=val1, key2=val2, ...}

// Custom :
{
		key1 -> val1
		key2 -> val2
		...
}
 

Function definition :

 fun Map<*, *>.toCustomString(): String {
    return buildString {
        append("{\n")
        entries.forEach { (key, value) ->
            append("\t")
            append("\"$key\" -> $value")
            append("\n")
        }
        append("}")
    }
}
 

We have defined an extension function of Map. It used buildString() function to build the string representation that we need. It iterates over all the entries and appends them in the required format.

Usage :

 fun main() {
    val statueOfUnityInfo = mapOf(...)

    println(statueOfUnityInfo.toCustomString())
}

fun Map<*, *>.toCustomString(): String {
    return buildString {
        append("{\n")
        entries.forEach { (key, value) ->
            append("\t")
            append("\"$key\" -> $value")
            append("\n")
        }
        append("}")
    }
}

/* Output :

{
	"name" -> Statue of Unity
	"location" -> Kevadia, Gujarat, India
	"height" -> 182 meters (597 ft)
	"weight" -> approx. 1700 tonnes
	"builder" -> Larsen & Toubro
	"inaugurated" -> 31 October 2018
}
 */
 

Functions

The String class provides many functions to perform operations on String. We can use them to make development faster and easier. Some of the most commonly used functions are described below.

Reverse

To reverse a string, use the reversed() function :

 /* string */.reversed()
 

Examples :

 fun main() {
    println("ROCKET".reversed())

    listOf("Rotator", "Wow", "Noon", "Moon", "Malayalam", "Man")
        .forEach {
            val isPalindrome = it.isPalindrome()
            println("$it is ${if (isPalindrome) "a" else "NOT a"} Palindrome")
        }
}

/* Output :

TEKCOR
Rotator is a Palindrome
Wow is a Palindrome
Noon is a Palindrome
Moon is NOT a Palindrome
Malayalam is a Palindrome
Man is NOT a Palindrome
 */

fun String.isPalindrome(): Boolean = 
    this.equals(reversed(), ignoreCase = true)
 

Case conversions

To convert a string from lowercase to uppercase and vice-versa, we have uppercase() and lowercase() function respectively :

 /* String */.uppercase()
/* String */.lowercase()
 

Examples :

 fun main() {
    println("256 meters".uppercase()) // Prints "256 METERS"
    println("ALPHA".lowercase()) // Prints "alpha"
}
 

Parsing numbers

To parse a numeric value from String, we have several functions like toInt(), toLong(), toFloat(), toDouble() etc.

Moreover, to avoid the NumberFormatException (which is thrown for invalid input), we can use …orNull() functions.

Examples :

 fun main() {
    val x: Int = "123".toInt()
    val pi: Float = "3.14".toFloat()

    val y: Int? = "123A".toIntOrNull() // toInt() throws Exception
    val z: Float? = "3.p".toFloatOrNull() // toFloat() throws Exception

    println("""
        x = $x
        pi = $pi
        y = $y
        z = $z
    """.trimIndent())
}

/* Output :

x = 123
pi = 3.14
y = null
z = null
 */
 

Contains

To check whether a string contains given character or string, we can use the contains() function.

This is particularly useful in search. From a list of string, to return search suggestions that match user query, we can use this function.

 fun String.contains(
		query: Char /* or String */, 
		ignoreCase: Boolean = false // Whether to ignore case
): Boolean
 

Note that contains() returns a Boolean and not the location of query in the string. To find the location of query, we have to use indexOf() function.

Examples :

 fun main() {
    val name = "BOMBAY"
    
    println(name.contains('c')) // false
    println(name.contains('b')) // false
    println(name.contains('b', true)) // true
    println(name.contains("mum")) // false 
    println(name.contains("bomb")) // false
    println(name.contains("bomb", true)) // true
}
 

Equality check

To check whether two strings are equal, we can simply use the equality operator == :

 fun main() {
		val a = "Moon"
		val b = "Moon"
		println(a == b)
}
 

For comparing while ignoring case, we can use the equals() function :

 fun main() {
    val a = "Moon"
    val b = "MOON"
    println(a == b) // false
    println(a.equals(b, ignoreCase = true)) // true
}
 

For partial equality checks i.e. prefix or suffix check, we can use startsWith() and endsWith() functions :

 fun main() {
    println("Mars".startsWith("m")) // false
    println("Mars".startsWith("m", ignoreCase = true)) // true
    println("Sun".endsWith("n")) // true
}
 

Another example :

 enum class Gender {

    Male, Female, Other;

    companion object {
        fun fromName(name: String): Gender {
            return when {
                name.startsWithAny("Mr.", "Master") -> Male
                name.startsWithAny("Miss", "Ms.", "Mrs.") -> Female
                else -> Other
            }
        }
    }
}

fun String.startsWithAny(vararg prefixes: String): Boolean {
    return prefixes.any { startsWith(it) }
}

fun main() {
    listOf(
        "Mr. X", "Master Y", "Miss Z", "Ms. P", "Mrs. Q", "R"
    ).forEach {
        println("$it is ${Gender.fromName(it)}")
    }
}

/* Output :

Mr. X is Male
Master Y is Male
Miss Z is Female
Ms. P is Female
Mrs. Q is Female
R is Other
 */
 

IndexOf

To finc the index of a character / string in a given string, we use the indexOf() function.

 // Returns the index of first occurrence or -1 otherwise
fun String.indexOf(
		query: Char /* or String */, 
		startIndex: Int = 0, // Where to start the search
		ignoreCase: Boolean = false // Whether to ignore case
): Int

// Returns the index of last occurrence or -1 otherwise
fun String.lastIndexOf(
		query: Char /* or String */, 
		startIndex: Int = 0, 
		ignoreCase: Boolean = false 
): Int
 

To check against multiple queries at once, we can use indexOfAny() function :

 // Returns the index of first / last occurrence or -1 otherwise
fun String.indexOfAny /* or lastIndexOfAny */(
		query: CharArray /* or List<String> */, 
		startIndex: Int = 0, // Where to start the search
		ignoreCase: Boolean = false // Whether to ignore case
): Int
 

Examples :

 fun main() {
    val name = "RAJASTHAN"

    println(name.indexOf('A')) // 1
    println(name.indexOf('a', 2, true)) // 3
    println(name.lastIndexOf('A')) // 7

    println(name.indexOf('X')) // -1 (Not found)

    val string = "Thiruvananthapuram"

    println(string.indexOf("VAN", ignoreCase = true)) // 5

    // Searches for vowel
    println(
        string.indexOfAny(
            chars = charArrayOf('a', 'e', 'i', 'o', 'u'), 
            ignoreCase = true
        )
    ) // 2
}
 

indexOf() also has a limitation that it finds only the first or last occurrence but not all. If we want to find all occurrences, we can use Regex discussed in Advanced Search topic.

Advanced Search

To perform advanced search on String i.e. find all occurrences of query, we can use Regular Expression (Regex).

Regular Expression is a string used to define a search pattern.

You can learn & experiment on Regex at this awesome website.

 // Define a Regex pattern
val regex = Regex(/* your pattern here */)

// Use the findAll() to get Sequence<MatchResult>
regex.findAll(/* inputString */, /* startIndex */)
 

Examples :

 fun main() {
    val string = "A11B22C33D44E55I"

    // Finds all numbers in the input string
    val numberResults = Regex("\\d+").findAll(string)
    for (result in numberResults) {
        println("number ${result.value} found at ${result.range}")
    }

    // Finds all vowels in the input string
    val vowelsResults = Regex("[aeiouAEIOU]").findAll(string)
    for (result in vowelsResults) {
        println("vowel ${result.value} found at ${result.range}")
    }
}

/* Output

number 11 found at 1..2
number 22 found at 4..5
number 33 found at 7..8
number 44 found at 10..11
number 55 found at 13..14
vowel A found at 0..0
vowel E found at 12..12
vowel I found at 15..15
 */
 

Replace

To a replace a character or substring of a string with some other character or string, we can use the replace() function.

 fun String.replace(
		old: Char /* or String */, 
		new: Char /* or String */, 
		ignoreCase: Boolean = false
): String
 

Example :

 fun main() {
    println("Palm down!".replace('P', 'C'))
    println("Landing on Moon".replace("Moon", "Mars"))
}

/* Output :

Calm down!
Landing on Mars
 */
 

To perform more complex replacements, we can use Regex :

 fun String.replace(
		regex: Regex, 
		replacement: String
): String

fun String.replace(
		regex: Regex, 
		transform: (MatchResult) -> CharSequence
): String
 

Example :

 fun main() {
    val html = """
        <h2>A</h2>
        <h3>B</h3>
        <h4>C</h4>
        <h5>D</h5>
        <h6>E</h6>
    """.trimIndent()

    println(promoteHeadings(html))
}

fun promoteHeadings(html: String): String {
    return html.replace(Regex("</?h[2-6]>")) { match ->
        val tag = match.value
        Regex("\\d").replace(tag) {
            val headingLevel = it.value.toInt()
            (headingLevel - 1).toString()
        }
    }
}

/* Output :

<h1>A</h1>
<h2>B</h2>
<h3>C</h3>
<h4>D</h4>
<h5>E</h5>
 */
 

Substring

To extract a portion of string, we can use the substring() function. There are 3 ways in which we can invoke this function :

 // 1. Using start index 
fun String.substring(
		startIndex: Int // Inclusive
): String

// 2. Using start & end indices
fun String.substring(
		startIndex: Int, // Inclusive
		endIndex: Int // Exclusive
): String

// 3. Using IntRange
fun String.substring(
		range: IntRange // Both ends inclusive
): String
 

Examples :

 fun main() {
    println("**--GOLD**".substring(4)) // Prints "GOLD**"
    println("**--GOLD**".substring(4, 8)) // Prints "GOLD"
    println("**--GOLD**".substring(4..7)) // Prints "GOLD"
}
 

Trim

To remove few characters from start or end of a string, we can use the trim() function.

This is particularly useful in applications which involve user input. User may enter extra spaces at the start (leading) or end (trailing) of the input. We should use this function to remove them.

Not only spaces, we can remove any custom defined char(s) from start or end of the string.

 fun String.trim(): String // Removes leading AND trailing whitespace (' ', '\t', '\n')

fun String.trimStart(): String // Removes only leading whitespaces

fun String.trimEnd(): String // Removes only trailing whitespaces

// All three functions take an optional lambda to specify custom characters to trim
fun String./* all 3 functions */(
		predicate: (Char) -> Boolean // Return true to remove
): String
 

Examples :

 fun main() {
    val name = "\t  Lady Ada Lovelace    "

    println(name.trim())        // Prints "Lady Ada Lovelace"
    println(name.trimStart())   // Prints "Lady Ada Lovelace    "
    println(name.trimEnd())     // Prints "	  Lady Ada Lovelace"

    val string = "*-*-* GOLD *-*-*"

    println(
        string.trim {
            it in listOf('*', '-', ' ') // Removes all 3 characters
        }
    ) // Prints "GOLD"

    println(
        string.trimStart {
            "*- ".contains(it) // Removes all 3 characters
        }
    ) // Prints "GOLD *-*-*"

    println(
        string.trimEnd {
            "*- ".contains(it) // Removes all 3 characters
        }
    ) // Prints "*-*-* GOLD"
}
 

Padding

Padding refers to prefixing or postfixing a specific character to the string, in order to achieve a fixed length.

For example, to make the string “**” of length 5, we need to pad 3 more characters (say ‘#’) at start / end of the string. So after pad start, it would look like ###**.

For padding, we have built-in padStart() and padEnd() functions.

 fun String.padStart /* or End */(
		length: Int,  // Length to be achieved
		padChar: Char = ' '
): String
 

Example : Basic

 // Returns a string consisting of (n = times) number of char
fun multiplyChar(char: Char, times: Int) =
    "".padStart(times, char)

fun main() {
    println("**".padStart(10, '#'))

    println(multiplyChar('X', 5))
    println(multiplyChar('O', 10))
}

/* Output :

########**
XXXXX
OOOOOOOOOO
 */
 

Example : Inverted Triangle Pattern

Following is a program that prints inverted triangle pattern of given height. To repeat a character a certain number of times, padStart() function is used on empty strings.

 fun main() {
    printInvertedTriangle(15)
}

/*

height = 6

' ', '*',
 0 ,  11, ***********
 1 ,   9,  *********
 2 ,   7,   *******
 3 ,   5,    *****
 4 ,   3,     ***
 5 ,   1,      *

 n(' ') = i
 n('*') = [2 * (h - i)] - 1
 */
fun printInvertedTriangle(height: Int, char: Char = '*') {
    repeat(height) { i ->
        // Print ' ' i times
        print("".padStart(i))
        
        // Print '*' {[2 * (h - i)] - 1} times
        println("".padStart(2 * (height - i) - 1, char))
    }
}

/* Output :

***********
 *********
  *******
   *****
    ***
     *
 */
 

Example : Holidays Hash

Consider a coaching attendance management application that requires to save the weekly holidays a student has. For example, out of 7 days in a week, a student may have [Sat, Sun] holidays.

There are multiple ways to store this holidays list.

One naive approach is to save it in a list or map :

 val holidaysList = listOf("Sat", "Sun")
/* Size : 

1 Holiday size = 3 * 2B = 6B
List Size = 6B * 2 = 12B
 */

// OR

val holidaysMap = mapOf(
    "Sun" to true,
    "Mon" to false,
    "Tue" to false,
    "Wed" to false,
    "Thu" to false,
    "Fri" to false,
    "Sat" to true
)
/*
Map size = 7 * (6B + 1B) = 49B
(Assuming Boolean of 1B)
*/
 

Roughly, the size of map is static i.e. 49B while that of list varies with number of holidayes i.e. 6*nB.

Memory wise, can we perform better than this? Yes we can, using a Boolean list.

 val holidays = listOf(
    true, false, false, false, false, false, true
)
// Index 0 -> Sun, 1 -> Mon, ...
// Size = 7 * 1B = 7B
 

Can we perform even better? Yes, we can save the entire information in a single Byte! Here is how :

Consider the booleans in above list as 0s and 1s. So, we can rewrite the list as a String of 0s and 1s as 1000001. This is nothing but a binary number of 7 bits, which can easily be stored in a Byte as :

 val holidaysHash: Byte = 0b1000001
 

Now, suppose we need to write a function that would take this hash: Byte as input and return the holidays as List<String>.

For this, we need to follow the steps :

  • Parse the Byte as Binary String
    • Eg. - 5“101”
  • Pad leading zeroes because the binary string length may be less than 7
    • Eg. - “101”“0000101”
  • Map each character in string to Pair<Int, Boolean> where key is index and value is false for 0 and true for 1
    • Eg. - “0000101”[0 → false, 1 → false, …, 4 → true, 5 → false, 6 → true]
  • Map each pair to a nullable string where true pairs are mapped to corresponding day and false pairs are mapped to null
    • Eg. - [0 → false, 1 → false, …, 4 → true, 5 → false, 6 → true][null, null, …, “Thu”, null, “Sat”]
  • Filter out null values and we have our result :
    • Eg. - [null, null, …, “Thu”, null, “Sat”][“Thu”, “Sat”]

Code :

 fun parseHolidays(hash: Int): List<String> {

    return hash

        .toString(2) // Convert hash to a binary number

        .padStart(7, '0') // Pad zeroes

        .mapIndexed { index, c -> // Create (index -> true/false) map
            index to (c != '0')
        }

        .mapNotNull { (index, isHoliday) ->
            if (isHoliday) {
                // If holiday, map the index to corresponding day
                when (index) {
                    0 -> "Sun"
                    1 -> "Mon"
                    2 -> "Tue"
                    3 -> "Wed"
                    4 -> "Thu"
                    5 -> "Fri"
                    6 -> "Sat"
                    else -> error("Invalid index - $index")
                }
            } else {
                null
            }
        }

}
 

Usage :

 fun main() {
    listOf(
        0b1000001,
        0b0010101,
        0b0101010
    ).forEach {
        println("Holidays(${it.toString(2)} = $it) = ${parseHolidays(it)}")
    }
}

/* Output

Holidays(1000001 = 65) = [Sun, Sat]
Holidays(10101 = 21) = [Tue, Thu, Sat]
Holidays(101010 = 42) = [Mon, Wed, Fri]
 */
 

It may seem useless to do such optimization where we reduced the size required to save holidays info of a single student. We started from 49B (Map) → 6nB (List) → 7B (Boolean list) to 1B (Byte). It may not make a big difference for a few hundreds of students data, but on comparing at a larger scale we’ll have huge savings. Following is a comparison chart for 1 Billion students data :

Size
Map 49B * 1 Billion = 49GB
List 6nB * 1 Billion = 6nGB
Boolean List 7B * 1 Billion = 7GB
Byte 1B * 1 Billion = 1GB

We save 48GB of space when using Byte approach as compared to Map approach!

Notice that all the above functions return new string instead of modifying the existing string. This is because String is immutable.

We will study custom implementation of all these functions in the Algorithms module.