skip to content

Combine zip, unzip, windowed, and flatten to build a small pipeline: given two parallel lists of timestamps and temperatures, compute per-interval rate-of-change. Walk through your operator choices.

level: seniorimportance: nice to knowfreq 22%

answer

  1. zip = align parallel columns by index
  2. zipWithNext/windowed(2) = neighbor deltas
  3. unzip = inverse of zip (split columns)
  4. flatten = un-nest grouping output
  5. asSequence first for streaming/large data

basics

~20 s

Pair the two lists with zip so each reading is one (time, temp). Slide a 2-wide window over the readings to get consecutive pairs, then for each pair divide the temperature change by the time change to get the rate. unzip can split paired data back out when needed.

solid answer

~40 s

Start by aligning the parallel data: timestamps.zip(temps) gives List<Pair<Long, Double>> (one reading each, truncated to the shorter). Then take consecutive readings — zipWithNext() (the adjacent-pair special case of windowed(2)) — and map each (prev, next) to (next.temp - prev.temp) / (next.time - prev.time). Using zipWithNext { a, b -> ... } avoids allocating the intermediate Pair list. If you instead had a List<Pair> and needed the raw columns, unzip() recovers Pair<List<Long>, List<Double>>. flatten() would re-concatenate any List<List<...>> produced along the way (e.g. if you grouped into windows). For large/streaming input, asSequence() first so zip/windowed run lazily. The key design points: zip aligns columns, zipWithNext/windowed(2) gives neighbor deltas, unzip is the column-splitter inverse of zip, flatten collapses nesting.

code

kotlin · 11 lines
kotlin
val times = listOf(0L, 10L, 30L)
val temps = listOf(20.0, 25.0, 28.0)

val rates = times.zip(temps)                       // [(0,20.0),(10,25.0),(30,28.0)]
    .zipWithNext { (t0, c0), (t1, c1) ->
        (c1 - c0) / (t1 - t0)
    }
println(rates)                                     // [0.5, 0.15]

val readings = times.zip(temps)
val (ts, cs) = readings.unzip()                    // ([0,10,30], [20.0,25.0,28.0])

go deeper

for a junior

Can zip two lists and knows windowed gives neighbors but may not assemble the full pipeline.

for a middle

Composes zip + zipWithNext correctly and knows unzip is the inverse.

for a senior

Chooses transform overloads to avoid allocation, handles edge cases, and adds asSequence for scale.

for a principal

Reasons about pipeline design, lazy streaming telemetry, operator selection trade-offs, and where a manual stateful fold would beat operator chaining.

## The problem Two parallel lists — `times: List<Long>` and `temps: List<Double>` — and we want the **rate of change** per interval: `(temp_next - temp_prev) / (time_next - time_prev)`. ## Step 1 — align columns with zip The two lists are **parallel** (index i in each belongs together). `zip` turns them into one stream of readings: ```kotlin val readings: List<Pair<Long, Double>> = times.zip(temps) // [(t0, c0), (t1, c1), ...] truncated to the shorter list ``` `zip` is the right tool because it merges **by position**, exactly how parallel arrays line up. ## Step 2 — consecutive pairs with zipWithNext / windowed(2) Rate of change needs **adjacent readings**. `zipWithNext()` yields `(reading_i, reading_{i+1})` — the clean adjacent-pair case of `windowed(2)`: ```kotlin val rates: List<Double> = readings.zipWithNext { (t0, c0), (t1, c1) -> (c1 - c0) / (t1 - t0) } ``` The **transform overload** maps each neighbor pair directly to a `Double`, so no intermediate `Pair`/sublist list is built. Equivalent with `windowed`: ```kotlin readings.windowed(2) { (a, b) -> (b.second - a.second) / (b.first - a.first) } ``` ## Step 3 — unzip when you need the raw columns back If downstream code wants the columns separately again, `unzip()` is the **inverse of zip**: ```kotlin val (allTimes, allTemps) = readings.unzip() // Pair<List<Long>, List<Double>> ``` ## Step 4 — flatten if you produced nested groups If an intermediate step grouped readings (e.g. `chunked` per hour producing `List<List<...>>`), `flatten()` collapses one nesting level back to a flat stream: ```kotlin readings.chunked(60).flatten() // back to the flat reading list ``` ## Scaling up For large or streaming telemetry, prefix with `.asSequence()` so `zip` and `windowed`/`zipWithNext` run **lazily** and only the consumed deltas are computed: ```kotlin times.asSequence().zip(temps.asSequence()) .zipWithNext { (t0, c0), (t1, c1) -> (c1 - c0) / (t1 - t0) } .take(100) .toList() ``` ## Operator-choice summary - **zip** — align two parallel lists into combined readings (by index, truncating). - **zipWithNext / windowed(2)** — adjacent neighbor pairs for deltas/rates. - **unzip** — split combined pairs back into separate columns (inverse of zip). - **flatten** — collapse one level of nesting from any grouping step.

  • Why zip the two lists before zipWithNext rather than the reverse?
    zip first binds each time to its temperature into one reading; then zipWithNext compares whole readings. Reversing would pair raw values within one list, losing the time-temp association.
  • What guards do you need if the input might be empty or single-element?
    zipWithNext/windowed(2) return empty for size < 2 (and zip truncates to the shorter), so the rate list is simply empty — no exception, but handle the empty result downstream.

Like merging two spreadsheet columns into rows (zip), then comparing each row to the one below it (zipWithNext) to compute slopes.

saying these in an interview costs you the question

  • Using windowed(2) but forgetting partialWindows default while expecting a trailing single reading
  • Computing deltas before zipping, breaking the time-temp pairing
  • Allocating intermediate Pair lists instead of using the transform overload
  • Assuming zip pads when the two input lists differ in length
  • Materializing a huge List before windowing instead of using asSequence

context