Welcome to today's lesson on applying Kotlin to perform data filtering
and aggregation
in a real-world scenario using a user management system. We'll start by building a foundational structure that can handle basic user operations. Then, we'll expand it by introducing more advanced functionalities that allow filtering and aggregating user data.
In our starter task, we will implement a class that manages basic operations on a collection of user data, specifically handling the addition of new users, retrieving user profiles, and updating user profiles.
Here are the starter task methods:
-
addUser(userId: String, age: Int, country: String, subscribed: Boolean): Boolean
- adds a new user with the specified attributes. Returnstrue
if the user was added successfully andfalse
if a user with the sameuserId
already exists. -
getUser(userId: String): Map<String, Any?>?
- returns the user's profile as a map if the user exists; otherwise, returnsnull
. -
updateUser(userId: String, age: Int?, country: String?, subscribed: Boolean?): Boolean
- updates the user's profile based on non-null parameters. Returnstrue
if the user exists and was updated,false
otherwise.
Here is the implementation of our starter task using Kotlin:
Kotlin1class UserManager { 2 private val users = mutableMapOf<String, MutableMap<String, Any?>>() 3 4 fun addUser(userId: String, age: Int, country: String, subscribed: Boolean): Boolean { 5 if (users.containsKey(userId)) { 6 return false 7 } 8 users[userId] = mutableMapOf("age" to age, "country" to country, "subscribed" to subscribed) 9 return true 10 } 11 12 fun getUser(userId: String): Map<String, Any?>? { 13 return users[userId] 14 } 15 16 fun updateUser(userId: String, age: Int?, country: String?, subscribed: Boolean?): Boolean { 17 val user = users[userId] ?: return false 18 age?.let { user["age"] = it } 19 country?.let { user["country"] = it } 20 subscribed?.let { user["subscribed"] = it } 21 return true 22 } 23} 24 25// Example usage 26fun main() { 27 val um = UserManager() 28 println(um.addUser("u1", 25, "USA", true)) // true 29 println(um.addUser("u2", 30, "Canada", false)) // true 30 println(um.addUser("u1", 22, "Mexico", true)) // false 31 println(um.getUser("u1")) // {age=25, country=USA, subscribed=true} 32 println(um.updateUser("u1", 26, null, null)) // true 33 println(um.updateUser("u3", 19, "UK", false)) // false 34}
This implementation covers all our starter methods. Let's move forward and introduce more complex functionalities.
With our foundational structure in place, it's time to add functionalities for filtering user data and aggregating statistics.
Here are new methods to implement in Kotlin:
-
filterUsers(minAge: Int?, maxAge: Int?, country: String?, subscribed: Boolean?): List<String>
:- Returns the list of user IDs that match the specified criteria. Criteria can be
null
, meaning that criterion should not be applied during filtering.
- Returns the list of user IDs that match the specified criteria. Criteria can be
-
aggregateStats(): Map<String, Number>
- returns statistics in the form of a map:total_users
: Total number of usersaverage_age
: Average age of all users (rounded down to the nearest integer)subscribed_ratio
: Ratio of subscribed users to total users (as a double with two decimals)
This method filters users based on the criteria provided. Let's see how it works in Kotlin:
Kotlin1class UserManager { 2 // Existing methods... 3 4 fun filterUsers(minAge: Int?, maxAge: Int?, country: String?, subscribed: Boolean?): List<String> { 5 return users.filter { (_, profile) -> 6 (minAge == null || profile["age"] as Int >= minAge) && 7 (maxAge == null || profile["age"] as Int <= maxAge) && 8 (country == null || profile["country"] == country) && 9 (subscribed == null || profile["subscribed"] == subscribed) 10 }.keys.toList() 11 } 12} 13 14// Example usage of the new method 15fun main() { 16 val um = UserManager() 17 um.addUser("u1", 25, "USA", true) 18 um.addUser("u2", 30, "Canada", false) 19 um.addUser("u3", 22, "USA", true) 20 println(um.filterUsers(20, 30, "USA", true)) // ["u1", "u3"] 21 println(um.filterUsers(null, 28, null, null)) // ["u1", "u3"] 22 println(um.filterUsers(null, null, "Canada", false)) // ["u2"] 23}
The filterUsers
method filters users based on minAge
, maxAge
, country
, and subscribed
status criteria. It uses Kotlin's filter
function on the users
map, where (_, profile)
destructures each map entry - the underscore _
discards the userId (key) since it's not needed for filtering, and profile
represents the user's data map. The method uses null-safe conditions (like minAge == null || profile["age"] as Int >= minAge
) to only apply filters when criteria are provided. Users meeting all specified criteria are included in the result, and their IDs are returned as a list using .keys.toList()
. The example usage shows filtering scenarios like finding users from USA who are subscribed and between ages 20-30, which returns matching user IDs.
This method aggregates statistics from the user profiles. Let's implement it in Kotlin:
Kotlin1class UserManager { 2 // Existing methods... 3 4 fun aggregateStats(): Map<String, Number> { 5 val totalUsers = users.size 6 if (totalUsers == 0) { 7 return mapOf("total_users" to 0, "average_age" to 0, "subscribed_ratio" to 0.00) 8 } 9 10 val totalAge = users.values.sumOf { it["age"] as Int } 11 val subscribedUsers = users.values.count { it["subscribed"] == true } 12 13 val averageAge = totalAge / totalUsers 14 val subscribedRatio = String.format("%.2f", subscribedUsers.toDouble() / totalUsers).toDouble() 15 16 return mapOf("total_users" to totalUsers, "average_age" to averageAge, "subscribed_ratio" to subscribedRatio) 17 } 18} 19 20fun main() { 21 val um = UserManager() 22 um.addUser("u1", 25, "USA", true) 23 um.addUser("u2", 30, "Canada", false) 24 um.addUser("u3", 22, "USA", true) 25 26 println(um.aggregateStats()) // {total_users=3, average_age=25, subscribed_ratio=0.67} 27}
This aggregateStats
method calculates and returns aggregate statistics about the users in the form of a map. It first determines totalUsers
, the total number of users. If there are no users, it returns a map with zeroed statistics. Otherwise, it calculates totalAge
by summing the ages of all users and counts subscribedUsers
who are subscribed. It then computes averageAge
via integer division of totalAge
by totalUsers
and calculates subscribedRatio
by dividing subscribedUsers
by totalUsers
and rounding to two decimal places. The resulting statistics map includes total_users
, average_age
, and subscribed_ratio
.
Here's the complete UserManager
class with all methods, including the new ones for filtering and aggregation:
Kotlin1class UserManager { 2 private val users = mutableMapOf<String, MutableMap<String, Any?>>() 3 4 fun addUser(userId: String, age: Int, country: String, subscribed: Boolean): Boolean { 5 if (users.containsKey(userId)) { 6 return false 7 } 8 users[userId] = mutableMapOf("age" to age, "country" to country, "subscribed" to subscribed) 9 return true 10 } 11 12 fun getUser(userId: String): Map<String, Any?>? { 13 return users[userId] 14 } 15 16 fun updateUser(userId: String, age: Int?, country: String?, subscribed: Boolean?): Boolean { 17 val user = users[userId] ?: return false 18 age?.let { user["age"] = it } 19 country?.let { user["country"] = it } 20 subscribed?.let { user["subscribed"] = it } 21 return true 22 } 23 24 fun filterUsers(minAge: Int?, maxAge: Int?, country: String?, subscribed: Boolean?): List<String> { 25 return users.filter { (_, profile) -> 26 (minAge == null || profile["age"] as Int >= minAge) && 27 (maxAge == null || profile["age"] as Int <= maxAge) && 28 (country == null || profile["country"] == country) && 29 (subscribed == null || profile["subscribed"] == subscribed) 30 }.keys.toList() 31 } 32 33 fun aggregateStats(): Map<String, Number> { 34 val totalUsers = users.size 35 if (totalUsers == 0) { 36 return mapOf("total_users" to 0, "average_age" to 0, "subscribed_ratio" to 0.00) 37 } 38 39 val totalAge = users.values.sumOf { it["age"] as Int } 40 val subscribedUsers = users.values.count { it["subscribed"] == true } 41 42 val averageAge = totalAge / totalUsers 43 val subscribedRatio = String.format("%.2f", subscribedUsers.toDouble() / totalUsers).toDouble() 44 45 return mapOf("total_users" to totalUsers, "average_age" to averageAge, "subscribed_ratio" to subscribedRatio) 46 } 47} 48 49// Example usage 50fun main() { 51 val um = UserManager() 52 um.addUser("u1", 25, "USA", true) 53 um.addUser("u2", 30, "Canada", false) 54 um.addUser("u3", 22, "USA", true) 55 56 println(um.filterUsers(20, 30, "USA", true)) // ["u1", "u3"] 57 println(um.filterUsers(null, 28, null, null)) // ["u1", "u3"] 58 println(um.filterUsers(null, null, "Canada", false)) // ["u2"] 59 60 println(um.aggregateStats()) // {total_users=3, average_age=25, subscribed_ratio=0.67} 61}
Great job! Today, you've learned how to effectively handle user data by implementing advanced functionalities like filtering and aggregation on top of a basic system. This is a critical skill in real-life software development, where you often need to extend existing systems to meet new requirements.
I encourage you to practice solving similar challenges to solidify your understanding of data filtering and aggregation. Happy coding, and see you in the next lesson!