Skip to content

Instantly share code, notes, and snippets.

@haylinmoore
Created September 26, 2022 04:40
Show Gist options
  • Save haylinmoore/875961e524d6a4e45ccd1da4c8644ef1 to your computer and use it in GitHub Desktop.
Save haylinmoore/875961e524d6a4e45ccd1da4c8644ef1 to your computer and use it in GitHub Desktop.
UMass Amherst Cal Converter
<header>
<h1><img src="https://spire.umass.edu/heproda/images/wordmark.svg" style="height:0.8em"> Cal Converter - Fall 2022</h1>
</header>
<div>
<p>Please manually verify the output of this POS POC. It only works (probably, no promises) if you're currently on a device in the EDT (once we shift to EST it'll probably break) timezone. In addition it only works for the Fall 2022 session of classes at UMass Amherst. You can get your class calendar file at <a href="https://www.umass.edu/it/support/spire/download-your-class-and-final-exam-schedules#Download%20Your%20Class%20Schedule">https://www.umass.edu/it/support/spire/download-your-class-and-final-exam-schedules#Download%20Your%20Class%20Schedule</a>. The only issue is that one skips holidays and breaks, doesn't swap days with swapped classes, and ignores the last day of classes.</p>
<input type="file" onchange="readText(event)">
<p>Site made by <a href="https://hamptonmoore.com">Hampton Moore</a> | This site is not made, maintained, or approved of by the univeristy.</p>
</div>
function getSchoolDays(startDate, endDate, exceptions) {
let dates = [];
const curDate = new Date(startDate.getTime());
while (curDate <= endDate) {
const thisDate = new Date(curDate);
curDate.setDate(curDate.getDate() + 1);
let dayOfWeek = thisDate.getDay();
// Skip weekends
if (dayOfWeek == 0 || dayOfWeek == 6) {
continue;
}
let skip = false;
for (let exception of exceptions) {
if (!exception.hasOwnProperty("end")) {
exception.end = exception.start;
}
if (
exception.type != undefined &&
exception.type == "swap" &&
thisDate >= exception.start &&
thisDate <= exception.end
) {
dayOfWeek = exception.newDay;
console.log(`Swapping ${thisDate} to a day ${exception.newDay}`);
break;
}
if (thisDate >= exception.start && thisDate <= exception.end) {
console.log(`Skipping ${thisDate} due to ${exception.name}`);
skip = true;
break;
}
}
if (skip) {
continue;
}
dates.push({ date: thisDate, dayOfWeek: dayOfWeek, classes: [] });
}
return dates;
}
dayLookup = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
function extractClassDays(cal) {
let days = {};
for (let classevent of cal[2]) {
if (classevent[0] != "vevent") {
continue;
}
let classItem = {
daysOfWeek: [],
props: [],
start: {
hour: null,
minute: null
}
};
for (let prop of classevent[1]) {
switch (prop[0]) {
case "rrule":
let temp = prop[3]["byday"];
if (typeof temp == "string") {
temp = [temp];
}
classItem["daysOfWeek"] = temp.map((d) => dayLookup.indexOf(d));
break;
case "dtstart":
let date = new Date(prop[3]);
classItem["start"] = {
hour: date.getHours(),
minute: date.getMinutes()
};
break;
default:
classItem["props"].push(prop);
}
}
for (let day of classItem["daysOfWeek"]) {
if (days[day] == undefined) {
days[day] = [];
}
days[day].push(classItem);
}
}
return days;
}
function convertDayListsToClasses(dates, classList) {
return dates.map((d) => {
d["classes"] = classList[d.dayOfWeek];
d["date"] = d.date.toISOString();
return d;
});
}
Date.prototype.getISOTimezoneOffset = function () {
const offset = this.getTimezoneOffset();
return (
(offset < 0 ? "+" : "-") +
Math.floor(Math.abs(offset / 60))
.toString()
.leftPad(2) +
":" +
Math.abs(offset % 60).leftPad(2)
);
};
function generateVEvents(classDays) {
vevents = [];
for (let day of classDays) {
for (let c of day.classes) {
let props = [...c.props];
let classTime = new Date(day.date);
classTime.setHours(c.start.hour);
classTime.setMinutes(c.start.minute);
var tzoffset = classTime.getTimezoneOffset() * 60000;
var localISOTime = new Date(classTime - tzoffset).toISOString();
props.push(["dtstart", {}, "date-time", localISOTime]);
vevents.push(["vevent", props, []]);
}
}
return vevents;
}
function download(filename, text) {
var element = document.createElement("a");
element.setAttribute(
"href",
"data:text/plain;charset=utf-8," + encodeURIComponent(text)
);
element.setAttribute("download", filename);
element.style.display = "none";
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
function stringError(e) {
return (
"Error: " +
e +
("fileName" in e ? "\nFilename: " + e.fileName : "") +
("lineNumber" in e ? "\nLine: " + e.lineNumber : "") +
("stack" in e ? "\nStack: " + e.stack : "")
);
}
function convert(text) {
var duration = document.getElementById("duration");
var data = document.getElementById("data");
// data.value
let ical = ICAL.parse(text);
var startDate = new Date("09/06/2022");
var endDate = new Date("12/12/2022");
dates = getSchoolDays(startDate, endDate, exceptions);
classList = extractClassDays(ical);
classDays = convertDayListsToClasses(dates, classList);
events = generateVEvents(classDays);
vcalTemp = [
"vcalendar",
[
["calscale", {}, "text", "GREGORIAN"],
["method", {}, "text", "PUBLISH"],
["x-wr-calname", {}, "unknown", "Work"],
["x-wr-timezone", {}, "unknown", "America/New_York"],
["x-wr-caldesc", {}, "unknown", ""]
]
];
vcalTemp[2] = events;
download("newCal.ics", ICAL.stringify(vcalTemp));
}
async function readText(event) {
const file = event.target.files.item(0);
const text = await file.text();
convert(text);
}
const exceptions = [
{
start: new Date("Oct 10 2022 00:00:00"),
name: "Indigenous Peoples Day"
},
{
start: new Date("Nov 11 2022 00:00:00"),
name: "Veterans Day"
},
{
type: "swap",
start: new Date("Nov 22 2022 00:00:00"),
name: "Weird Day Swap Before November Break",
newDay: 5
},
{
start: new Date("Nov 23 2022 00:00:00"),
end: new Date("Nov 27 2022 00:00:00"),
name: "November Break"
}
];
<script src="https://unpkg.com/[email protected]/build/ical.js"></script>
html,
body {
margin: 0;
padding: 0;
width: 100%;
font-family: 'Roboto Slab', serif !important;
}
header {
margin: 0;
padding: 16px;
background-color: #881c1c;
color: white;
}
h1 {
margin: auto;
font-size: 2.2em;
text-align:center;
font-weight: 100;
}
div {
padding: 16px;
width: 100%;
max-width: 600px;
margin: auto;
font-weight: 300;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment