#!/usr/bin/perl
# funswap.pl
# EDIT: modified by Calvin,
# Changes include:
# -changed $weekStart, starting date of semester (now works for 12s2)
# -changed $OUTFILE, output file name to timetable12s2.ics
# -changed $weekBeforeBreak
#
# written by Rowan Katekar (rowan.katekar@gmail.com)
# released under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
# http://creativecommons.org/licenses/by-nc-sa/3.0/
# USAGE SUGGESTION:
# When importing into google calendar, create a special calendar just for this timetable.
# That way, if something goes wrong you can delete the events from the calendar all at once, rather
# than having to delete them one by one.
# uses DateTime module - install from cpan using
# sudo apt-get install libdatetime-perl
# reads in UNSW timetable html file
# outputs .ics file for importing into Outlook, Evolution, Google Calendar, ... etc.
# assumes UNSW timetable html file is in the same folder as the perl script and called "timetable.html"
## Edited by William Pickering (@YodaDaCoda)
## * Changed from cat timetable.html (linux only file handling) to native perl file handling.
## Solves problems with things being printed to the terminal and generally not working
## properly on Windows.
## * Changed date logic
## Every single class was set to be on the next week from the previous class,
## leading to classes being held years into the future.
# updated by Rowan Katekar again on 11-12-2011
# * Changed dates to semester 1 2012 dates.
# * Also changed the lecture name to the code of the subject
# * Subject title is now part of the description
use DateTime;
# convert time of the format /(\d)?\d:\d\d[AP]M/ to a number from 0..23
sub conv24($);
# convert a weekday abbreviation to a number from 1-5
sub dayToNumber($);
# substitutes event values into iCal string & returns resultant string
sub subInValues($$$$$);
## Gets the minutes of a specified time string.
sub getMins($);
# First day of semester (currently set to first day of semester for 2012 semester 1)
$weekStart = DateTime->new( year=>2012, month=>7, day=>16, time_zone => 'Australia/Sydney' );
# Last week before mid-semester break
$weekBeforeBreak = 7;
# destination of ical file
$OUTFILE = "timetable12s2.ics";
# file to read from
$INFILE = "timetable.html";
# crap to ignore
@ignore = ("Teaching", "Status", "Enrolled", "Units", "Campus", "Activity", "Section",
"Weeks", "Location", "Instructor", "bsdsSequence", "outerTable", "Key",
"printBt", "Print", "onClick", "Site Map", "Site Feedback", "Privacy Statement",
"Provider", "Copyright");
##open files for reading/writing
open OUTFILE, ">", $OUTFILE or die $!;
open INFILE, "<", $INFILE or die $!;
# read in page
@lines = <INFILE>;
close INFILE;
##convert from array to one long string
$page = join (' ', @lines);
# split into sections based on subject
@sections = split (/([A-Z]{4}[0-9]{4}\s-.*?)</, $page);
shift @sections;
# do for each subject
foreach $section (@sections) {
@useful = ();
# print subject title
if ($section =~ m/[A-Z]{4}[0-9]{4}\s-/) {
$subject = $section;
} else {
# extract everything that's not in html tags
@info = ($section =~ m/>([\w-:;,\s\(\)\&]*?[A-Z0-9]+[\w-:;,\s\)\&]*?)</g);
foreach (@info) {
push @useful, $_ if !(m/(data|row|table|sectionHeading)/ || m/^\s*\d+\s*$/);
}
# find times, classes, locations
foreach (@useful) {
$classes{$subject} .= "\n" if m/^Lecture|Tutorial|Laboratory$/;
s/^\s*// && s/\s*$//;
# ignore garbage
if (!m/^[A-Z0-9]+$/ or m/Lecture|Tutorial|Laboratory|Mon|Tue|Wed|Thu|Fri/) {
$garbage = 0;
foreach $word (@ignore) {
$garbage = 1 if ($_ =~ m/$word/);
}
$classes{$subject} .= "$_\n" if not $garbage;
}
}
}
}
# go through classes for each subject
foreach $subject (keys %classes) {
@lines = split /\n/, $classes{$subject};
$lesson = 0;
for $i (0..$#lines) {
# get day
if ($lines[$i] =~ m/\b(Mon|Tue|Wed|Thu|Fri)\b/) {
$classDays{$subject}[$lesson] = $1;
$lesson++;
}
# get weeks
if ($lines[$i] =~ m/^[\d,-]+$/) {
while ($lines[$i] =~ m/(\d+-\d+)/) {
$replacement = "";
$numbers = $1;
$numbers =~ m/(\d+)-(\d+)/;
$from = $1;
$to = $2;
$replacement .= "$_," for ($from..$to);
$replacement =~ s/,$//;
$lines[$i] =~ s/$numbers/$replacement/;
}
$classWeeks{$subject}[$lesson - 1] = $lines[$i]; # hax
$classLocation{$subject}[$lesson - 1] = $lines[$i + 1]; # get location
}
# get time
$classTimes{$subject}[$lesson - 1] = $1 if ($lines[$i] =~ m/(\d+:\d+[AP]M - \d+:\d+[AP]M)/);
# get class type (lecture/tute/lab)
$classType = $1 if ($lines[$i] =~ m/(Lecture|Tutorial|Laboratory)/ or $lines[$i] =~ m/(.*\bof\b.*)/ );
$classTypes{$subject}[$lesson] = $classType;
}
$classTypes{$subject}[$lesson] = ""; # moar hax
}
# ical 'header'
$STARTCALENDAR = <<START;
BEGIN:VCALENDAR
PRODID:-//fun swap//hacked together perl//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
START
# *START*, *END*, *EVENT* etc. are substituted with actual class details later on
$EVENTSTRING = <<EVENT;
BEGIN:VEVENT
DTSTART:*START*
DTEND:*END*
SUMMARY:*TITLE*
LOCATION:*LOCATION*
DESCRIPTION:*DESCRIPTION*
SEQUENCE:0
END:VEVENT
EVENT
# goes at the end of the produced file
$ENDCALENDAR = "END:VCALENDAR";
print OUTFILE $STARTCALENDAR;
foreach $subject (keys %classes) {
foreach $lesson (0..length($classTypes{$subject})) {
if (defined $classDays{$subject}[$lesson]) {
@weeksClassIsOn = split (/,/,$classWeeks{$subject}[$lesson]);
# convert start and end times to 24h time
@times = split (/ - /, $classTimes{$subject}[$lesson]);
$startClassH = conv24($times[0]);
$startClassM = getMins($times[0]);
$endClassH = conv24($times[1]);
$endClassM = getMins($times[0]);
# format the title of the event as "[SUBJECT CODE] [Lecture|Tutorial|Laboratory]"
$subject;
@subjectCode = split(" - ", $subject);
$subjectCode = @subjectCode[0];
@subjectName = split(" - ", $subject);
$subjectName = @subjectName[1];
$subjectCode =~ s/^([A-Z]{4}[0-9]{4}).*$/$1/;
$classType = $classTypes{$subject}[$lesson];
$title = "$subjectCode $classType";
$description = "$subjectName";
# location of event
$location = $classLocation{$subject}[$lesson];
# add classes to calendar
for $week (1..13) { #for each week in the timetable
if ($week ~~ @weeksClassIsOn) { #if the class is on that week
##starting date
$weekDate = $weekStart->clone();
$weekDate->add( weeks => ($week -1) );
# skip the week of mid-session break
$weekDate->add( days => 7) if ($week > $weekBeforeBreak);
$weekDate->add( days => dayToNumber($classDays{$subject}[$lesson]) - 1 );
##set the time the class starts
$startTime = $weekDate->clone();
$startTime->set_hour($startClassH);
$startTime->set_minute($startClassM);
##set the time the class ends
$endTime = $weekDate->clone();
$endTime->set_hour($endClassH);
$endTime->set_minute($endClassM);
##DateTime hackery pary one...
##Time part of the DateTime stamp needs to be formatted in Zulu time for compatibility with google calendar.
##If at least the time part is not in Zulu, all events show up as being one hour in duration.
##Convert time to UTC...
$startTime->set_time_zone('UTC');
$endTime->set_time_zone('UTC');
# convert DateTime formatting to iCal formatting
$startTime =~ s/[^0-9A-Z]//g;
$endTime =~ s/[^0-9A-Z]//g;
##DateTime hackery part 2...
##Append Z for Zulu timezone to the times
$startTime = $startTime."Z";
$endTime = $endTime."Z";
# substitute values into iCal event string
$icalEvent = subInValues($title, $location, $startTime, $endTime, $description);
## append to file
print OUTFILE $icalEvent;
}
}
}
}
}
## append the final line to the file... and we're done
print OUTFILE $ENDCALENDAR;
close OUTFILE;
##return the minutes of a given time
sub getMins($) {
my $mins = $_[0];
$mins =~ m/.*?:(.{2}).*?/;
$mins = $1;
return $mins;
}
# convert a weekday abbreviation to a number from 1-5
sub dayToNumber($) {
my $dayName = $_[0];
my %days = ("Mon" => 1,
"Tue" => 2,
"Wed" => 3,
"Thu" => 4,
"Fri" => 5);
return $days{$dayName};
}
# convert time of the format /(\d)?\d:\d\d[AP]M/ to a number from 0..23
sub conv24($) {
my $time = $_[0];
$hour = $time;
$hour =~ s/:.*$//;
$hour += 12 if ($time =~ m/PM/ && $hour < 12); # convert to 24h time
return $hour;
}
# substitutes event values into iCal string & returns resultant string
sub subInValues($$$$$) {
my ($title, $location, $startTime, $endTime, $description) = @_[0..4];
my $icalEvent = $EVENTSTRING;
$icalEvent =~ s/\*START\*/$startTime/;
$icalEvent =~ s/\*END\*/$endTime/;
$icalEvent =~ s/\*TITLE\*/$title/;
$icalEvent =~ s/\*LOCATION\*/$location/;
$icalEvent =~ s/\*DESCRIPTION\*/$description/;
return $icalEvent;
}