Featured resource
2026 Tech Forecast
2026 Tech Forecast

1,500+ tech insiders, business leaders, and Pluralsight Authors share their predictions on what’s shifting fastest and how to stay ahead.

Download the forecast
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
    • Core Tech
Labs

Analyze Weather Data with Kotlin

In this Code Lab, you'll turn a raw CSV of historical weather readings into a working Kotlin analysis tool. You'll model the data as idiomatic Kotlin data classes, filter and summarize temperature, humidity, and pressure trends across a date range, and use standard library collection operations to flag notable anomalies. By the end, you'll have a command-line program that turns raw weather data into a clear, exportable report.

Lab platform
Lab Info
Level
Intermediate
Last updated
Jul 14, 2026
Duration
45m

Contact sales

By clicking submit, you agree to our Privacy Policy and Terms of Use, and consent to receive marketing emails from Pluralsight.
Table of Contents
  1. Challenge

    Step 1: Model the weather data

    Introduction

    A folder full of comma-separated weather readings doesn't tell you much on its own. Before you can filter it, summarize it, or spot anything unusual in it, you need a way to treat each row as more than just text.

    In this lab, you'll build a Kotlin command-line tool that turns a raw weather station export into a working analysis pipeline: parsing daily readings into typed objects, narrowing them to a reporting window, computing summary statistics, flagging temperature anomalies, and writing the results back out to CSV and text files.

    To start, you'll give the CSV's rows a real shape by modeling each day's reading as a Kotlin type, then teach your program how to read that shape out of the file.

    The application directory already contains a Gradle-based Kotlin project with Main.kt ready for your implementation.

    info> Feeling stuck? Check out the matching solution/stepN/ folder for the step you're on to see a working implementation. Give it a try on your own first. The solution folder is your safety net, not your starting point. ### Modeling a single weather reading

    Kotlin's data class modifier is the idiomatic way to represent a type whose entire purpose is to carry a fixed set of values, rather than provide behavior.

    Adding data to a class declaration gives you structural equality for free: two instances with the same property values are equal to each other, and the compiler generates readable toString, hashCode, and copy implementations for you. Without data, a class falls back to reference equality, so two instances with identical property values are still considered different objects.

    data class Point(val x: Int, val y: Int)
    

    Here, Point(1, 2) == Point(1, 2) is true because the class is a data class. If the data keyword were removed, the same comparison would be false.

    warning> The lab environment on the right may take up to a minute to finish loading. Continue after the terminal and project files appear. ### Turning CSV lines into typed objects

    Kotlin's File type exposes convenient extension functions for reading a file's contents as a List<String>, one entry per line, using readLines().

    Since the first line of a CSV file is usually a header rather than data, you typically want to drop it before processing the remaining lines. From there, splitting each line on its delimiter and converting the resulting string fields with functions like LocalDate.parse and toDouble() lets you turn a purely textual row into a fully typed object.

    fun loadItems(path: String): List<Item> =
        File(path).readLines()
            .drop(1)
            .map { line ->
                val fields = line.split(",")
                Item(fields[0], fields[1].toDouble())
            }
    

    The shape above reads a file, skips its header row, and maps each remaining line into a typed instance by splitting on commas and converting the fields you need. info> This lab experience was developed by the Pluralsight team using an internally developed AI tool. All sections were verified by human experts for accuracy prior to publication. However, content may still contain errors or inaccuracies, and we recommend independent verification.

    To report a problem or provide feedback, click here. Feedback may be used to improve accuracy in accordance with our Privacy Policy.

  2. Challenge

    Step 2: Filter by date range

    With a full list of typed WeatherRecord readings in hand, you can start narrowing that data down to the window a report actually needs. Here, you'll write a function that filters the parsed records down to a given start and end date.

    Because LocalDate implements Comparable, Kotlin lets you express an inclusive date range directly with the .. range operator and check membership with in, rather than chaining isAfter and isBefore comparisons by hand.

    By the end of this step, you'll be able to hand any date range to your program and get back just the readings that fall inside it. ### Filtering with ranges and predicates

    Kotlin lets you build a range directly from two Comparable values using the .. operator, and check whether a value falls inside that range with the in keyword.

    Combined with the standard library's filter function, this gives you a concise way to keep only the elements of a collection whose property falls within a given range, without writing manual comparison logic.

    val inRange = items.filter { it.value in lowerBound..upperBound }
    

    This reads naturally: for each item, check whether its value lies within the closed range from lowerBound to upperBound, keeping only the ones that do.

  3. Challenge

    Step 3: Calculate summary statistics

    With the ability to narrow readings to a specific window, the next step is turning that filtered list into numbers that summarize the period.

    Here, you'll model a summary result and compute the average, minimum, and maximum temperature alongside average humidity and pressure for a set of records, using Kotlin's standard library aggregate functions.

    By the end of this step, your program will be able to turn any filtered list of readings into a concise statistical summary. ### Modeling a summary result

    Just like an individual reading, a summary of many readings is a good candidate for a data class: it exists purely to carry a fixed set of computed values, and its equality should be based on those values rather than on object identity.

    data class Range(val min: Int, val max: Int)
    

    Two Range instances built from the same min and max values compare equal to each other because the type is declared as a data class. ### Aggregating a collection with the standard library

    Kotlin's standard library provides ready-made aggregate functions for numeric collections, so you rarely need to write a manual loop to compute a sum, average, minimum, or maximum.

    Calling average(), min(), or max() directly on a List<Double>, often after mapping to just the property you need, gives you these statistics in a single expression.

    val prices = items.map { it.price }
    val averagePrice = prices.average()
    val cheapest = prices.min()
    val priciest = prices.max()
    

    Mapping first to the specific property you want to aggregate, then calling the aggregate function, keeps each calculation focused and easy to read.

  4. Challenge

    Step 4: Detect trends and anomalies

    With a way to summarize a set of readings, the program can now use that summary as a baseline for spotting what stands out.

    Here, you'll write two functions: one that flags individual days whose temperature deviates sharply from the period average, and one that classifies whether the period as a whole is trending warmer or cooler by comparing its earlier and later halves.

    By the end of this step, your program will be able to surface both isolated anomalies and a general temperature trend for any filtered range of readings. ### Measuring deviation from an average

    Once you have an average value for a collection, you can flag individual elements that stray too far from it by comparing the absolute difference between each element's value and the average against a threshold.

    Kotlin's kotlin.math.abs function returns the absolute value of a number, which is exactly what you need to measure deviation regardless of whether a value is above or below the average.

    val outliers = items.filter { abs(it.value - average) > threshold }
    

    Filtering on the absolute difference, rather than checking separately for values above and below the average, keeps the anomaly check to a single comparison. ### Comparing two halves of a sorted collection

    To compare an earlier period against a later one, you first need the records in chronological order, which sortedBy gives you by sorting on a chosen property.

    From a sorted list, take(n) returns its first n elements and drop(n) returns everything after them, letting you split one list into two halves without any manual indexing.

    val sorted = items.sortedBy { it.date }
    val half = sorted.size / 2
    val earlier = sorted.take(half)
    val later = sorted.drop(half)
    

    Computing an aggregate like average() separately on earlier and later gives you two numbers you can compare to classify the overall direction of change.

  5. Challenge

    Step 5: Export the results

    With records filtered, summarized, and analyzed for trends and anomalies, the only piece left is turning those results into files the station's operators can actually read.

    Here, you'll write the filtered records back out as a CSV file and write a separate plain-text report containing the summary statistics, trend classification, and any flagged anomalies, then wire every piece built so far together behind a runnable command-line entry point.

    By the end of this step, running the program end to end will produce both output files from a real weather CSV export. ### Writing text to a file

    Kotlin's File type also provides extension functions for writing content back out, mirroring the ones used for reading.

    writeText takes a single string and writes it to the given path, replacing any existing content, which makes it a natural fit for output built up as a list of lines joined together with newline characters.

    val lines = listOf(header) + rows
    File(path).writeText(lines.joinToString("\n") + "\n")
    

    Joining a header and a list of row strings together before writing keeps the file-writing logic itself simple, independent of how each row is formatted. ### Building a multi-line report from parts

    A plain-text report is often easiest to build as a list of individual lines, each one added as you go, then joined together at the end.

    Kotlin's string templates let you embed a value directly inside a string using $name or ${expression}, which keeps each line's formatting readable without manual string concatenation.

    val total = items.sumOf { it.amount }
    val lines = mutableListOf<String>()
    lines.add("Total: $total")
    lines.add(if (items.isEmpty()) "None" else items.joinToString("\n"))
    

    Building the report this way lets you add each piece, such as summary values, a classification, or a list of flagged entries, as its own line before writing the whole thing to disk. ### Reading flag/value command-line arguments

    Command-line arguments arrive as a flat array, but a CLI that accepts --flag value pairs is easier to read from a map keyed by flag name. chunked(2) groups a list into sublists of two consecutive elements at a time, turning a flat sequence of alternating flags and values into a list of flag/value pairs. Calling associate on that list, destructuring each pair into (flag, value), then builds a single map from all of them.

    val options = args.toList().chunked(2).associate { (flag, value) -> flag to value }
    val target = options["--target"]
    

    Given arrayOf("--target", "foo"), chunked(2) first produces [["--target", "foo"]], and associate turns that into the map {"--target" to "foo"}, so options["--target"] returns "foo", without a manual loop or if/else chain. ### Running the CLI with arguments

    The Gradle application plugin's run task launches your program's main function, and any arguments after --args="..." are passed straight through as the array main receives. Wrapping the whole argument string in quotes lets it contain spaces and multiple --flag value pairs.

    ./gradlew run --args="--target foo"
    

    This runs the program with arrayOf("--target", "foo") passed to main. Nice work. You built a complete Kotlin command-line tool that reads a weather station's CSV export, models each reading as a typed data class, and works through filtering, summarizing, and flagging anomalies before writing the results back out to disk.

    Along the way, you practiced idiomatic Kotlin patterns for working with real data: data class types for structural equality, range-based filtering with .. and in, standard library aggregate functions for computing statistics, and file I/O for both reading and writing. You also wired several independent functions together into a single, runnable pipeline driven by command-line arguments.

    When you ran the CLI against the sample weather data, you saw that pipeline in action: a filtered CSV file and a text report, both generated from a single command, summarizing a real, if fictional, month of weather readings.

About the author

Pluralsight’s AI authoring technology is designed to accelerate the creation of hands-on, technical learning experiences. Serving as a first-pass content generator, it produces structured lab drafts aligned to learning objectives defined by Pluralsight’s Curriculum team. Each lab is then enhanced by our Content team, who configure the environments, refine instructions, and conduct rigorous technical and quality reviews. The result is a collaboration between artificial intelligence and human expertise, where AI supports scale and efficiency, and Pluralsight experts ensure accuracy, relevance, and instructional quality, helping learners build practical skills with confidence.

Real skill practice before real-world application

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Learn by doing

Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.

Follow your guide

All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.

Turn time into mastery

On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.

Get started with Pluralsight