In modern software development, the ability to handle, aggregate, and format data efficiently is a crucial skill. Whether you're building APIs, data analytics tools, or any data-driven application, understanding how to manipulate data streams effectively can significantly impact your application's performance and maintainability. In this lesson, we'll explore advanced data aggregation techniques in Kotlin, focusing on data formatting and stream operations. We'll start with a basic sales record aggregator and gradually enhance it with more sophisticated features like date-based filtering, statistical aggregation, and multiple output formats including JSON and CSV. Let's dive into the implementation details and explore these concepts hands-on.
To begin, we'll implement a basic sales record aggregator. Here are the methods we'll be focusing on:
addSale(saleId: String, amount: Double): Unit
- Adds a sale record with a unique identifiersaleId
and anamount
. If a sale with the samesaleId
already exists, it updates the amount.getSale(saleId: String): Double?
- Retrieves the sale amount associated with thesaleId
. If the sale does not exist, it returnsnull
.deleteSale(saleId: String): Boolean
- Deletes the sale record with the givensaleId
. Returnstrue
if the sale was deleted andfalse
if the sale does not exist.
Let's now look at how we would implement them.
Here is the complete code for the starter task:
Kotlin1class SalesAggregator { 2 private val sales = mutableMapOf<String, Double>() 3 4 fun addSale(saleId: String, amount: Double) { 5 sales[saleId] = amount 6 } 7 8 fun getSale(saleId: String): Double? { 9 return sales[saleId] 10 } 11 12 fun deleteSale(saleId: String): Boolean { 13 return if (sales.containsKey(saleId)) { 14 sales.remove(saleId) 15 true 16 } else { 17 false 18 } 19 } 20} 21 22// Example Usage 23fun main() { 24 val aggregator = SalesAggregator() 25 26 // Add sales 27 aggregator.addSale("001", 100.50) 28 aggregator.addSale("002", 200.75) 29 30 // Get sale 31 println(aggregator.getSale("001")) // Output: 100.5 32 33 // Delete sale 34 println(aggregator.deleteSale("002")) // Output: true 35 println(aggregator.getSale("002")) // Output: null 36}
Explanation:
- The
sales
property is initialized as a mutable map to store sales records. - The
addSale
method adds a new sale or updates the amount for an existing sale ID. - The
getSale
method retrieves the amount for a given sale ID or returnsnull
if the sale does not exist. - The
deleteSale
method removes the sale record for the given sale ID or returnsfalse
if the sale does not exist.
Now that we have our basic aggregator, let's extend it to include more advanced functionalities.
To increase the complexity and usefulness of our sales aggregator, we'll introduce some new methods. These new methods will handle advanced data aggregation, filtering, and formatting functionalities.
-
aggregateSales(minAmount: Double = 0.0): Map<String, Any>
- Returns a map with the total number of sales and the total amount of sales where the sale amount is aboveminAmount
. The map format looks like this:Kotlin1mapOf( 2 "totalSales" to totalSales, 3 "totalAmount" to totalAmount 4)
-
formatSalesJSON(minAmount: Double = 0.0): String
- Returns the sales data, filtered byminAmount
, formatted as JSON. -
formatSalesCSV(minAmount: Double = 0.0): String
- Returns the sales data, filtered byminAmount
, formatted as CSV with headers. -
addSale(saleId: String, amount: Double, date: String): Unit
- Adds or updates a sale record with a unique identifiersaleId
,amount
, and adate
in the format "YYYY-MM-DD". -
getSalesInDateRange(startDate: String, endDate: String): List<Map<String, Any>>
- Retrieves all sales that occurred within the given date range, inclusive. Each sale includessaleId
,amount
, anddate
.
Let's implement these methods step-by-step.
We'll first modify the addSale
method to accept a date.
Kotlin1private val sales = mutableMapOf<String, Map<String, Any>>() 2 3fun addSale(saleId: String, amount: Double, date: String) { 4 sales[saleId] = mapOf("amount" to amount, "date" to date) 5}
This ensures that each sale record includes a date in addition to the amount.
Now, we create the aggregateSales
method:
Kotlin1fun aggregateSales(minAmount: Double = 0.0): Map<String, Any> { 2 var totalSales = 0 3 var totalAmount = 0.0 4 for (sale in sales.values) { 5 val saleAmount = sale["amount"] as Double 6 if (saleAmount > minAmount) { 7 totalSales++ 8 totalAmount += saleAmount 9 } 10 } 11 return mapOf("totalSales" to totalSales, "totalAmount" to totalAmount) 12}
This method iterates through the sales and sums up those that exceed the minAmount
.
JSON is a lightweight, text-based data format that's easy for humans to read and write, and easy for machines to parse and generate. It consists of:
- Objects (enclosed in curly braces
{}
) - Arrays (enclosed in square brackets
[]
) - Key-value pairs where keys are strings
- Values can be strings, numbers, objects, arrays, booleans, or null
Kotlin1import kotlinx.serialization.* 2import kotlinx.serialization.json.* 3 4@Serializable 5data class SaleRecord(val saleId: String, val amount: Double, val date: String) 6 7fun formatSalesJSON(minAmount: Double = 0.0): String { 8 val filteredSales = sales.filter { it.value["amount"] as Double > minAmount } 9 val salesList = filteredSales.map { SaleRecord(it.key, it.value["amount"] as Double, it.value["date"] as String) } 10 return Json.encodeToString(salesList) 11}
Let's break down these concepts in detail:
@Serializable
andSaleRecord
data class:
Kotlin1@Serializable 2data class SaleRecord(val saleId: String, val amount: Double, val date: String)
-
The
@Serializable
annotation is part of Kotlin's serialization library that automatically generates code to convert objects to and from JSON format. -
When applied to the data class, it tells the Kotlin compiler to create special serialization logic for this class.
-
The data class defines the exact structure that will appear in the JSON output:
- Each property becomes a JSON field
- Property names become JSON keys
- Property types determine the JSON value types
-
JSON Serialization:
Kotlin1Json.encodeToString(salesList)
Json
is a serializer instance from the Kotlin serialization libraryencodeToString
converts the Kotlin object to a JSON string using these steps:- Reads each
SaleRecord
object from the list - Maps each property to its JSON representation
- Formats everything according to JSON syntax (with brackets, quotes, commas)
- For example, a
SaleRecord(saleId="001", amount=100.50, date="2023-01-15")
becomes{"saleId":"001","amount":100.50,"date":"2023-01-15"}
- The entire list is wrapped in square brackets
[]
- Reads each
Example output:
JSON1[ 2 {"saleId":"001","amount":100.50,"date":"2023-01-15"}, 3 {"saleId":"002","amount":200.75,"date":"2023-01-16"}, 4 {"saleId":"003","amount":150.25,"date":"2023-01-17"} 5]
CSV (Comma-Separated Values) is a simple file format used to store tabular data, such as spreadsheets or databases. Each line in a CSV file represents a row of data, and each field within a row is separated by a comma. CSV files are widely used for data exchange because they're simple to create, read, and process across different systems and applications.
Here's the implementation of the formatSalesCSV
method:
Kotlin1fun formatSalesCSV(minAmount: Double = 0.0): String { 2 val header = "saleId,amount,date\n" 3 val filteredSales = sales.filter { it.value["amount"] as Double > minAmount } 4 val rows = filteredSales.map { (saleId, sale) -> 5 "$saleId,${sale["amount"]},${sale["date"]}" 6 }.joinToString("\n") 7 8 return header + rows 9}
Example output:
csv1saleId,amount,date 2001,100.50,2023-01-15 3002,200.75,2023-01-16 4003,150.25,2023-01-17
Let's break down how the code works:
val header = "saleId,amount,date\n"
- Defines the CSV column headers.val filteredSales = sales.filter { ... }
- Filters sales above the minimum amount.val rows
transforms filtered sales into CSV format by mapping each sale to a comma-separated string (with saleId, amount, and date) and joins them with newlines between rows.return header + rows
- Combines headers and data into final CSV string.
This creates a standard CSV file format that's compatible with spreadsheets and data analysis tools.
Finally, let's implement the getSalesInDateRange
method:
Kotlin1import java.time.LocalDate 2import java.time.format.DateTimeFormatter 3 4fun getSalesInDateRange(startDate: String, endDate: String): List<Map<String, Any>> { 5 val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") 6 val start = LocalDate.parse(startDate, formatter) 7 val end = LocalDate.parse(endDate, formatter) 8 return sales.filter { 9 val date = LocalDate.parse(it.value["date"] as String, formatter) 10 date in start..end 11 }.map { 12 mapOf("saleId" to it.key, "amount" to it.value["amount"]!!, "date" to it.value["date"]!!) 13 } 14}
This method filters sales records within a specified date range. It first converts the input date strings into LocalDate
objects using a formatter. The function then filters the sales by checking if each sale's date falls within the specified range (inclusive). Finally, it maps the filtered results into a list of maps, where each map contains the sale ID, amount, and date. The !!
operators are used because we're certain these values exist in our data structure.
Congratulations! You've now extended a basic sales aggregator to an advanced one capable of filtering, aggregating, and formatting data in JSON and CSV using Kotlin. These skills are crucial for handling data efficiently, especially when dealing with large datasets. Feel free to experiment with similar challenges to reinforce your understanding. Well done, and see you in the practice!