Attached is my solution (in progress). Before I get onto the analysis
reports, I'm still trying to simplify the data post parsing. But, at
least it's getting there. :)

As you can see from the results, I'm only properly pulling data from
the first day. This is why I enjoy quizzes; I'm sure I tried to fix
this same problem at some point.

#!/usr/bin/env ruby
WHEELBASE_AVG    = 100    # inches
INCHES_PER_MILE  = 63_360
SECONDS_PER_HOUR = 3600.0
INCHES_PER_SECOND_TO_MPH = 17.6

USAGE = <<ENDUSAGE
Usage:
   vehicle_counter [-t time_segment] [-a] data_file
   -t,--time     the number of minutes per segment (defaults to 60)
   -a,--average  average time samples across days
                 (defaults to showing each day indpendently)
ENDUSAGE

ARGS = {
  :time_segment => 60,
  #:data_file    => 'vehicle_counter.data'
}
UNFLAGGED_ARGS = [ :data_file ]
next_arg = UNFLAGGED_ARGS.first
ARGV.each{ |arg|
   case arg
     when '-t','--time'
       next_arg = :time_segment
     when '-a','--average'
       ARGS[:average] = true
     else
       if next_arg
         if next_arg==:time_segment
           arg = arg.to_i
         end
         ARGS[next_arg] = arg
         UNFLAGGED_ARGS.delete( next_arg )
       end
       next_arg = UNFLAGGED_ARGS.first
   end
}

if !ARGS[:data_file] || !ARGS[:time_segment]
   puts USAGE
   exit
end

class Record
  SECONDS_PER_DAY = 3600 * 24
  attr_reader :time, :direction, :ms
  attr_accessor :speed
  def initialize( str, day_offset )
    _, @direction, @ms = /([AB])(\d+)/.match( str ).to_a
    @ms = @ms.to_i
    @time = Time.gm( 2007 ) + ( @ms.to_i / 1000.0 ) + ( day_offset *
SECONDS_PER_DAY )
  end
end


# Prepare data
raw_data = IO.readlines( ARGS[:data_file] )
day_changes = 0
records = raw_data.inject([]){ |records,line|
  record = Record.new( line, ARGS[:average] ? 0 : day_changes )
  if (last_record = records.last) && (record.ms < last_record.ms)
    day_changes += 1
  end
  records << record
}

# Convert axle pairs to speed
last_record = {}
records.each{ |record|
  if last_axle = last_record[ record.direction ]
    last_axle.speed  = WHEELBASE_AVG / ( record.time -
last_axle.time )
    last_axle.speed /= INCHES_PER_SECOND_TO_MPH
    last_record[ record.direction ] = nil
  else
    last_record[ record.direction ] = record
  end
}
records.delete_if{ |r| r.speed.nil? }

# Figure out which direction gets double hits
possible_directions  = records.map{ |r| r.direction }.uniq
double_hit_direction = possible_directions.map{ |dir|
  [ records.select{ |r| r.direction == dir }.length , dir ]
}.sort.last.last

# Remove extraneous records
require 'enumerator'
records.each_cons(2){ |r1,r2|
  if (r1.direction == double_hit_direction) &&
     (r2.direction != double_hit_direction) &&
     # 0.02 seconds @ 50mph is ~18 inches
     # If the times are this close, it must be a double hit
     (r1.time - r2.time).abs < 0.02
    r1.speed = nil
  end
}
records.delete_if{ |r| r.speed.nil? }

t1 = Time.gm(0)
ms_trigger = 0
slot_count = nil
ms_per_slot = ARGS[ :time_segment ] * 60 * 1000
records << Record.new( 'B99999999', 0 )
records.each{ |r|
  if r.ms >= ms_trigger
    if slot_count
      t2 = t1 + ms_per_slot / 1000.0
      print "#{t1.strftime('%H:%M')}..#{t2.strftime('%H:%M')} : "
      puts "%4i %s, %4i %s" % possible_directions.map{ |dir|
        [slot_count[dir],dir=="A" ? "left" : "right"]
      }.flatten
      t1 = t2
    end
    slot_count = Hash[ *possible_directions.map{ |d| [d,0] }.flatten ]
    ms_trigger += ms_per_slot
  end
  slot_count[ r.direction ] += 1
}


Slim2:Code phrogz$ ruby vehicle_counter.rb vehicle_counter.data -t 15
00:00..00:15 :    2 left,    1 right
00:15..00:30 :    1 left,    4 right
00:30..00:45 :    3 left,    2 right
00:45..01:00 :    1 left,    1 right
01:00..01:15 :    0 left,    2 right
01:15..01:30 :    0 left,    1 right
01:30..01:45 :    0 left,    3 right
01:45..02:00 :    1 left,    1 right
02:00..02:15 :    0 left,    1 right
02:15..02:30 :    1 left,    2 right
02:30..02:45 :    0 left,    2 right
02:45..03:00 :    1 left,    1 right
03:00..03:15 :    0 left,    3 right
03:15..03:30 :    0 left,    2 right
03:30..03:45 :    1 left,    1 right
03:45..04:00 :    1 left,    1 right
04:00..04:15 :    1 left,    3 right
04:15..04:30 :    0 left,    1 right
04:30..04:45 :    0 left,    1 right
04:45..05:00 :    1 left,    0 right
05:00..05:15 :    0 left,    5 right
05:15..05:30 :    0 left,   15 right
05:30..05:45 :    1 left,    4 right
05:45..06:00 :    2 left,    8 right
06:00..06:15 :   13 left,    8 right
06:15..06:30 :   18 left,    9 right
06:30..06:45 :   13 left,    9 right
06:45..07:00 :    8 left,    5 right
07:00..07:15 :   18 left,   64 right
07:15..07:30 :   16 left,   62 right
07:30..07:45 :   44 left,   66 right
07:45..08:00 :   44 left,   64 right
08:00..08:15 :   49 left,   59 right
08:15..08:30 :   45 left,   75 right
08:30..08:45 :   46 left,  151 right
08:45..09:00 :   44 left,  169 right
09:00..09:15 :   35 left,   25 right
09:15..09:30 :   32 left,   14 right
09:30..09:45 :   23 left,   16 right
09:45..10:00 :   26 left,   14 right
10:00..10:15 :   29 left,   16 right
10:15..10:30 :   33 left,   15 right
10:30..10:45 :   25 left,   14 right
10:45..11:00 :   23 left,   12 right
11:00..11:15 :   26 left,   32 right
11:15..11:30 :   16 left,   38 right
11:30..11:45 :   35 left,   26 right
11:45..12:00 :   26 left,   26 right
12:00..12:15 :   33 left,   29 right
12:15..12:30 :   25 left,   42 right
12:30..12:45 :   30 left,   27 right
12:45..13:00 :   31 left,   27 right
13:00..13:15 :   27 left,   41 right
13:15..13:30 :   29 left,   32 right
13:30..13:45 :   24 left,   38 right
13:45..14:00 :   31 left,   25 right
14:00..14:15 :   26 left,   41 right
14:15..14:30 :   23 left,   44 right
14:30..14:45 :   28 left,   49 right
14:45..15:00 :   21 left,   46 right
15:00..15:15 :   42 left,   48 right
15:15..15:30 :   55 left,   46 right
15:30..15:45 :   44 left,   55 right
15:45..16:00 :   42 left,   48 right
16:00..16:15 :   41 left,   51 right
16:15..16:30 :   58 left,   41 right
16:30..16:45 :   93 left,   40 right
16:45..17:00 :  102 left,   35 right
17:00..17:15 :   80 left,   41 right
17:15..17:30 :  111 left,   53 right
17:30..17:45 :  114 left,   39 right
17:45..18:00 :  106 left,   39 right
18:00..18:15 :   19 left,    6 right
18:15..18:30 :   21 left,   11 right
18:30..18:45 :   27 left,    7 right
18:45..19:00 :   25 left,    6 right
19:00..19:15 :    9 left,    9 right
19:15..19:30 :   12 left,    7 right
19:30..19:45 :   11 left,   18 right
19:45..20:00 :    3 left,   10 right
20:00..20:15 :    7 left,   11 right
20:15..20:30 :    9 left,   12 right
20:30..20:45 :    9 left,    6 right
20:45..21:00 :   11 left,    8 right
21:00..21:15 :    8 left,   10 right
21:15..21:30 :    7 left,    4 right
21:30..21:45 :   12 left,   11 right
21:45..22:00 :    6 left,   16 right
22:00..22:15 :   10 left,    5 right
22:15..22:30 :   17 left,    5 right
22:30..22:45 :    7 left,    8 right
22:45..23:00 :   16 left,    2 right
23:00..23:15 :    9 left,    2 right
23:15..23:30 :    4 left,    4 right
23:30..23:45 :    4 left,    6 right
23:45..00:00 : 8916 left, 9061 right