Lesson 3
Mastering Data Aggregation and Streams with Data Formatting in Kotlin
Introduction

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.

Starter Task Methods and Their Definitions

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 identifier saleId and an amount. If a sale with the same saleId already exists, it updates the amount.
  • getSale(saleId: String): Double? - Retrieves the sale amount associated with the saleId. If the sale does not exist, it returns null.
  • deleteSale(saleId: String): Boolean - Deletes the sale record with the given saleId. Returns true if the sale was deleted and false if the sale does not exist.

Let's now look at how we would implement them.

Starter Task Solution

Here is the complete code for the starter task:

Kotlin
1class 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 returns null if the sale does not exist.
  • The deleteSale method removes the sale record for the given sale ID or returns false if the sale does not exist.

Now that we have our basic aggregator, let's extend it to include more advanced functionalities.

New Methods and Their Definitions

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 above minAmount. The map format looks like this:

    Kotlin
    1mapOf( 2 "totalSales" to totalSales, 3 "totalAmount" to totalAmount 4)
  • formatSalesJSON(minAmount: Double = 0.0): String - Returns the sales data, filtered by minAmount, formatted as JSON.

  • formatSalesCSV(minAmount: Double = 0.0): String - Returns the sales data, filtered by minAmount, formatted as CSV with headers.

  • addSale(saleId: String, amount: Double, date: String): Unit - Adds or updates a sale record with a unique identifier saleId, amount, and a date 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 includes saleId, amount, and date.

Let's implement these methods step-by-step.

Step 1: Enhancing the 'addSale' Method to Include Date

We'll first modify the addSale method to accept a date.

Kotlin
1private 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.

Step 2: Implementing the 'aggregateSales' Method

Now, we create the aggregateSales method:

Kotlin
1fun 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.

Step 3: Implementing the 'formatSalesJSON' Method

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
Kotlin
1import 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 and SaleRecord data class:
Kotlin
1@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:

Kotlin
1Json.encodeToString(salesList)
  • Json is a serializer instance from the Kotlin serialization library
  • encodeToString 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 []

Example output:

JSON
1[ 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]
Step 4: Implementing the 'formatSalesCSV' Method

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:

Kotlin
1fun 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:

csv
1saleId,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:

  1. val header = "saleId,amount,date\n" - Defines the CSV column headers.
  2. val filteredSales = sales.filter { ... } - Filters sales above the minimum amount.
  3. 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.
  4. 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.

Step 5: Implementing the 'getSalesInDateRange' Method

Finally, let's implement the getSalesInDateRange method:

Kotlin
1import 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.

Lesson Summary

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!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.