├── LICENSE ├── dailyreminders.sh ├── getgooglecals.sh ├── cal_futureonly.pl ├── README.md └── ical2rem.pl /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2005, 2007, 2019 Justin B. Alcorn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dailyreminders.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | r=`basename $0` 4 | 5 | if [ $r == 'weeklyreminders.sh' ]; 6 | then 7 | t=14; 8 | w=Weekly; 9 | elif [ $r == 'dailyreminders.sh' ]; 10 | then 11 | t=3; 12 | w=Daily; 13 | else 14 | t=5 15 | w=Test; 16 | fi 17 | 18 | cd .rem 19 | for d in * ; 20 | do 21 | if [ "$( ls -A $d/$w 2>/dev/null )" ]; 22 | then 23 | echo "Sending a $w reminder to $d" 24 | ft=/tmp/$d-t-$$.txt 25 | f=/tmp/$d-$$.txt 26 | echo "Reminders for next $t days:" >> $f 27 | cat /dev/null > $d/ical2rem 28 | for c in $d/*.ics 29 | do 30 | calname=`basename $c .ics | tr a-z A-Z` 31 | cat $c 2>/dev/null | sed -e "s/^SUMMARY:/SUMMARY: {${calname}} /" \ 32 | | sed -e 's/DT\([A-Z]*\);TZID=UTC:\([0-9T]*\)/DT\1:\2Z/' >> $ft 33 | done 34 | cat $ft | ~/bin/ical2rem.pl --label "Online Calendar" --heading "PRIORITY 9999" --lead-time $t >> $d/ical2rem 35 | if [ -e $d/reminders ];then r="${d}/reminders"; else r="${d}/ical2rem";fi 36 | /usr/bin/remind -q -iplain=1 $r >> $f 37 | echo " 38 | All calendars can be accessed by logging into https://calendar.google.com/ as $d@jalcorn.net 39 | " >> $f 40 | cat $f | mail -s "$w Reminders for $d" $d@jalcorn.net; 41 | cat $f 42 | rm $f 43 | rm $ft 44 | fi; 45 | done 46 | -------------------------------------------------------------------------------- /getgooglecals.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Get google calendars, fix issues caused by changes in Google calendars, and remove all past events. 4 | # 5 | # Obviously, I've removed the private hashes from private calendars. 6 | # 7 | cd ~/calendars 8 | wget -q -O full/justin.ics --no-check-certificate https://www.google.com/calendar/ical/jbalcorn\%40gmail.com/private-aaaaaaaaaaaaaaaaaaaaaaaaaa/basic.ics 9 | wget -q -O full/family.ics --no-check-certificate https://www.google.com/calendar/ical/jalcorn.net_aaaaaaaaaaaaaaaaaaaaaaaaaa\%40group.calendar.google.com/private-6c42a79dec0b3b3bb7b9b0ebf9776bc1/basic.ics 10 | wget -q -O full/son1.ics --no-check-certificate https://www.google.com/calendar/ical/son1\%40jalcorn.net/private-aaaaaaaaaaaaaaaaaaaaaaaaaa/basic.ics 11 | wget -q -O full/vmd.ics --no-check-certificate https://calendar.google.com/calendar/ical/chuh.org_0pmkefjkiqc4snoel7occlslh8%40group.calendar.google.com/public/basic.ics 12 | 13 | for i in full/*.ics;do 14 | cat $i 2>/dev/null | sed -e 's/DT\([A-Z]*\);TZID=UTC:\([0-9T]*\)/DT\1:\2Z/' > /tmp/temp.ics 15 | cp /tmp/temp.ics $i 16 | done 17 | 18 | ~/bin/cal_futureonly.pl --infile=full/justin.ics --file=justin.ics 19 | ~/bin/cal_futureonly.pl --infile=full/family.ics --file=family.ics 20 | ~/bin/cal_futureonly.pl --infile=full/son1.ics --file=son1.ics 21 | ~/bin/cal_futureonly.pl --infile=full/vmd.ics --file=vmd.ics 22 | -------------------------------------------------------------------------------- /cal_futureonly.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # 3 | # cal_futureonly.pl - 4 | # Reads iCal files and outputs events between 1 month ago and 1 year from now. 5 | # Copyright (c) 2005, 2007, 2019 Justin B. Alcorn 6 | 7 | =head1 SYNOPSIS 8 | 9 | cal_futureonly.pl --file=filname.ics > output.ics 10 | 11 | --help Usage 12 | --man Complete man page 13 | --infile (REQUIRED) name of input calendar file 14 | --file (REQUIRED) name of output calendar file 15 | 16 | Expects an ICAL stream on STDIN. Converts it to the format 17 | used by the C script and prints it to STDOUT. 18 | 19 | =head2 --infile 20 | 21 | Input file 22 | 23 | =head2 --file 24 | 25 | Output File 26 | 27 | =cut 28 | 29 | use strict; 30 | use Data::ICal; 31 | use Data::ICal::Entry; 32 | use DateTime::Span; 33 | use Data::ICal::DateTime; 34 | use DateTime; 35 | use Getopt::Long 2.24 qw':config auto_help'; 36 | use Pod::Usage; 37 | use Data::Dumper; 38 | use vars '$VERSION'; 39 | $VERSION = "0.1"; 40 | 41 | my $help; 42 | my $man; 43 | my $infile; 44 | my $file; 45 | my $debug = 0; 46 | 47 | GetOptions ( 48 | "help|?" => \$help, 49 | "man" => \$man, 50 | "debug" => \$debug, 51 | "infile=s" => \$infile, 52 | "file=s" => \$file 53 | ); 54 | pod2usage(1) if $help; 55 | pod2usage(1) if (! $file); 56 | pod2usage(-verbose => 2) if $man; 57 | 58 | my $limit = DateTime->now(); 59 | $limit->subtract( months => 1); 60 | my $endlimit = DateTime->now()->add(years =>1); 61 | print STDERR "including events from: ",$limit->ymd," to: ".$endlimit->ymd,"\n" if $debug; 62 | my $span = DateTime::Span->from_datetimes( start => $limit, end => $endlimit ); 63 | print STDERR "Parsing $infile\n" if $debug; 64 | my $cal = Data::ICal->new(filename => $infile); 65 | if (! $cal) { 66 | die "Died Trying to read $infile :".$cal->error_message; 67 | } 68 | #my $archive = Data::ICal->new(filename => 'archive.ics'); 69 | print "Output = $file\n" if $debug; 70 | my $new = Data::ICal->new(); 71 | if (! $new) { 72 | die $new->error_message; 73 | } 74 | 75 | my @events = $cal->events($span); 76 | $new->add_entries(@events); 77 | 78 | open(NEW, ">$file"); 79 | print NEW $new->as_string; 80 | close NEW; 81 | exit 0; 82 | #:vim set ft=perl ts=4 sts=4 expandtab : 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ical2rem 2 | The original iCal to Remind script, first released in 2005. 3 | 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 | 7 | ## License 8 | In 2005, this was released with the Gnu Public License V2. However, I am changing it to the MIT License, since that provides greater freedom to do with this code what you want. 9 | 10 | Copyright (c) 2005, 2007, 2019 Justin B. Alcorn 11 | 12 | ## How I use Remind and Google Calendar together 13 | 14 | - My family has a Google Email domain, and our email addresses all end in the same domain. We all use Google Calendars and I want to mail reminders to each of the family members containing both Google Calendar and .reminder information. 15 | - Under my ~/.rem/ directory each family member has a directory. Each directory contains a standard remind file called 'reminders' that at the very least has the line "INCLUDE /home/jalcorn/.rem//ical2rem" and flag files indicating whether they want Daily or Weekly reminders. My reminders files references my standard .reminders file, and I also have a flag so if I run a Test run I'll get it. There's actually a lot more files (I have a big family). 16 | ```` 17 | ./rem 18 | ./son1: 19 | drwxrwxr-x 2 jalcorn jalcorn 4096 Dec 12 14:02 . 20 | drwxr-xr-x 12 jalcorn jalcorn 4096 Dec 12 14:13 .. 21 | -rw-rw-r-- 1 jalcorn jalcorn 51 Mar 3 06:10 ical2rem 22 | lrwxrwxrwx 1 jalcorn jalcorn 33 Oct 27 2016 son1.ics -> /home/jalcorn/calendars/son1.ics 23 | -rw-rw-r-- 1 jalcorn jalcorn 976 Dec 12 14:02 reminders 24 | -rw-rw-r-- 1 jalcorn jalcorn 0 Oct 27 2016 Weekly 25 | 26 | ./justin: 27 | drwxrwxr-x 2 jalcorn jalcorn 4096 Feb 27 08:29 . 28 | drwxr-xr-x 12 jalcorn jalcorn 4096 Dec 12 14:13 .. 29 | lrwxrwxrwx 1 jalcorn jalcorn 32 Oct 27 2016 son1.ics -> /home/jalcorn/calendars/son1.ics 30 | -rw-rw-r-- 1 jalcorn jalcorn 0 Nov 7 2016 Daily 31 | lrwxrwxrwx 1 jalcorn jalcorn 34 Oct 27 2016 family.ics -> /home/jalcorn/calendars/family.ics 32 | -rw-rw-r-- 1 jalcorn jalcorn 37320 Mar 3 06:10 ical2rem 33 | lrwxrwxrwx 1 jalcorn jalcorn 34 Oct 27 2016 justin.ics -> /home/jalcorn/calendars/justin.ics 34 | lrwxrwxrwx 1 jalcorn jalcorn 24 Nov 7 2016 reminders -> /home/jalcorn/.reminders 35 | lrwxrwxrwx 1 jalcorn jalcorn 34 Oct 27 2016 vmd.ics -> /home/jalcorn/calendars/vmd.ics 36 | -rw-rw-r-- 1 jalcorn jalcorn 0 Oct 27 2016 Test 37 | -rw-rw-r-- 1 jalcorn jalcorn 0 Nov 7 2016 Weekly 38 | ```` 39 | - bin/getgooglecals.sh runs out of crontab and downloads whatever calendars I want. Note that we can also download organization calendars, I've included a public one here (Cleveland Heights Vocal Music Department calendar). 40 | - dailyreminders.sh is linked to weeklyreminders.sh and testreminders.sh so I can run it in different modes. The concatenate the various calendar outputs as a single remind file then send the reminders via email. 41 | ### Example: .rem/son1/reminders file: 42 | ```` 43 | INCLUDE /home/jalcorn/.rem/defs.rem 44 | INCLUDE /home/jalcorn/.rem/float 45 | INCLUDE /home/jalcorn/.rem/son1/ical2rem 46 | fset _weeks() coerce("STRING", (trigdate()-today())/7) + plural((trigdate()-today())/7, " week") 47 | FSET _sfun(x) choose(x, -60, 30, 5, 0) 48 | FSET oldfloat(y,m,d) trigger(MAX(realtoday(), date(y,m,d))) 49 | FSET due(y,m,d) "(" + (date(y,m,d)-trigdate()) + ")" 50 | SET fullmoon moondate(2) 51 | REM [trigger(realtoday())] SPECIAL SHADE 145 70 100 % 52 | REM [float(2019,4,15,105)] MSG File tax return [due(2017,4,15)]% 53 | REM PRIORITY 9999 MSG %"%"% 54 | INCLUDE /home/jalcorn/.rem/bdays 55 | SET $LongDeg 81 56 | SET $LongMin 11 57 | SET $LongSec 11 58 | SET $LatDeg 41 59 | SET $LatMin 11 60 | SET $LatSec 11 61 | REM [trigger(moondate(2))] +1 MSG %"Full Moon%" %b% 62 | fset _srtd() coerce("STRING", _no_lz(_am_pm(sunrise(today())))) 63 | fset _sstd() coerce("STRING", _no_lz(_am_pm(sunset(today())))) 64 | MSG Sun is up today from [_srtd()] to [_sstd()].%"%"% 65 | ```` 66 | ## Revision History 67 | ### Version 0.8 2024-10-16 68 | - ISSUE 10: All day events were appearing twice because they included 00:00. Check and remove those entries. 69 | ### Version 0.7 2024-09-04 70 | - ISSUE 8: New version of remind complains if _sfun isn't defined. Output a header 71 | to define a function that does nothing if the function doesn't exist. 72 | ### Version 0.6 2019-03-01 73 | - Publish on GitHub and change license to MIT License 74 | - Add supporting files and explanation of how I use it 75 | ### version 0.5.2 2007-03-23 76 | - BUG: leadtime for recurring events had a max of 4 instead of DEFAULT_LEAD_TIME 77 | - remove project-lead-time, since Category was a non-standard attribute 78 | - NOTE: There is a bug in iCal::Parser v1.14 that causes multiple calendars to 79 | fail if a calendar with recurring events is followed by a calendar with no 80 | recurring events. This has been reported to the iCal::Parser author. 81 | ### version 0.5.1 2007-03-21 82 | - BUG: Handle multiple calendars on STDIN 83 | - add --heading option for priority on section headers 84 | ### version 0.5 2007-03-21 85 | - Add more help options 86 | - --project-lead-time option 87 | - Supress printing of heading if there are no todos to print 88 | ### version 0.4 89 | - Version 0.4 changes all written or inspired by, and thanks to Mark Stosberg 90 | - Change to GetOptions 91 | - Change to pipe 92 | - Add --label, --help options 93 | - Add Help Text 94 | - Change to subroutines 95 | - Efficiency and Cleanup 96 | ### version 0.3 97 | - Convert to GPL (Thanks to Mark Stosberg) 98 | - Add usage 99 | ### version 0.2 100 | - add command line switches 101 | - add debug code 102 | - add SCHED _sfun keyword 103 | - fix typos 104 | ### version 0.1 - ALPHA CODE. 105 | 106 | -------------------------------------------------------------------------------- /ical2rem.pl: -------------------------------------------------------------------------------- 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 | # MIT License 7 | # 8 | # Copyright (c) 2005, 2007, 2019 Justin B. Alcorn 9 | # 10 | # Permission is hereby granted, free of charge, to any person obtaining a copy 11 | # of this software and associated documentation files (the "Software"), to deal 12 | # in the Software without restriction, including without limitation the rights 13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | # copies of the Software, and to permit persons to whom the Software is 15 | # furnished to do so, subject to the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be included in all 18 | # copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | # SOFTWARE. 27 | # 28 | # version 0.8 2024-10-16 29 | # - Issue 9. Remove extra day notation on all day events. 30 | # version 0.7.1 2024-09-19 31 | # - Made sure license statements were consistent 32 | # version 0.7 2024-09-04 33 | # - Added dummy _sfun to resolve Issue #8 34 | # version 0.6 2019-03-01 35 | # - Updates to put on GitHub 36 | # version 0.5.2 2007-03-23 37 | # - BUG: leadtime for recurring events had a max of 4 instead of DEFAULT_LEAD_TIME 38 | # - remove project-lead-time, since Category was a non-standard attribute 39 | # - NOTE: There is a bug in iCal::Parser v1.14 that causes multiple calendars to 40 | # fail if a calendar with recurring events is followed by a calendar with no 41 | # recurring events. This has been reported to the iCal::Parser author. 42 | # version 0.5.1 2007-03-21 43 | # - BUG: Handle multiple calendars on STDIN 44 | # - add --heading option for priority on section headers 45 | # version 0.5 2007-03-21 46 | # - Add more help options 47 | # - --project-lead-time option 48 | # - Supress printing of heading if there are no todos to print 49 | # version 0.4 50 | # - Version 0.4 changes all written or inspired by, and thanks to Mark Stosberg 51 | # - Change to GetOptions 52 | # - Change to pipe 53 | # - Add --label, --help options 54 | # - Add Help Text 55 | # - Change to subroutines 56 | # - Efficiency and Cleanup 57 | # version 0.3 58 | # - Convert to GPL (Thanks to Mark Stosberg) 59 | # - Add usage 60 | # version 0.2 61 | # - add command line switches 62 | # - add debug code 63 | # - add SCHED _sfun keyword 64 | # - fix typos 65 | # version 0.1 - ALPHA CODE. 66 | 67 | =head1 SYNOPSIS 68 | 69 | cat /path/to/file*.ics | ical2rem.pl > ~/.ical2rem 70 | 71 | All options have reasonable defaults: 72 | --label Calendar name (Default: Calendar) 73 | --start Start of time period to parse (parsed by str2time) 74 | --end End of time period to parse 75 | --lead-time Advance days to start reminders (Default: 3) 76 | --todos, --no-todos Process Todos? (Default: Yes) 77 | --iso8601 Use YYYY-MM-DD date format 78 | --locations, --no-locations Include location? (Default: Yes) 79 | --end-times, --no-end-times Include event end times in reminder text 80 | (Default: No) 81 | --heading Define a priority for static entries 82 | --help Usage 83 | --debug Enable debug output 84 | --man Complete man page 85 | 86 | Expects an ICAL stream on STDIN. Converts it to the format 87 | used by the C script and prints it to STDOUT. 88 | 89 | =head2 --label 90 | 91 | ical2rem.pl --label "Bob's Calendar" 92 | 93 | The syntax generated includes a label for the calendar parsed. 94 | By default this is "Calendar". You can customize this with 95 | the "--label" option. 96 | 97 | =head2 --iso8601 98 | 99 | Use YYYY-MM-DD date format in output instead of Mmm DD YYYY 100 | 101 | =head2 --locations, --no-locations 102 | 103 | Whether or not to include locations in events 104 | 105 | =head2 --lead-time 106 | 107 | ical2rem.pl --lead-time 3 108 | 109 | How may days in advance to start getting reminders about the events. Defaults to 3. 110 | 111 | =head2 --no-todos 112 | 113 | ical2rem.pl --no-todos 114 | 115 | If you don't care about the ToDos the calendar, this will surpress 116 | printing of the ToDo heading, as well as skipping ToDo processing. 117 | 118 | =head2 --heading 119 | 120 | ical2rem.pl --heading "PRIORITY 9999" 121 | 122 | Set an option on static messages output. Using priorities can made the static messages look different from 123 | the calendar entries. See the file defs.rem from the remind distribution for more information. 124 | 125 | =cut 126 | 127 | use strict; 128 | use iCal::Parser; 129 | use Date::Parse; 130 | use DateTime; 131 | use Getopt::Long 2.24 qw':config auto_help'; 132 | use Pod::Usage; 133 | use Data::Dumper; 134 | use vars '$VERSION'; 135 | $VERSION = "0.5.2"; 136 | 137 | # Declare how many days in advance to remind 138 | my $DEFAULT_LEAD_TIME = 3; 139 | my $PROCESS_TODOS = 1; 140 | my $HEADING = ""; 141 | my $help; 142 | my $debug; 143 | my $man; 144 | my $iso8601; 145 | my $do_location = 1; 146 | my $do_end_times; 147 | my $start; 148 | my $end; 149 | 150 | my $label = 'Calendar'; 151 | GetOptions ( 152 | "label=s" => \$label, 153 | "start=s" => \$start, 154 | "end=s" => \$end, 155 | "lead-time=i" => \$DEFAULT_LEAD_TIME, 156 | "todos!" => \$PROCESS_TODOS, 157 | "iso8601!" => \$iso8601, 158 | "locations!" => \$do_location, 159 | "end-times!" => \$do_end_times, 160 | "heading=s" => \$HEADING, 161 | "help|?" => \$help, 162 | "debug" => \$debug, 163 | "man" => \$man 164 | ) or pod2usage(1); 165 | pod2usage(1) if $help; 166 | pod2usage(-verbose => 2) if $man; 167 | 168 | my $month = ['None','Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; 169 | 170 | my @calendars; 171 | my $in; 172 | 173 | while (<>) { 174 | $in .= $_; 175 | if (/END:VCALENDAR/) { 176 | push(@calendars,$in); 177 | $in = ""; 178 | } 179 | } 180 | print STDERR "Read all calendars\n" if $debug; 181 | my(%parser_opts) = ("debug" => $debug); 182 | if ($start) { 183 | my $t = str2time($start); 184 | die "Invalid time $start\n" if (! $t); 185 | $parser_opts{'start'} = DateTime->from_epoch(epoch => $t); 186 | } 187 | if ($end) { 188 | my $t = str2time($end); 189 | die "Invalid time $end\n" if (! $t); 190 | $parser_opts{'end'} = DateTime->from_epoch(epoch => $t); 191 | } 192 | print STDERR "About to parse calendars\n" if $debug; 193 | my $parser = iCal::Parser->new(%parser_opts); 194 | my $hash = $parser->parse_strings(@calendars); 195 | print STDERR "Calendars parsed\n" if $debug; 196 | 197 | ############################################################## 198 | # 199 | # Subroutines 200 | # 201 | ############################################################# 202 | # 203 | # _process_todos() 204 | # expects 'todos' hashref from iCal::Parser is input 205 | # returns String to output 206 | sub _process_todos { 207 | my $todos = shift; 208 | 209 | my ($todo, @newtodos, $leadtime); 210 | my $output = ""; 211 | 212 | $output .= 'REM '.$HEADING.' MSG '.$label.' ToDos:%"%"%'."\n"; 213 | 214 | # For sorting, make sure everything's got something 215 | # To sort on. 216 | my $now = DateTime->now; 217 | for $todo (@{$todos}) { 218 | # remove completed items 219 | if ($todo->{'STATUS'} && $todo->{'STATUS'} eq 'COMPLETED') { 220 | next; 221 | } elsif ($todo->{'DUE'}) { 222 | # All we need is a due date, everything else is sugar 223 | $todo->{'SORT'} = $todo->{'DUE'}->clone; 224 | } elsif ($todo->{'DTSTART'}) { 225 | # for sorting, sort on start date if there's no due date 226 | $todo->{'SORT'} = $todo->{'DTSTART'}->clone; 227 | } else { 228 | # if there's no due or start date, just make it now. 229 | $todo->{'SORT'} = $now; 230 | } 231 | push(@newtodos,$todo); 232 | } 233 | if (! (scalar @newtodos)) { 234 | return ""; 235 | } 236 | # Now sort on the new Due dates and print them out. 237 | for $todo (sort { DateTime->compare($a->{'SORT'}, $b->{'SORT'}) } @newtodos) { 238 | my $due = $todo->{'SORT'}->clone(); 239 | my $priority = ""; 240 | if (defined($todo->{'PRIORITY'})) { 241 | if ($todo->{'PRIORITY'} == 1) { 242 | $priority = "PRIORITY 1000"; 243 | } elsif ($todo->{'PRIORITY'} == 3) { 244 | $priority = "PRIORITY 7500"; 245 | } 246 | } 247 | if (defined($todo->{'DTSTART'}) && defined($todo->{'DUE'})) { 248 | # Lead time is duration of task + lead time 249 | my $diff = ($todo->{'DUE'}->delta_days($todo->{'DTSTART'})->days())+$DEFAULT_LEAD_TIME; 250 | $leadtime = "+".$diff; 251 | } else { 252 | $leadtime = "+".$DEFAULT_LEAD_TIME; 253 | } 254 | $output .= "REM ".$due->month_abbr." ".$due->day." ".$due->year." $leadtime $priority MSG \%a $todo->{'SUMMARY'}\%\"\%\"\%\n"; 255 | } 256 | $output .= 'REM '.$HEADING.' MSG %"%"%'."\n"; 257 | return $output; 258 | } 259 | 260 | 261 | ####################################################################### 262 | # 263 | # Main Program 264 | # 265 | ###################################################################### 266 | 267 | # Issue 8 https://github.com/jbalcorn/ical2rem/issues/8 268 | # Make sure there is a _sfun function declared in the reminder file. We'll just make it do nothing here. 269 | print 'IF args("_sfun") < 1 270 | FSET _sfun(x) choose(x,0) 271 | ENDIF 272 | '; 273 | 274 | print _process_todos($hash->{'todos'}) if $PROCESS_TODOS; 275 | 276 | my ($leadtime, $yearkey, $monkey, $daykey,$uid,%eventsbyuid); 277 | print 'REM '.$HEADING.' MSG '.$label.' Events:%"%"%'."\n"; 278 | my $events = $hash->{'events'}; 279 | foreach $yearkey (sort keys %{$events} ) { 280 | my $yearevents = $events->{$yearkey}; 281 | foreach $monkey (sort {$a <=> $b} keys %{$yearevents}){ 282 | my $monevents = $yearevents->{$monkey}; 283 | foreach $daykey (sort {$a <=> $b} keys %{$monevents} ) { 284 | my $dayevents = $monevents->{$daykey}; 285 | foreach $uid (sort { 286 | DateTime->compare($dayevents->{$a}->{'DTSTART'}, $dayevents->{$b}->{'DTSTART'}) 287 | } keys %{$dayevents}) { 288 | my $event = $dayevents->{$uid}; 289 | if ($eventsbyuid{$uid}) { 290 | my $curreventday = $event->{'DTSTART'}->clone; 291 | $curreventday->truncate( to => 'day' ); 292 | $eventsbyuid{$uid}{$curreventday->epoch()} =1; 293 | for (my $i = 0;$i < $DEFAULT_LEAD_TIME && !defined($event->{'LEADTIME'});$i++) { 294 | if ($eventsbyuid{$uid}{$curreventday->subtract( days => $i+1 )->epoch() }) { 295 | $event->{'LEADTIME'} = $i; 296 | } 297 | } 298 | # Issue 9. Multi-day events have extra day with zero time. Mark this as the date not needed in reminders file so we can skip later 299 | # Repeating multi-day events have the same uid so we need to mark each one. 300 | if ($event->{'DTSTART'} eq $event->{'DTEND'} and $event->{'DTEND'}->hour eq 0 and $event->{'DTEND'}->minute eq 0 and $event->{'DTEND'}->second eq 0) { 301 | $eventsbyuid{$uid}{$event->{'DTSTART'}->ymd} = 'rm'; 302 | } 303 | } else { 304 | $eventsbyuid{$uid} = $event; 305 | my $curreventday = $event->{'DTSTART'}->clone; 306 | $curreventday->truncate( to => 'day' ); 307 | $eventsbyuid{$uid}{$curreventday->epoch()} =1; 308 | } 309 | } 310 | } 311 | } 312 | } 313 | foreach $yearkey (sort keys %{$events} ) { 314 | my $yearevents = $events->{$yearkey}; 315 | foreach $monkey (sort {$a <=> $b} keys %{$yearevents}){ 316 | my $monevents = $yearevents->{$monkey}; 317 | foreach $daykey (sort {$a <=> $b} keys %{$monevents} ) { 318 | my $dayevents = $monevents->{$daykey}; 319 | foreach $uid (sort { 320 | DateTime->compare($dayevents->{$a}->{'DTSTART'}, $dayevents->{$b}->{'DTSTART'}) 321 | } keys %{$dayevents}) { 322 | my $event = $dayevents->{$uid}; 323 | if (exists($event->{'LEADTIME'})) { 324 | $leadtime = "+".$event->{'LEADTIME'}; 325 | } else { 326 | $leadtime = "+".$DEFAULT_LEAD_TIME; 327 | } 328 | my $start = $event->{'DTSTART'}; 329 | my $end = $event->{'DTEND'}; 330 | my $duration = ""; 331 | # Issue 9. All Day events create an event that has zero length and DTSTART and DTEND at 00:00 on last day. Marked while handling multi-day events. Ignore these 332 | if ($start eq $end and $eventsbyuid{$uid}{$start->ymd} and $eventsbyuid{$uid}{$start->ymd} eq 'rm') { 333 | next; 334 | } 335 | if ($end and ($start->hour or $start->minute or $end->hour or $end->minute)) { 336 | # We need both an HH:MM version of the delta, to put in the 337 | # DURATION specifier, and a human-readable version of the 338 | # delta, to put in the message if the user requested it. 339 | my $seconds = $end->epoch - $start->epoch; 340 | my $minutes = int($seconds / 60); 341 | my $hours = int($minutes / 60); 342 | $minutes -= $hours * 60; 343 | $duration = sprintf("DURATION %d:%02d ", $hours, $minutes); 344 | } 345 | print "REM "; 346 | if ($iso8601) { 347 | print $start->strftime("%F "); 348 | } else { 349 | print $start->month_abbr." ".$start->day." ".$start->year." "; 350 | } 351 | print "$leadtime "; 352 | if ($duration or $start->hour > 0 or $start->minute > 0) { 353 | print "AT "; 354 | print $start->strftime("%H:%M"); 355 | print " SCHED _sfun ${duration}MSG %a %2 "; 356 | } else { 357 | print "MSG %a "; 358 | } 359 | print "%\"", "e($event->{'SUMMARY'}); 360 | print(" at ", "e($event->{'LOCATION'})) 361 | if ($do_location and $event->{'LOCATION'}); 362 | print "\%\""; 363 | if ($do_end_times and ($start->hour or $start->minute or 364 | $end->hour or $end->minute)) { 365 | my $start_date = $start->strftime("%F"); 366 | my $start_time = $start->strftime("%k:%M"); 367 | my $end_date = $end->strftime("%F"); 368 | my $end_time = $end->strftime("%k:%M"); 369 | # We don't want leading whitespace; some strftime's support 370 | # disabling the pdding in the format string, but not all, 371 | # so for maximum portability we do it ourselves. 372 | $start_time =~ s/^\s+//; 373 | $end_time =~ s/^\s+//; 374 | my(@pieces); 375 | if ($start_date ne $end_date) { 376 | push(@pieces, $end_date); 377 | } 378 | if ($start_time ne $end_time) { 379 | push(@pieces, $end_time); 380 | } 381 | print " (-", join(" ", @pieces), ")"; 382 | } 383 | print "%\n"; 384 | } 385 | } 386 | } 387 | } 388 | 389 | sub quote { 390 | local($_) = @_; 391 | s/\[/["["]/g; 392 | return $_; 393 | } 394 | 395 | exit 0; 396 | #:vim set ft=perl ts=4 sts=4 tabstop=4 expandtab : 397 | --------------------------------------------------------------------------------