skip to content

In Ruby, how would you pair each month's franchise sales with the same month last year and group consecutive months of growth?

level: middleimportance: nice to knowfreq 20%

answer

  1. zip pairs by position
  2. receiver's length wins
  3. shorter side padded with nil
  4. chunk_while splits where false
  5. slice_when splits where true

basics

~20 s

this_year.zip(last_year) pairs values by position, keeping the receiver's length and padding a shorter argument with nil. chunk_while then groups adjacent elements while its two-element block stays true; slice_when is the same but splits where its block is true.

solid answer

~40 s

`zip` pairs elements **by position**: `this_year.zip(last_year)` returns `[[jan, jan_before], ...]`, always as long as the **receiver**. A shorter argument is padded with `nil`, and extra elements in a longer one are dropped, so a missing month turns into a `nil` that later raises `NoMethodError` when compared. With a block, `zip` yields each pair and returns `nil`. To find runs, `chunk_while { |a, b| ... }` compares each **adjacent pair** and starts a new chunk wherever the block returns false; `slice_when` is its mirror image and starts one where the block returns true. Both return an **Enumerator** of arrays, so call `to_a` or keep chaining. For growth runs: `growth.chunk_while { |a, b| a == b }` groups consecutive equal flags.

code

ruby · 14 lines
ruby
this_year = [120, 135, 150, 140]
last_year = [100, 140, 130]            # December is missing

this_year.zip(last_year)
# => [[120, 100], [135, 140], [150, 130], [140, nil]]

this_year.zip(last_year) { |pair| p pair }  # => nil

growth = [true, false, true, true]
growth.chunk_while { |a, b| a == b }.to_a   # => [[true], [false], [true, true]]
growth.slice_when  { |a, b| a != b }.to_a   # => [[true], [false], [true, true]]

[1, 2, 4, 5, 7].chunk_while { |a, b| b == a + 1 }.to_a
# => [[1, 2], [4, 5], [7]]

go deeper

for a junior

Recall that zip pairs elements by position and that chunk_while groups neighbours into runs.

for a middle

Explain zip's receiver-length and nil-padding rules, and how chunk_while and slice_when decide where a chunk starts.

for a senior

Validate lengths before zipping data from different sources, since a nil pad fails far from the missing row.

for a principal

Treat positional alignment between datasets as a contract, keyed by month rather than by index where sources can drift.

## Pairing months with `zip` `zip` combines the receiver with one or more other collections **by position**. Element `n` of the result is an array holding element `n` of each input: ```ruby this_year = [120, 135, 150, 140] last_year = [100, 140, 130, 120] this_year.zip(last_year) # => [[120, 100], [135, 140], [150, 130], [140, 120]] ``` Its length rules are fixed by the receiver: 1. The result has **exactly as many pairs as the receiver** has elements. 2. If an argument is **shorter**, the missing positions are filled with **`nil`**. 3. If an argument is **longer**, its extra elements are **ignored**. 4. With a block, `zip` yields each combined array to the block and returns **`nil`**, not the pairs. Rule 2 is the production trap. If last year's data is missing a month, the comparison `now > before` meets `nil` and raises `NoMethodError` in a place that looks unrelated to the data gap. Checking lengths before zipping, or treating `nil` explicitly, keeps the failure close to its cause. `zip` accepts several arguments at once, e.g. `this_year.zip(last_year, targets)` gives triples. ## Flagging growth Once the months are paired, a per-month flag is one `map` away: ```ruby growth = this_year.zip(last_year).map { |now, before| now > before } # => [true, false, true, true] ``` ## Grouping runs with `chunk_while` `chunk_while` takes a block with **two** parameters, the element before and the element after, and calls it for each **adjacent pair**, the collection's length minus one times. Wherever the block returns **false**, a new chunk begins: ```ruby growth.chunk_while { |a, b| a == b }.to_a # => [[true], [false], [true, true]] ``` The last chunk is a two-month growth streak. The classic example in the rdoc groups consecutive integers: `[1, 2, 4, 9, 10, 11].chunk_while { |i, j| i + 1 == j }` yields `[1, 2]`, `[4]` and `[9, 10, 11]`. ## `slice_when`: the mirror image `slice_when` works the same way but starts a new chunk where the block returns **true**. So these are equivalent: - `growth.chunk_while { |a, b| a == b }` - `growth.slice_when { |a, b| a != b }` Choose whichever makes the block read naturally: "keep going while consecutive" or "cut when there is a gap". | Method | Block receives | New chunk starts when | Returns | |---|---|---|---| | `chunk_while` | two adjacent elements | block is false | an `Enumerator` of arrays | | `slice_when` | two adjacent elements | block is true | an `Enumerator` of arrays | ## Things to remember - Both return an **`Enumerator`**, not an array; call `to_a`, `map` or `each` on it. Inspecting it directly shows an enumerator, which confuses people in a console session. - Every element ends up in exactly one chunk; nothing is dropped. - They compare **neighbours only**. Grouping all equal values regardless of position is a different operation. ## Pairing by key instead of position `zip` trusts that element `n` of each input describes the same month. That holds when both lists come from the same query with the same ordering, and fails quietly when one source skips a month or orders differently. Two safer patterns: - Build lookups first, `last = months.zip(last_year).to_h`, then read `last[month]` for each month of this year, so a missing month is an explicit `nil` at a known key. - Assert equal lengths before zipping, and fail loudly if they differ. `zip` also accepts **several** arguments at once, `this_year.zip(last_year, targets)`, returning triples; the receiver-length rule applies to each argument. ## Putting it together A compact pipeline for the report's "longest growth streak": ```ruby this_year.zip(last_year) .map { |now, before| now > before } .chunk_while { |a, b| a == b } .select(&:first) .map(&:size) .max ``` Each step does one thing: pair by month, flag growth, group runs, keep the growth runs, measure them, take the longest.

  • What happens if last_year is longer than this_year?
    The extra elements are ignored: `zip` always returns as many combined arrays as the receiver has elements. If you need every element of both, zip from the longer collection, or check the lengths first and handle the gap explicitly.
  • How does chunk_while differ from each_cons(2)?
    `each_cons(2)` yields every overlapping pair and returns nothing grouped; it is for looking at neighbours. `chunk_while` also looks at neighbours, but uses the answers to cut the sequence into runs, and returns an enumerator of those runs, each element appearing in exactly one of them.

saying these in an interview costs you the question

  • zip raises an error when the arrays have different lengths
  • zip with a block returns the array of pairs
  • chunk_while splits where the block returns true
  • chunk_while returns an array of arrays directly
  • zip drops the receiver's extra elements when the argument is shorter