#!/usr/bin/env ruby # # Copyright (C) 2004 Hiroyuki KUROSAKI # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # $KCODE = 'UTF-8' module ICalendar class ICalendarObject def initialize @properties = Hash.new @components = Array.new end attr_accessor :properties, :components end class Component def initialize(name = '', properties = {}) @name = name @properties = properties end attr_accessor :name, :properties def [](property_name) @properties[property_name] end def []=(property_name, other) @properties[property_name] = other end end class Property def initialize(name = '', parameters = {}, value = nil) @name = name if parameters.is_a?(String) then @parameters = Hash.new parameters.scan(/([^;=]+)=([^;=]+)/) {|key, val| @parameters[key] = val } else @parameters = parameters end @value = value end attr_accessor :name, :parameters, :value end class Text def Text.unescape(str) str.gsub(/\\(.)/) { |s| case $1 when '\\', ';' $1 when 'n', 'N' "\n" else s end } end end class Reader def initialize @doc = nil end def parse(io) @doc = nil curr = '' prev = nil currp = nil io.each {|line| line.sub!(/\r?\n\z/, '') if m = /^\s+/.match(line) then curr << m.post_match else curr = line if prev then if /^([^:;]+)((;([^:]+))*):(.*)$/ =~ prev then name = $1; param = $2; value = $5 if name == "BEGIN" then case value when "VCALENDAR" @doc = currp = ICalendarObject.new when "VEVENT", "VTODO" currp = Component.new(value) @doc.components.push currp end elsif name == "END" then currp = nil else if currp then currp.properties[name] = Property.new(name, param, Text::unescape(value)) if name == 'RRULE' then value_hash = Hash.new value.scan(/([^;=]+)=([^;=]+)/) {|key, val| value_hash[key] = val } currp.properties[name].value = value_hash end end end end end end prev = curr } @doc end end end class ISO8601 class Date def Date.to_utc(str, timezone = nil) if /\A(\d{4})-?(\d{2})-?(\d{2})/ =~ str then year = $1.to_i; month = $2.to_i; day = $3.to_i Time.gm(year, month, day).to_i else nil end end end class DateTime def DateTime.to_utc(str, timezone = nil) if /\A(\d{4})-?(\d{2})-?(\d{2})T(\d{2}):?(\d{2}):?(\d{2})(.*)\z/ =~ str then year = $1.to_i; month = $2.to_i; day = $3.to_i hour = $4.to_i; min = $5.to_i; sec = $6.to_i tzstr = $7 if tzstr && tzstr == 'Z' then Time.gm(year, month, day, hour, min, sec).to_i else Time.local(year, month, day, hour, min, sec).utc.to_i end else ISO8601::Date.to_utc(str, timezone) end end end end module Zdbat class NotSupportedError 1, 'TU' => 2, 'WE' => 4, 'TH' => 8, 'FR' => 16, 'SA' => 32, 'SU' => 64 } def fromVEvent(vevent) self['DSRP'] = vevent['SUMMARY'].value self['PLCE'] = vevent['LOCATION'] && vevent['LOCATION'].value self['MEM1'] = vevent['DESCRIPTION'] && vevent['DESCRIPTION'].value self['ARON'] = '1' # not supported self['ARMN'] = '0' # not supported dateval = vevent['DTSTART'].parameters['VALUE'] if dateval && dateval == 'DATE' self['ADAY'] = '1' self['ALSD'] = vevent['DTSTART'].value self['ALED'] = vevent['DTEND'] && vevent['DTEND'].value if self['ALED'] then self['ALED'] = Time.at(ISO8601::DateTime.to_utc(self['ALED'] + 'Z') - 60 * 60 * 24).strftime("%Y%m%d") end self['TIM1'] = self['ALSD'] self['TIM2'] = self['ALED'] else self['ADAY'] = '0' self['TIM1'] = Time.at(ISO8601::DateTime.to_utc(vevent['DTSTART'].value)).utc.strftime("%Y%m%dT%H%M%S") self['TIM2'] = vevent['DTEND'] && vevent['DTEND'].value if self['TIM2'] then self['TIM2'] = Time.at(ISO8601::DateTime.to_utc(self['TIM2'])).utc.strftime("%Y%m%dT%H%M%S") end end if self['ALSD'] != self['ALED'] then self['MDAY'] = '1' else self['MDAY'] = '0' end if vevent['DURATION'] then if /^([+\-])?P(?:(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?|(\d+)W)/ =~ vevent['DURATION'].value then sign = $1; day = $2; hour = $3; min = $4; sec = $5; week = $6 dtend_time = ISO8601::DateTime.to_utc(self['TIM1'] + 'Z') if day then dtend_time += day.to_i * 60 * 60 * 24 end if hour then dtend_time += hour.to_i * 60 * 60 end if min then dtend_time += min.to_i * 60 end if sec then dtend_time += sec.to_i end if week then dtend_time += week.to_i * 60 * 60 * 24 * 7 end if self['ADAY'] == '1' then self['TIM2'] = Time.at(dtend_time - 60 * 60 * 24).utc.strftime("%Y%m%d") self['ALED'] = self['TIM2'] else self['TIM2'] = Time.at(dtend_time).utc.strftime("%Y%m%dT%H%M%S") end end end self['RPOS'] = '0' rrule = vevent['RRULE'] if rrule then self['RFRQ'] = rrule.value['INTERVAL'] if rrule.value['UNTIL'] then self['REND'] = '1' self['REDT'] = rrule.value['UNTIL'].sub(/Z\z/, '') else self['REND'] = '0' end case rrule.value['FREQ'] when 'DAILY' self['RTYP'] = '0' if rrule.value['COUNT'] then self['REND'] = '1' self['REDT'] = Time.at(ISO8601::DateTime.to_utc(vevent['DTSTART'].value) + rrule.value['COUNT'].to_i * 60 * 60 * 24 * self['RFRQ'].to_i).utc.strftime('%Y%m%dT%H%M%S') end when 'WEEKLY' self['RTYP'] = '1' if rrule.value['BYDAY'] then rdys_val = 0 rrule.value['BYDAY'].split(/,/).each{|w| rdys_val += RDYS_VALUE[w] } self['RDYS'] = rdys_val.to_s end when 'MONTHLY' if rrule.value['BYDAY'] then self['RTYP'] = '2' if /\A\+?(\d)([A-Z][A-Z])\z/ =~ rrule.value['BYDAY'] then self['RPOS'] = $1 self['RDYS'] = RDYS_VALUE[$2].to_s else raise Zdbat::NotSupportedError end else self['RTYP'] = '3' if rrule.value['BYMONTHDAY'] then raise Zdbat::NotSupportedError end end when 'YEARLY' self['RTYP'] = '4' end else self['RTYP'] = '255' end end end class ToDo