Saying stuff about stuff.

Challenge: find the longest time gap

Very occasionally I’m inspired by a code challenge, this time it’s finding the longest gap in minutes between consecutive timestamps (via @andycroll). Here’s my approach:

def find_longest_time_gap(times)
  times
    .map { _1.split(':', 2).map(&:to_i) }
    .map { |(h, m)| h * 60 + m }
    .sort
    .each_cons(2)
    .map { |(a, b)| b - a }
    .max || 0
end

find_longest_time_gap(['14:00', '09:00', '15:00', '10:30'])
# => 210

Breaking it down.

I’m using a “numbered parameter” _1 (which I kinda like more than the newer it 😬 though I’m not sure I’d use either in “proper” code). This is split into two-and-only-two parts (it’s maybe a bit defensive but I like that it’s explicit) and turned into integers to get an array of hours/minutes tuples:

.map { _1.split(':', 2).map(&:to_i) }
# => [[14, 0], [9, 0], [15, 0], [10, 30]]

They’re converted to minutes. Here I’m expanding each two-element tuple/array block argument into two variables (I’m not sure what this is called) which is helpful in these simple cases because it turns it into a neat single line:

.map { |(h, m)| h * 60 + m }
# => [840, 540, 900, 630]

The original times may not be in order so we need to sort them:

.sort
# => [540, 630, 840, 900]

Ruby has a wealth of useful methods – I’m thinking of chunk_while – so I knew there would be something to help me out. I cheated a little and peeked at Andy’s solution 👀 which revealed that this time it’s each_cons (its name was bound to contain an underscore).

each_cons is one of those methods I find slightly weird. It’s called “each” which suggests a side-effect rather than something being returned, but calling it without a block returns an Enumerator which I think I’d want more often (map_cons?).

Here’s what each_cons(n) does:

# Original array
[1, 2, 3, 4, 5]

# each_cons(2)
[1, 2]
   [2, 3]
      [3, 4]
         [4, 5]

# each_cons(3)
[1, 2, 3]
   [2, 3, 4]
      [3, 4, 5]

So here’s the each_cons(2) magic:

.each_cons(2)
# => [[540, 630], [630, 840], [840, 900]]

Now we can map over these to get the differences:

.map { |(a, b)| b - a }
# => [90, 210, 60]

Then get the maximum value. If the array has fewer items than the number passed to each_cons then the returned Enumerator will be empty and the call to max will return nil so we should default to zero:

.max || 0
# => 210

Update: I realised I hadn’t read the question fully and had an incorrect assumption that the times would always be sorted. I fixed that, and the case where there’s only one time (or zero) and therefore no gap, by adding some tests:

require 'minitest/autorun'

class TestMe < Minitest::Test
  def test_cases
    assert_equal 0, find_longest_time_gap(['12:00'])
    assert_equal 120, find_longest_time_gap(['09:00', '11:00'])
    assert_equal 210, find_longest_time_gap(['14:00', '09:00', '15:00', '10:30'])
    assert_equal 240, find_longest_time_gap(['08:00', '10:00', '10:00', '14:00'])
  end
end