# Vehicle Counters (#146)

**URL:** <https://www.ruby-forum.com/t/vehicle-counters-146/120276>\
**Category:** Ruby\
**Created:** [November 15, 2007, 1:57pm UTC](https://www.ruby-forum.com/t/vehicle-counters-146/120276 "2007-11-15T13:57:10Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![bbazzarrakk](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/b/47e85d/32.png) [@bbazzarrakk](https://www.ruby-forum.com/u/bbazzarrakk)\
**Post date:** [November 15, 2007, 1:57pm UTC](https://www.ruby-forum.com/t/vehicle-counters-146/120276/1 "2007-11-15T13:57:10Z")

</div>

This is a cool little data mining problem. Unfortunately, it is a fair  
bit of  
work to get a good solution. Justin Either sent in the only full  
solution and  
even he chose to cut a few corners. His code does produce almost 200  
lines of  
interesting feedback when run on the sample data set though, so it’s fun  
to play  
around with. The downside is that it’s close to 300 lines of code.  
Because of  
that, I’m going to try giving a pretty high-level overview of how it  
works.

Let’s work backwards this time and just follow the code:

# …

if ARGV.size \< 1  
puts “Usage: vehicle\_counter.rb datafile [-avg]”  
else  
vc = VehicleCounter.new  
vc.process(ARGV[0], ARGV[1] != nil)  
end

This is the code that actually gets executed when you run the program.  
I always  
call that the application code.

Here we see a simple branch that either presents a usage message or  
builds a  
VehicleCounter and calls process() on it. This could actually be just a  
module,  
since it doesn’t maintain any state. It may help you to think of the  
following  
code that way:

# …

class VehicleCounter  
# …

```
def process(file, report_averages)
  raw_data = parse(file)
  for sensor in 0..1
    sensor_report = DirectionDataReport.new(raw_data[sensor])
    sensor_report.report(sensor, report_averages)
  end
end

# ...

```

end

# …

This defines the overall process of this program which is just two  
steps: read  
in the data and build a report. The parse() method manages the reading  
step  
while reporting is encapsulated in DirectionDataReport objects. We will  
begin  
with the reading:

```
# ...

Dir_A = 0
Dir_B = 1

def parse(file)
  times = []
  dirs = [[], []]

  f = File.open(file)
  f.each_line do |line|
    sensor, time = parse_record(line)
    times << time

    if (times.size % 2) == 0
      if (times.size == 2 and sensor == Dir_A) or
         (times.size == 4 and sensor == Dir_B)

         # Remove "B" records from second direction
         times = [times[0], times[2]] if sensor == Dir_B

         dirs[sensor] << times
         times = []
       elsif (times.size == 4 and sensor != Dir_B)
        puts "Parse error"
        times = []
       end
    elsif (times.size % 2) == 1 and sensor == Dir_B
      puts "Parse error - Unexpected B record"
    end
  end
  f.close

  dirs
end

def parse_record(data)
  unpacked = data.unpack("a1a*")
  return unpacked[0] == "A" ? Dir_A : Dir_B, unpacked[1].to_i
end

# ...

```

This parser is what I meant by cutting corners earlier. The data  
provided is  
quite simple and you don’t have to worry about cars going both  
directions at the  
same time when working with it. Given that, a pair of A times is a  
northbound  
car and a set of A, B, A, B times is a southbound car. This parse just  
hunts  
for those sets.

Both sets of times are maintained in the Array of Arrays called dirs.  
The  
constants are used to select the right group based on the indicator  
found in  
unpack()ing the last record of the group. These two Arrays are the  
ultimate  
results of this method.

Note that the file reading loop could be reduced to a File.foreach()  
call.

The rest of the code is reporting. We saw a DirectionDataReport  
initialized in  
the process() method and then witnessed a call to report() on that  
object.  
These two steps are the primary interface:

MSecPerMin = 1000 \* 60  
MSecPerHour = MSecPerMin \* 60  
InchesPerMile = 63360

class DirectionDataReport  
Verbose = false  
Sensors = %w(Northbound Southbound)  
Days = %w(Mon Tue Wed Thu Fri Sat Sun)

```
def initialize(raw_data)
  @raw_data = raw_data
end

def report(sensor, report_averages)
  puts "Direction: #{Sensors[sensor]}"
  puts "Total Cars: #{self.total_count}"

  report_time_periods(sensor, report_averages, MSecPerHour * 12)
  report_time_periods(sensor, report_averages, MSecPerHour)
  report_time_periods(sensor, report_averages, MSecPerHour / 2)
  report_time_periods(sensor, report_averages, MSecPerHour / 4)
  puts ""
end

def total_count()
  @raw_data.size
end

# ...

```

You can see here that the constructor just squirrels away the data from  
the  
parse. The report() method, on the other, hand drives the output  
process.  
After reporting the direction and a total car count, it displays time  
oriented  
breakdowns for various scales: 12 hours, one hour, 30 minutes, and 15  
minutes.

Here is the report\_time\_periods() method and three of the methods it  
relies on:

```
# ...

def report_time_periods(sensor, report_averages, time_period_length)
  days = create_time_periods(time_period_length)
  num_time_periods = (MSecPerHour * 24) / time_period_length

  counts = count_per_time_period(days)
  avg_speeds = speed_per_time_period(days)
  avg_dists = dist_per_time_period(days)

  puts("\nTime Interval: #{time_period_length/MSecPerMin} Minutes")
  if (num_time_periods > 2)
    peaks = find_peak_times(days)
    puts("Peak Times")
    for i in 0...peaks.size
      printf("#{Days[i]}:")
      peaks[i].size.times {|p|
        printf(format_time_interval_index(peaks[i][p][1],
                                          time_period_length))
      }
      puts ""
    end
  end

  puts("Statistics")
  printf("Data ")
  printf("\tDay") if not report_averages

  num_time_periods.times{|i|
    printf(format_time_interval_index(i, time_period_length))
  }
  report_column_data(days, num_time_periods, report_averages, 

```

counts,  
report\_averages ? “Avg Count” : "Count ", “%  
5d”)  
report\_column\_data(days, num\_time\_periods, report\_averages,  
avg\_speeds,  
“Avg Speed”, “%02.02f”)  
report\_column\_data(days, num\_time\_periods, report\_averages,  
avg\_dists,  
"Avg Dist ", “%02.02f”)  
puts “”  
end

```
def create_time_periods(time_period_length = MSecPerHour)
  days = []
  time_periods = nil
  prev_start = @raw_data[0][0] + 1

  for data in @raw_data
    if prev_start > data[0]
      days << time_periods if time_periods != nil

      cur_time_period = 0
      time_periods = [[]]

      puts "New day: data=#{data[0]}" if Verbose
    elsif data[0] > ((cur_time_period + 1) * time_period_length)
      while data[0] > ((cur_time_period + 1) * time_period_length)
        cur_time_period += 1
        time_periods[cur_time_period] = []
      end

      puts "New time period: data=#{data[0]}" if Verbose
    end

    time_periods[cur_time_period] << data
    prev_start = data[0]
  end

  days << time_periods
  days
end

def format_time_interval_index(index, time_period_length)
  sprintf("\t%02d:%02d",
      index * time_period_length / MSecPerHour,
      (index * time_period_length / MSecPerMin) % 60)
end

def report_column_data(days, num_time_periods, report_averages, 

```

data,  
data\_label, format\_string)  
if report\_averages  
printf("\n#{data\_label}")  
for time in 0…num\_time\_periods  
avg = 0  
days.size.times {|day| avg += data[day][time] }  
printf("\t#{format\_string}", avg / days.size)  
end  
else  
for day in 0…days.size  
printf("\n#{data\_label}\t#{Days[day]}")  
for time in 0…num\_time\_periods  
printf("\t#{format\_string}", data[day][time])  
end  
end  
end  
end

```
# ...

```

There is a lot of code here obviously, but none of it is very complex.  
First,  
report\_time\_periods() breaks the data down into time intervals using  
create\_time\_periods(). That method is just a partition tool for the  
data. Once  
it’s divided out, a few utility method we will examine next are used to  
separate  
the interesting information out.

After that, the entire rest of the of the method is printing code for  
what it  
found. (The peak data section actually calculates, again using another  
helper,  
and prints for appropriate data ranges.) Most of the print work is  
delegated to  
the bottom two methods and much or their magic is for printing columnar  
data.

Here are the helpers I glossed over and the helpers they rely on:

```
# ...

def find_peak_times(days, num_peaks=4)
  days.map do |day|
    find_daily_peak_times(day, num_peaks)
  end
end

def find_daily_peak_times(daily_time_periods, num_peaks)
  peaks = []
  daily_time_periods.size.times {|i|
    peaks << [daily_time_periods[i].size, i]
  }
  peaks.sort.reverse.slice(0, num_peaks).sort {|a,b| a[1]<=>b[1]}
end

def count_per_time_period(days)
  days.map do |day|
    day.map {|time_period| time_period.size}
  end
end

def speed_per_time_period(days)
  days.map do |day|
    day.map {|time_period| calc_average_speed(time_period) }
  end
end

def dist_per_time_period(days)
  days.map do |day|
    day.map {|time_period| calc_average_distance(time_period) }
  end
end

def calc_average_speed(time_period)
  return 0 if time_period.size == 0

  sum = 0
  for time in time_period
    sum += calc_speed(time[0], time[1])
  end
  sum / (time_period.size)
end

def calc_speed(start_time, end_time)
  return (100.0 / (end_time - start_time)) *
         MSecPerHour / (InchesPerMile * 1.0)
end

def calc_average_distance(time_period)
  return 0 if time_period.size <= 1 # Need at least 2 cars
  sum = 0
  for i in 0...(time_period.size - 1)
    sum += calc_distance(time_period[0], time_period[1])
  end
  return sum / (time_period.size - 1)
end

def calc_distance(leader_time, follower_time)
  follower_speed = calc_speed(follower_time[0], follower_time[1])

  dist = follower_speed * ((follower_time[0] - leader_time[0]) /
         (MSecPerHour * 1.0))

  return dist
end

```

end

# …

There are three kinds of helpers here. The peak methods just sort the  
data and  
take so many entries off the top. The _per\_time\_period() method are  
wrappers  
that apply the calc_() methods over periods of data. Which means the  
calc\_\*()  
methods are where the action is. They figure out car speeds, distances  
between  
cars, and the averages for both using simple math. This is the primary  
data  
that we see in the report output.

Again it’s a lot of code to cover, but Justin has produced a great end  
result.  
I’m not going to include the output here because it too is large, but go  
run the  
program and take a look at what it builds.

My thanks to Justin for the heroic effort and Gavin for the interesting  
problem.

The Ruby Q. began with an encoding problem that came out of a book and  
tomorrow we have another one for you…
