Ruby 23 Mar 2006 07:47 am

Palm Database Parsing With Ruby

Tracking hours for a handful of billable projects can be a lot of scribblin’ and calculatin’. So I was pleased to find the PunchClock time tracker application for PalmOS. It’s missing a few features I’d like, but on the whole it does a great job. The main problem is counting hours for my bi-monthly timesheet. PunchClock can show summaries by day, by week, by two-week, by month, and by year…but not bi-monthly. So I’m still stuck with hand-calculating it myself.

A while back, I threw something together in Java, using the JPilot DB library to parse Palm PRC database files. It was a bit clunky, even at 550 lines of source code (not including JPilot), but it mostly did the job.

Never really satisfied with the Java hack, but lacking the time to improve it, I just let it sit, using it every couple weeks, then not at all. Finally, last night, I sat down with the Palm online docs plus a tech note I found long ago, to see what it would be like to do it as a Ruby script.

Lo and behold, it only took me about 60 lines of Ruby code (only 30 of which is parsing code), from start to finish. I had to fiddle with the pack/unpack specs a bit, but things fell together quickly. I was impressed by how intuitive Ruby scripting was. It was easy to group results by year or by year-and-month, etc., by using a Time object as the hash key (see byyear() and bymonth() methods below). Here’s the source code for the parser…so far.

class Record
attr_accessor :start, :stop
def initialize(start, stop)
@start, @stop = start, stop
end

def to_s
"#{@start} - #{@stop}"
end
end

class TimeDb
attr_accessor :record_count, :records
def initialize(filename)
@contents = File.read(filename)
parse
end

def parse
@hdr, @body = @contents.unpack("a78 a*")
@hdrparts = @hdr.unpack("Z32 nn A4 A4 A4 III A4 A4 II n ")
@record_count = @hdrparts.last

@records = []
for i in 0…@record_count
offset, flags, id = @body[(i * 8)...(i * 8 + 8)].unpack(”I c a3″)
next if flags == -64

record = @contents[offset..(offset + 14)]
date_time, hour, minute, time_spent = record.unpack(”xxx a3 x CC x I”)

date_time = date_time.insert(0, 0.chr).unpack(”I”).first
month = (date_time & 0xf00000) >> 20
day = (date_time & 0xf8000) >> 15
year = (date_time & 0×7fff)

t1 = Time.gm(year, month, day, hour, minute)
t2 = t1 + time_spent
@records < < Record.new(t1, t2)
end
end

def by_year
map = {}
@records.each do |record|
key = Time.gm record.start.year
(map[key] ||= []) << record
end
map
end

def by_month
map = {}
@records.each do |record|
key = Time.gm record.start.year, record.start.month
(map[key] ||= []) << record
end
map
end

end

And here’s a sample of code to list all entries by month.

db = TimeDb.new("PC_MyProject.pdb")

map = db.by_month
map.keys.sort.each do |key|
puts "  #{key.strftime("%Y/%m/%d")}n #{map[key].inspect}nn”
end

I’m very pleased with the results. The next step is to write a little Rails app to provide an interactive browser for my PunchClock databases. And after that, tools for creating SuperMemo databases. Fun fun fun….

Comments are closed.

Trackback This Post |