Created
September 26, 2022 04:40
-
-
Save haylinmoore/875961e524d6a4e45ccd1da4c8644ef1 to your computer and use it in GitHub Desktop.
UMass Amherst Cal Converter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | |
} | |
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script src="https://unpkg.com/[email protected]/build/ical.js"></script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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