#!/usr/bin/ruby require "date" # Add a starts_with? method class String def starts_with?(text) return false if (length < text.length) return (self[0,text.length] == text) end end module ParsePalmDatebook class Constants EVENTTYPES = {'APPOINTMENT' => {'hasTime'=> true, 'numDates'=> 2}, 'UNTIMED_EVENT' => {'hasTime'=> false, 'numDates'=> 1}} MONTHTOINDEX = {'jan'=> 1, 'feb'=> 2, 'mar'=> 3, 'apr'=> 4, 'may'=> 5, 'jun'=> 6, 'jul'=> 7, 'aug'=> 8, 'sep'=> 9, 'oct'=> 10, 'nov'=> 11, 'dec'=> 12} DAYTOINDEX = {} INDEXTODAY = {} dayIndices = [[0, 'sun'], [1, 'mon'], [2, 'tue'], [3, 'wed'], [4, 'thu'], [5, 'fri'], [6, 'sat']] dayIndices.each do |entry| index = entry[0] day = entry[1] DAYTOINDEX[day] = index INDEXTODAY[index] = day end PALMUNITTOMINS = {} GCALUNITTOMINS = {} units = [["min", "minutes", 1], ["hours", "hours", 60], ["days", "days", 1440], [nil, nil, 0]] units.each do |entry| palmUnit = entry[0] gCalUnit = entry[1] PALMUNITTOMINS[palmUnit] = entry[2] GCALUNITTOMINS[gCalUnit] = entry[2] end DATERE = /^(\d+)\s+(\w+)\s+(\d+)(?:\s+(\d+):(\d+))?$/ CATEGORYRE = Regexp.new('category=\d+\s+\((.*?)\)$', Regexp::IGNORECASE) end # FFV - this is ugly, should subclass? class Repeat attr_accessor :startDate, :endDate, :repeatType, :repeatDays, :repeatInterval, :monthlyRepeatDate, :monthlyRepeatWeekdayDay, :monthlyRepeatWeekdayNumber, :yearlyRepeatMonth, :yearlyRepeatDate, :omitList def initialize() @startDate = nil @endDate = nil @repeatType = nil @repeatDays = [] @repeatInterval = 1 @monthlyRepeatDate = nil @monthlyRepeatWeekdayDay = nil @monthlyRepeatWeekdayNumber = nil @yearlyRepeatMonth = nil @yearlyRepeatDate = nil @omitList = [] end def to_s output = "Repeats from #@startDate to #@endDate" if (@repeatType == :weekly) then output = output + " on" @repeatDays.each {|day| output = output + " " + Constants::INDEXTODAY[day]} end return output end def containsDate?(date, printExceptions) return false if (date < @startDate or @endDate < date) return false if (@omitList.include?(date)) if (@repeatType == :daily) then return (((date-@startDate) % @repeatInterval).floor == 0) elsif (@repeatType == :weekly) then return (@repeatDays.include?(date.wday) and (((date-@startDate)/7) % @repeatInterval).floor == 0) elsif (@repeatType == :monthly) then monthsSinceStartDate = 12 * (date.year - @startDate.year) + (date.month - @startDate.month) if (@monthlyRepeatDate != nil) then return (date.mday == @monthlyRepeatDate and (monthsSinceStartDate % @repeatInterval) == 0) else return (date.wday == @monthlyRepeatWeekdayDay and ((date.mday - 1)/7) == (@monthlyRepeatWeekdayNumber - 1) and (monthsSinceStartDate % @repeatInterval) == 0) end elsif (@repeatType == :yearly) then return ((date.mday == @yearlyRepeatDate) and (date.month == @yearlyRepeatMonth) and (((date.year - @startDate.year) % @repeatInterval).floor == 0)) else if (printExceptions) then puts "Unknown repeat type: #@repeatType" end return false end end end class DatebookEntry attr_accessor :entryType, :hasTime, :startDate, :endDate, :category, :title, :repeat, :note, :alarmNumber, :alarmUnit def initialize @entryType = '' @hasTime = false @startDate = nil @endDate = nil @title = '' @category = nil @repeat = nil @note = nil @alarmNumber = 0 @alarmUnit = nil end def to_s output = "#@title (#@startDate" output = output + " to #@endDate" if @endDate output = output + ")" output = output + " - #@category" if @category output = output + " #@repeat" if @repeat output = output + " (NOTE: #@note)" if @note output = output + " unit #@alarmUnit number #@alarmNumber" output end def containsDate?(date, printExceptions) return @repeat.containsDate?(date, printExceptions) if @repeat return (getStartDate() == date or getEndDate() == date) end def DatebookEntry.getDateFromDateOrDatetime(dordt) toReturn = dordt if (dordt.instance_of?(DateTime)) then toReturn = Date.civil(dordt.year(), dordt.month(), dordt.day()) end return toReturn end def getStartDate return DatebookEntry::getDateFromDateOrDatetime(@startDate) end def getEndDate return DatebookEntry::getDateFromDateOrDatetime(@endDate) end end def ParsePalmDatebook.parseDate(text, hasTime, printExceptions) if text =~ Constants::DATERE if (hasTime) then return DateTime.civil($3.to_i, Constants::MONTHTOINDEX[$2.downcase], $1.to_i, $4.to_i, $5.to_i) else return Date.civil($3.to_i, Constants::MONTHTOINDEX[$2.downcase], $1.to_i) end else if (printExceptions) then puts "couldn't parse date: \"#{ text }\"" end return nil end end def ParsePalmDatebook.parseLines(lines, printExceptions) entries = [] inEntry = false curEntry = nil inNote = false lines.each do |line| if (not inEntry) Constants::EVENTTYPES.keys().each do |eventType| if line.starts_with?(eventType) inEntry = true # Find all <> pairs after the first lessThanIndex = line.index('<') dates = [] datesOK = true Constants::EVENTTYPES[eventType]['numDates'].times do lessThanIndex = line.index('<', lessThanIndex+1) greaterThanIndex = line.index('>', lessThanIndex) date = parseDate(line[lessThanIndex+1...greaterThanIndex], Constants::EVENTTYPES[eventType]['hasTime'], printExceptions) if (date == nil) datesOK = false end dates.push(date) end if (not datesOK) inEntry = false next end #print "got event with dates: " #dates.each {|date| print date} curEntry = DatebookEntry.new curEntry.entryType = eventType curEntry.hasTime = (dates.length > 1) curEntry.startDate = dates[0] curEntry.endDate = dates[1] if curEntry.hasTime end end else if (line == "" or line == "\n") then #puts "pushing entry: #{ curEntry.to_s }" entries.push(curEntry) curEntry = nil inEntry = false inNote = false elsif inNote if curEntry.note curEntry.note += "\n" + line.chop else curEntry.note = line.chop end else if (line.starts_with?('attributes=')) then # We don't care about attributes nil elsif (line.starts_with?('category=')) then if line =~ Constants::CATEGORYRE then curEntry.category = $1 else if (printExceptions) then puts "ERROR - couldn't parse category line: #{ line }" end end elsif (line.starts_with?('REPEAT from')) repeatRe = /^REPEAT from <(.*?)> until <(.*?)>:\s*(.*)$/ if line =~ repeatRe startRepeatDate = parseDate($1, false, printExceptions) if ($2 == "forever") then endRepeatDate = nil else endRepeatDate = parseDate($2, false, printExceptions) end intervalRe = /^<(.*?)>\s*(.*)$/ intervalLine = $3 if intervalLine =~ intervalRe repeatExtra = $2 omitSpec = nil repeat = Repeat.new() repeat.startDate = startRepeatDate repeat.endDate = endRepeatDate repeat.repeatType = $1.downcase.intern if (repeat.repeatType == :daily) dailyRe = /^(?:every <(\d+)> times)?(?:\s*OMIT:\s*(.*))?$/ if (repeatExtra =~ dailyRe) then omitSpec = $2 if ($1 != nil) repeat.repeatInterval = $1.to_i end else if (printExceptions) puts "ERROR - couldn't parse daily repeat line: #{ line }" end end elsif (repeat.repeatType == :weekly) weeklyRe = /^on <(.*?)>(?: every <(\d+)> times)?(?:\s*OMIT:\s*(.*))?$/ if (repeatExtra =~ weeklyRe) then omitSpec = $3 weeklyRepeat = $2 repeat.repeatDays = $1.downcase.split(',').collect {|x| Constants::DAYTOINDEX[x]} if (weeklyRepeat != nil) then repeat.repeatInterval = weeklyRepeat.to_i end else if (printExceptions) puts "ERROR - couldn't parse weekly repeat line: #{ line }" end end elsif (repeat.repeatType == :monthly or repeat.repeatType == :"monthly/weekday") if (repeat.repeatType == :monthly) monthRe = /^on <(\d+)>\s*(?:every <(\d+)> times)?(?:\s*OMIT:\s*(.*))?$/ if (repeatExtra =~ monthRe) omitSpec = $3 repeat.monthlyRepeatDate = $1.to_i if ($2 != nil) then repeat.repeatInterval = $2.to_i end else if (printExceptions) puts "ERROR - couldn't parse monthly repeat line: #{ line }" end end else monthWeekdayRe = /^on <(.*?)>\s*(?:every <(\d+)> times)?(?:\s*OMIT:\s*(.*))?$/ if (repeatExtra =~ monthWeekdayRe) omitSpec = $3 weekdaySpec = $1 if ($2 != nil) then repeat.repeatInterval = $2.to_i end weekdayRe = /^(\w+) ge (\d+)$/ if (weekdaySpec =~ weekdayRe) then repeat.monthlyRepeatWeekdayDay = Constants::DAYTOINDEX[$1.downcase] repeat.monthlyRepeatWeekdayNumber = $2.to_i else if (printExceptions) puts "ERROR - couldn't parse monthly-weekday weekday line: #{ line }" end end else if (printExceptions) puts "ERROR - couldn't parse monthly-weekday repeat line: #{ line }" end end end repeat.repeatType = :monthly elsif (repeat.repeatType == :yearly) yearlyRe = /^on <(\w+) (\d+)>\s*(?:every <(\d+)> times)?(?:\s*OMIT:\s*(.*))?$/ if (repeatExtra =~ yearlyRe) omitSpec = $4 repeat.yearlyRepeatMonth = Constants::MONTHTOINDEX[$1.to_s.downcase] repeat.yearlyRepeatDate = $2.to_i if ($3 != nil) repeat.repeatInterval = $3.to_i end else if (printExceptions) puts "ERROR - couldn't parse yearly repeat line: #{ line.chop } (tried to match \"#{ repeatExtra }\")" end end else if (printExceptions) puts "ERROR - got unknown repeatType #{ repeat.repeatType.to_s } (line is #{ line })" end end if (omitSpec) while (omitSpec =~ /^\s*<(.*?)>\s*(.*)$/) omitDate = $1 omitSpec = $2 repeat.omitList.push(parseDate($1, false, printExceptions)) end #puts omitSpec end curEntry.repeat = repeat if (repeat.endDate == nil) then # We have to make up an end date here. if (curEntry.repeat.repeatType != :yearly) then # If we're not yearly, stop a year after today. curEntry.repeat.endDate = Date::today >> 12 else # Stop after 2 * repeatInterval years after today curEntry.repeat.endDate = Date::today >> (12 * 2 * repeat.repeatInterval) end end else if (printExceptions) puts "ERROR - couldn't parse interval line #{ intervalLine }" end end else if (printExceptions) puts "ERROR - couldn't parse repeat line #{ line }" end end elsif (line.starts_with?('ALARM ')) alarmRe = /^ALARM <(\d+) (\w+) before>\s*$/ if line =~ alarmRe curEntry.alarmNumber = $1.to_i curEntry.alarmUnit = $2 else if (printExceptions) puts "ERROR - couldn't parse alarm line #{ line }" end end elsif (line.starts_with?('NOTE:')) inNote = true else curEntry.title = line.chop end end end end return entries end def ParsePalmDatebook.getAllEntries(fileName, printExceptions) lines = [] IO.foreach(fileName) {|line| lines.push(line)} # FFV - why doesn't this work? #lines = IO.readlines(fileName, 'r') #puts lines parseLines(lines, printExceptions) end def ParsePalmDatebook.getEntries(fileName, date, numDays) entries = ParsePalmDatebook::getAllEntries(fileName, true) curDate = date toReturn = {} numDays.times do entriesToday = entries.find_all {|e| e.containsDate?(curDate, true)} if (entriesToday) toReturn[curDate] = entriesToday end curDate = curDate + 1 end toReturn end end # module ParsePalmDatebook if __FILE__ == $0 datebookFilename = 'datebookfile21264.0' #datebookFilename = 'datebookquicktest.txt' entries = ParsePalmDatebook.getEntries(datebookFilename, Date::today-10, 20) entries.keys.sort.each do |date| puts "Entries for #{ date }:" entries[date].each {|entry| puts " #{ entry }"} if entries[date] end end