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