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:

class SalesAggregator {
    private val sales = mutableMapOf<String, Double>()

    fun addSale(saleId: String, amount: Double) {
        sales[saleId] = amount
    }

    fun getSale(saleId: String): Double? {
        return sales[saleId]
    }

    fun deleteSale(saleId: String): Boolean {
        return if (sales.containsKey(saleId)) {
            sales.remove(saleId)
            true
        } else {
            false
        }
    }
}

// Example Usage
fun main() {
    val aggregator = SalesAggregator()

    // Add sales
    aggregator.addSale("001", 100.50)
    aggregator.addSale("002", 200.75)

    // Get sale
    println(aggregator.getSale("001"))  // Output: 100.5

    // Delete sale
    println(aggregator.deleteSale("002"))  // Output: true
    println(aggregator.getSale("002"))  // Output: null
}

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:

    mapOf(
        "totalSales" to totalSales,
        "totalAmount" to totalAmount
    )
  • 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.

private val sales = mutableMapOf<String, Map<String, Any>>()

fun addSale(saleId: String, amount: Double, date: String) {
    sales[saleId] = mapOf("amount" to amount, "date" to date) 
}

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:

fun aggregateSales(minAmount: Double = 0.0): Map<String, Any> {
    var totalSales = 0
    var totalAmount = 0.0
    for (sale in sales.values) {
        val saleAmount = sale["amount"] as Double
        if (saleAmount > minAmount) {
            totalSales++
            totalAmount += saleAmount
        }
    }
    return mapOf("totalSales" to totalSales, "totalAmount" to totalAmount)
}

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
import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
data class SaleRecord(val saleId: String, val amount: Double, val date: String)

fun formatSalesJSON(minAmount: Double = 0.0): String {
    val filteredSales = sales.filter { it.value["amount"] as Double > minAmount }
    val salesList = filteredSales.map { SaleRecord(it.key, it.value["amount"] as Double, it.value["date"] as String) }
    return Json.encodeToString(salesList)
}

Let's break down these concepts in detail:

  • @Serializable and SaleRecord data class:
@Serializable
data 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:

Json.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:

[
    {"saleId":"001","amount":100.50,"date":"2023-01-15"},
    {"saleId":"002","amount":200.75,"date":"2023-01-16"},
    {"saleId":"003","amount":150.25,"date":"2023-01-17"}
]

Step 4: Implementing the 'formatSalesCSV' Method

Step 5: Implementing the 'getSalesInDateRange' Method

Finally, let's implement the getSalesInDateRange method:

import java.time.LocalDate
import java.time.format.DateTimeFormatter

fun getSalesInDateRange(startDate: String, endDate: String): List<Map<String, Any>> {
    val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
    val start = LocalDate.parse(startDate, formatter)
    val end = LocalDate.parse(endDate, formatter)
    return sales.filter { 
        val date = LocalDate.parse(it.value["date"] as String, formatter)
        date in start..end 
    }.map {
        mapOf("saleId" to it.key, "amount" to it.value["amount"]!!, "date" to it.value["date"]!!)
    }
}

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!

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal