#!/usr/local/bin/ruby # parse-schedule.rb # # parses the 22c3 schedule's iCal file and creates a HTML-version, # because the official schedule pages can't be printed properly # # gets the schedule at http://events.ccc.de/congress/2005/fahrplan/schedule.ics # # execute like this: # # ruby parse-schedule.rb > schedule.html # # # NOTES: # - I attempted to make the logic as flexible as was conveniently possible: timetables change. # - sorry for the unmaintainable spaghetti code, this was written to get a job done, # not to be beautiful # - this might work as a basis to parse other iCal files, but don't count on it. # - PowerBooks rule # # by Martin Dittus (martin@dekstop.de), 2005-12-23 # last change: 2005-12-26 require 'net/http' require 'date' # ====================== # = load the iCal data = # ====================== # either read from a local file # filename = 'schedule.ics' # ical_data = File.read(filename) # or from the 22c3 webserver host = 'events.ccc.de' url = '/congress/2005/fahrplan/schedule.ics' ical_data = Net::HTTP.start(host, 80) { |http| http.get(url).response.body } # ========== # = parser = # ========== # takes a string: the content of the iCal file # # returns an array of hashes, each hash storing the fields of an iCal VEVENT # # DTSTART and DTEND fields are stored as DateTime instances, # ATTENDEE fields are arrays of strings (== names), # all other fields are stored as strings def parse_schedule(ical_data) events = Array.new # array of hashes cur_event_idx = 0 in_vevent = false # for multiline fields: remember where we are cur_field = nil # multiline structured fields can only be parsed after all lines have been read in_structured_field = false structured_field_data = nil ical_data.each do |line| line.gsub!(/\r?\n?/, '') if line.match(/^BEGIN:VEVENT/i) in_vevent = true events[cur_event_idx] = Hash.new elsif line.match(/^END:VEVENT/i) in_vevent = false cur_event_idx += 1 else if in_vevent if data = line.match(/^(\w+):(.*)$/) # generic string property in_structured_field = false cur_field = data[1] events[cur_event_idx][cur_field] = data[2] elsif data = line.match(/^(\w+);(.*)$/) # structured data property cur_field = data[1] if (cur_field.match(/^DTEND/i) || cur_field.match(/^DTSTART/i)) # date field date_str = data[2].match(/:([\dT]+)$/i)[1] # DateTime can't parse this format, so we reformat before parsing it date_str.gsub!(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/, '\1-\2-\3 \4:\5:\6') date = DateTime::strptime(date_str, "%Y-%m-%d %H:%M:%S") events[cur_event_idx][cur_field] = date elsif (cur_field.match(/^ATTENDEE/i)) # person in_structured_field = true structured_field_data = data[2] else # treat as generic string property events[cur_event_idx][cur_field] = data[2] end elsif data = line.match(/^ (.+)$/) if (in_structured_field) structured_field_data << data[1] else events[cur_event_idx][cur_field] += data[1] end else # unknow pattern # parse the collected structured field data if (in_structured_field) if (cur_field.match(/^ATTENDEE/i)) if events[cur_event_idx][cur_field] == nil events[cur_event_idx][cur_field] = Array.new end name = structured_field_data.match(/CN="(.*)"/i)[1] events[cur_event_idx][cur_field].push(name) end in_structured_field = false structured_field_data = nil end # close current field cur_field = nil end end end end events end # ================ # = html helpers = # ================ # format an event's property hash # returns an HTML-formatted string def format_event(event) "

#{event['SUMMARY']}

By #{event['ATTENDEE'].join(', ')}

#{event['DESCRIPTION']}

URL: #{event['URL'].match(/\/([^\/]+)$/)[1]}

" end # lalala def same_day(date1, date2) (date1 != nil && date2 != nil) && (date1.year == date2.year) && (date1.month == date2.month) && (date1.day == date2.day) end # ======== # = main = # ======== # parse events = parse_schedule(ical_data) version = ical_data.match(/22C3 Schedule Release (\d+.\d+)/i)[1] # group events by starting time start_times = Hash.new # maps DateTime to an array of event-indexes events.each_index do |event_idx| event = events[event_idx] if (start_times[event['DTSTART']] == nil) start_times[event['DTSTART']] = Array.new end start_times[event['DTSTART']] << event_idx end # get a list of all locations locations = events.collect { |event| event['LOCATION'] }.uniq.sort # get a list of all days days = events.collect { |event| event['DTSTART'].strftime('%Y-%m-%d') }.uniq.sort # ========== # = output = # ========== puts " 22C3 Fahrplan version #{version}

22C3 Fahrplan version #{version}

" # links to indiv. days link_days = days.map { |day| "#{day}"} puts "

#{ link_days.join(' · ') } · official schedule

" # day counter, stores Dates cur_day = nil # to keep the current rowspan state for each column rowspan_counter = Hash.new # maps a column's name to an int locations.each { |location| rowspan_counter[location] = 0 } # for each start time == row start_times.keys.sort.each do |start_time| # see if we are at the start of a new day -> print table header next_day = start_time if (same_day(next_day, cur_day) == false) if (cur_day != nil) # we're not on the first day -> close previous day's table puts "" puts "

top

" end cur_day = next_day puts "" puts "

#{cur_day.strftime('%A, %Y-%m-%d')}

" puts "" puts "" puts " " locations.each { |location| puts " " } puts "" end # table row puts "" puts " " cur_events = start_times[start_time] # for each column locations.each do |location| # check if t if (rowspan_counter[location] != 0) # overhanging rowspan -> no new cell rowspan_counter[location] -= 1 else # new cell # select event for this cell cell_event_idx = nil cur_events.each do |event_idx| event = events[event_idx] if (event['LOCATION'] == location) cell_event_idx = event_idx end end # print cell if cell_event_idx event = events[cell_event_idx] # does this event take more than an hour? -> rowspan duration = event['DTEND'].hour - event['DTSTART'].hour if (duration > 1) puts " " rowspan_counter[location] = duration - 1 else puts " " end else # no event here -> empty cell puts " " end end end puts "" end puts "

#{location}

#{start_time.strftime("%H:%M")}

#{format_event(event)}#{format_event(event)}
" # eof puts "" puts ""