]> git.donarmstrong.com Git - bin.git/blob - ical2rem
add common subscriber
[bin.git] / ical2rem
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
66 Expects an ICAL stream on STDIN. Converts it to the format
67 used by the C<remind> script and prints it to STDOUT. 
68
69 =head2 --label
70
71   ical2rem.pl --label "Bob's Calendar"
72
73 The syntax generated includes a label for the calendar parsed.
74 By default this is "Calendar". You can customize this with 
75 the "--label" option.
76
77 =head2 --lead-time 
78
79   ical2rem.pl --lead-time 3
80  
81 How 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
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. 
89
90 =head2 --heading
91
92   ical2rem.pl --heading "PRIORITY 9999"
93
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.
96
97 =cut 
98
99 use strict;
100 use iCal::Parser;
101 use DateTime;
102 use Getopt::Long 2.24 qw':config auto_help';
103 use Pod::Usage;
104 use Data::Dumper;
105 use vars '$VERSION';
106 $VERSION = "0.5.2";
107
108 # Declare how many days in advance to remind
109 my $DEFAULT_LEAD_TIME = 3;
110 my $PROCESS_TODOS     = 1;
111 my $HEADING                       = "";
112 my $help;
113 my $man;
114
115 my $label = 'Calendar';
116 GetOptions (
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 );
124 pod2usage(1) if $help;
125 pod2usage(-verbose => 2) if $man;
126
127 my $month = ['None','Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
128
129 my @calendars;
130 my $in;
131
132 while (<>) {
133         $in .= $_;
134         if (/END:VCALENDAR/) {
135                 push(@calendars,$in);
136                 $in = "";
137         }
138 }
139 my $parser = iCal::Parser->new();
140 my $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
151 sub _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
212 print _process_todos($hash->{'todos'}) if $PROCESS_TODOS;
213
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};
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 }
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};
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 }
278 exit 0;
279 #:vim set ft=perl ts=4 sts=4 expandtab :