4 # Reads iCal files and outputs remind-compatible files. Tested ONLY with
5 # calendar files created by Mozilla Calendar/Sunbird. Use at your own risk.
6 # Copyright (c) 2005, 2007, Justin B. Alcorn
8 # This program is free software; you can redistribute it and/or
9 # modify it under the terms of the GNU General Public License
10 # as published by the Free Software Foundation; either version 2
11 # of the License, or (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
23 # version 0.5.2 2007-03-23
24 # - BUG: leadtime for recurring events had a max of 4 instead of DEFAULT_LEAD_TIME
25 # - remove project-lead-time, since Category was a non-standard attribute
26 # - NOTE: There is a bug in iCal::Parser v1.14 that causes multiple calendars to
27 # fail if a calendar with recurring events is followed by a calendar with no
28 # recurring events. This has been reported to the iCal::Parser author.
29 # version 0.5.1 2007-03-21
30 # - BUG: Handle multiple calendars on STDIN
31 # - add --heading option for priority on section headers
32 # version 0.5 2007-03-21
33 # - Add more help options
34 # - --project-lead-time option
35 # - Supress printing of heading if there are no todos to print
37 # - Version 0.4 changes all written or inspired by, and thanks to Mark Stosberg
38 # - Change to GetOptions
40 # - Add --label, --help options
42 # - Change to subroutines
43 # - Efficiency and Cleanup
45 # - Convert to GPL (Thanks to Mark Stosberg)
48 # - add command line switches
50 # - add SCHED _sfun keyword
52 # version 0.1 - ALPHA CODE.
56 cat /path/to/file*.ics | ical2rem.pl > ~/.ical2rem
58 All options have reasonable defaults:
59 --label Calendar name (Default: Calendar)
60 --lead-time Advance days to start reminders (Default: 3)
61 --todos, --no-todos Process Todos? (Default: Yes)
62 --heading Define a priority for static entries
64 --man Complete man page
66 Expects an ICAL stream on STDIN. Converts it to the format
67 used by the C<remind> script and prints it to STDOUT.
71 ical2rem.pl --label "Bob's Calendar"
73 The syntax generated includes a label for the calendar parsed.
74 By default this is "Calendar". You can customize this with
79 ical2rem.pl --lead-time 3
81 How may days in advance to start getting reminders about the events. Defaults to 3.
85 ical2rem.pl --no-todos
87 If you don't care about the ToDos the calendar, this will surpress
88 printing of the ToDo heading, as well as skipping ToDo processing.
92 ical2rem.pl --heading "PRIORITY 9999"
94 Set an option on static messages output. Using priorities can made the static messages look different from
95 the calendar entries. See the file defs.rem from the remind distribution for more information.
102 use Getopt::Long 2.24 qw':config auto_help';
108 # Declare how many days in advance to remind
109 my $DEFAULT_LEAD_TIME = 3;
110 my $PROCESS_TODOS = 1;
115 my $label = 'Calendar';
117 "label=s" => \$label,
118 "lead-time=i" => \$DEFAULT_LEAD_TIME,
119 "todos!" => \$PROCESS_TODOS,
120 "heading=s" => \$HEADING,
124 pod2usage(1) if $help;
125 pod2usage(-verbose => 2) if $man;
127 my $month = ['None','Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
134 if (/END:VCALENDAR/) {
135 push(@calendars,$in);
139 my $parser = iCal::Parser->new();
140 my $hash = $parser->parse_strings(@calendars);
142 ##############################################################
146 #############################################################
149 # expects 'todos' hashref from iCal::Parser is input
150 # returns String to output
154 my ($todo, @newtodos, $leadtime);
157 $output .= 'REM '.$HEADING.' MSG '.$label.' ToDos:%"%"%'."\n";
159 # For sorting, make sure everything's got something
161 my $now = DateTime->now;
162 for $todo (@{$todos}) {
163 # remove completed items
164 if ($todo->{'STATUS'} && $todo->{'STATUS'} eq 'COMPLETED') {
166 } elsif ($todo->{'DUE'}) {
167 # All we need is a due date, everything else is sugar
168 $todo->{'SORT'} = $todo->{'DUE'}->clone;
169 } elsif ($todo->{'DTSTART'}) {
170 # for sorting, sort on start date if there's no due date
171 $todo->{'SORT'} = $todo->{'DTSTART'}->clone;
173 # if there's no due or start date, just make it now.
174 $todo->{'SORT'} = $now;
176 push(@newtodos,$todo);
178 if (! (scalar @newtodos)) {
181 # Now sort on the new Due dates and print them out.
182 for $todo (sort { DateTime->compare($a->{'SORT'}, $b->{'SORT'}) } @newtodos) {
183 my $due = $todo->{'SORT'}->clone();
185 if (defined($todo->{'PRIORITY'})) {
186 if ($todo->{'PRIORITY'} == 1) {
187 $priority = "PRIORITY 1000";
188 } elsif ($todo->{'PRIORITY'} == 3) {
189 $priority = "PRIORITY 7500";
192 if (defined($todo->{'DTSTART'}) && defined($todo->{'DUE'})) {
193 # Lead time is duration of task + lead time
194 my $diff = ($todo->{'DUE'}->delta_days($todo->{'DTSTART'})->days())+$DEFAULT_LEAD_TIME;
195 $leadtime = "+".$diff;
197 $leadtime = "+".$DEFAULT_LEAD_TIME;
199 $output .= "REM ".$due->month_abbr." ".$due->day." ".$due->year." $leadtime $priority MSG \%a $todo->{'SUMMARY'}\%\"\%\"\%\n";
201 $output .= 'REM '.$HEADING.' MSG %"%"%'."\n";
206 #######################################################################
210 ######################################################################
212 print _process_todos($hash->{'todos'}) if $PROCESS_TODOS;
214 my ($leadtime, $yearkey, $monkey, $daykey,$uid,%eventsbyuid);
215 print 'REM '.$HEADING.' MSG '.$label.' Events:%"%"%'."\n";
216 my $events = $hash->{'events'};
217 foreach $yearkey (sort keys %{$events} ) {
218 my $yearevents = $events->{$yearkey};
219 foreach $monkey (sort {$a <=> $b} keys %{$yearevents}){
220 my $monevents = $yearevents->{$monkey};
221 foreach $daykey (sort {$a <=> $b} keys %{$monevents} ) {
222 my $dayevents = $monevents->{$daykey};
224 DateTime->compare($dayevents->{$a}->{'DTSTART'}, $dayevents->{$b}->{'DTSTART'})
225 } keys %{$dayevents}) {
226 my $event = $dayevents->{$uid};
227 if ($eventsbyuid{$uid}) {
228 my $curreventday = $event->{'DTSTART'}->clone;
229 $curreventday->truncate( to => 'day' );
230 $eventsbyuid{$uid}{$curreventday->epoch()} =1;
231 for (my $i = 0;$i < $DEFAULT_LEAD_TIME && !defined($event->{'LEADTIME'});$i++) {
232 if ($eventsbyuid{$uid}{$curreventday->subtract( days => $i+1 )->epoch() }) {
233 $event->{'LEADTIME'} = $i;
237 $eventsbyuid{$uid} = $event;
238 my $curreventday = $event->{'DTSTART'}->clone;
239 $curreventday->truncate( to => 'day' );
240 $eventsbyuid{$uid}{$curreventday->epoch()} =1;
247 foreach $yearkey (sort keys %{$events} ) {
248 my $yearevents = $events->{$yearkey};
249 foreach $monkey (sort {$a <=> $b} keys %{$yearevents}){
250 my $monevents = $yearevents->{$monkey};
251 foreach $daykey (sort {$a <=> $b} keys %{$monevents} ) {
252 my $dayevents = $monevents->{$daykey};
254 DateTime->compare($dayevents->{$a}->{'DTSTART'}, $dayevents->{$b}->{'DTSTART'})
255 } keys %{$dayevents}) {
256 my $event = $dayevents->{$uid};
257 if (exists($event->{'LEADTIME'})) {
258 $leadtime = "+".$event->{'LEADTIME'};
260 $leadtime = "+".$DEFAULT_LEAD_TIME;
262 my $start = $event->{'DTSTART'};
263 print "REM ".$start->month_abbr." ".$start->day." ".$start->year." $leadtime ";
264 if ($start->hour > 0) {
266 print $start->strftime("%H:%M");
267 print " SCHED _sfun MSG %a %2 ";
271 print "%\"$event->{'SUMMARY'}";
272 print " at $event->{'LOCATION'}" if $event->{'LOCATION'};
279 #:vim set ft=perl ts=4 sts=4 expandtab :