1#!/usr/bin/perl -w
2#
3# ical2rem.pl -
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
7
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.
12#
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.
17#
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.
21#
22#
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
36# version 0.4
37#	- Version 0.4 changes all written or inspired by, and thanks to Mark Stosberg
38#	- Change to GetOptions
39#	- Change to pipe
40#	- Add --label, --help options
41#	- Add Help Text
42#	- Change to subroutines
43#	- Efficiency and Cleanup
44# version 0.3
45#	- Convert to GPL (Thanks to Mark Stosberg)
46#	- Add usage
47# version 0.2
48#	- add command line switches
49#	- add debug code
50#	- add SCHED _sfun keyword
51#	- fix typos
52# version 0.1 - ALPHA CODE.
53
54=head1 SYNOPSIS
55
56 cat /path/to/file*.ics | ical2rem.pl > ~/.ical2rem
57
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
63 --help				   Usage
64 --man				   Complete man page
65
66Expects an ICAL stream on STDIN. Converts it to the format
67used by the C<remind> script and prints it to STDOUT.
68
69=head2 --label
70
71  ical2rem.pl --label "Bob's Calendar"
72
73The syntax generated includes a label for the calendar parsed.
74By default this is "Calendar". You can customize this with
75the "--label" option.
76
77=head2 --lead-time
78
79  ical2rem.pl --lead-time 3
80
81How may days in advance to start getting reminders about the events. Defaults to 3.
82
83=head2 --no-todos
84
85  ical2rem.pl --no-todos
86
87If you don't care about the ToDos the calendar, this will surpress
88printing of the ToDo heading, as well as skipping ToDo processing.
89
90=head2 --heading
91
92  ical2rem.pl --heading "PRIORITY 9999"
93
94Set an option on static messages output.  Using priorities can made the static messages look different from
95the calendar entries.  See the file defs.rem from the remind distribution for more information.
96
97=cut 
98
99use strict;
100use iCal::Parser;
101use DateTime;
102use Getopt::Long 2.24 qw':config auto_help';
103use Pod::Usage;
104use Data::Dumper;
105use vars '$VERSION';
106$VERSION = "0.5.2";
107
108# Declare how many days in advance to remind
109my $DEFAULT_LEAD_TIME = 3;
110my $PROCESS_TODOS     = 1;
111my $HEADING			  = "";
112my $help;
113my $man;
114
115my $label = 'Calendar';
116GetOptions (
117	"label=s"     => \$label,
118	"lead-time=i" => \$DEFAULT_LEAD_TIME,
119	"todos!"	  => \$PROCESS_TODOS,
120	"heading=s"	  => \$HEADING,
121	"help|?" 	  => \$help,
122	"man" 	 	  => \$man
123);
124pod2usage(1) if $help;
125pod2usage(-verbose => 2) if $man;
126
127my $month = ['None','Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
128
129my @calendars;
130my $in;
131
132while (<>) {
133	$in .= $_;
134	if (/END:VCALENDAR/) {
135		push(@calendars,$in);
136		$in = "";
137	}
138}
139my $parser = iCal::Parser->new();
140my $hash = $parser->parse_strings(@calendars);
141
142##############################################################
143#
144# Subroutines
145#
146#############################################################
147#
148# _process_todos()
149# expects 'todos' hashref from iCal::Parser is input
150# returns String to output
151sub _process_todos {
152	my $todos = shift;
153
154	my ($todo, @newtodos, $leadtime);
155	my $output = "";
156
157	$output .=  'REM '.$HEADING.' MSG '.$label.' ToDos:%"%"%'."\n";
158
159# For sorting, make sure everything's got something
160#   To sort on.
161	my $now = DateTime->now;
162	for $todo (@{$todos}) {
163		# remove completed items
164		if ($todo->{'STATUS'} && $todo->{'STATUS'} eq 'COMPLETED') {
165			next;
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;
172		} else {
173			# if there's no due or start date, just make it now.
174			$todo->{'SORT'} = $now;
175		}
176		push(@newtodos,$todo);
177	}
178	if (! (scalar @newtodos)) {
179		return "";
180	}
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();
184		my $priority = "";
185		if (defined($todo->{'PRIORITY'})) {
186			if ($todo->{'PRIORITY'} == 1) {
187				$priority = "PRIORITY 1000";
188			} elsif ($todo->{'PRIORITY'} == 3) {
189				$priority = "PRIORITY 7500";
190			}
191		}
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;
196		} else {
197			$leadtime = "+".$DEFAULT_LEAD_TIME;
198		}
199		$output .=  "REM ".$due->month_abbr." ".$due->day." ".$due->year." $leadtime $priority MSG \%a $todo->{'SUMMARY'}\%\"\%\"\%\n";
200	}
201	$output .= 'REM '.$HEADING.' MSG %"%"%'."\n";
202	return $output;
203}
204
205
206#######################################################################
207#
208#  Main Program
209#
210######################################################################
211
212print _process_todos($hash->{'todos'}) if $PROCESS_TODOS;
213
214my ($leadtime, $yearkey, $monkey, $daykey,$uid,%eventsbyuid);
215print 'REM '.$HEADING.' MSG '.$label.' Events:%"%"%'."\n";
216my $events = $hash->{'events'};
217foreach $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};
223            foreach $uid (sort {
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;
234                        }
235                    }
236                } else {
237                    $eventsbyuid{$uid} = $event;
238                    my $curreventday = $event->{'DTSTART'}->clone;
239                    $curreventday->truncate( to => 'day' );
240                    $eventsbyuid{$uid}{$curreventday->epoch()} =1;
241                }
242
243            }
244        }
245    }
246}
247foreach $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};
253            foreach $uid (sort {
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'};
259                } else {
260                    $leadtime = "+".$DEFAULT_LEAD_TIME;
261                }
262                my $start = $event->{'DTSTART'};
263                print "REM ".$start->month_abbr." ".$start->day." ".$start->year." $leadtime ";
264                if ($start->hour > 0) {
265                    print " AT ";
266                    print $start->strftime("%H:%M");
267                    print " SCHED _sfun MSG %a %2 ";
268                } else {
269                    print " MSG %a ";
270                }
271                print "%\"$event->{'SUMMARY'}";
272                print " at $event->{'LOCATION'}" if $event->{'LOCATION'};
273                print "\%\"%\n";
274            }
275        }
276    }
277}
278exit 0;
279#:vim set ft=perl ts=4 sts=4 expandtab :
280