Skip to content

Instantly share code, notes, and snippets.

@IvanaGyro
Last active September 10, 2022 16:44
Show Gist options
  • Save IvanaGyro/e24d8da3ede07f39ed24670e03b15f9b to your computer and use it in GitHub Desktop.
Save IvanaGyro/e24d8da3ede07f39ed24670e03b15f9b to your computer and use it in GitHub Desktop.
Create the Google Calendar event links of the courses on the NTU "My Schedule" page.
javascript: (() => {
/**
* This script is used to create the Google Calendar event links of the
* courses on the NTU "My Schedule" page.
*/
if (
location.href !== "https://nol.ntu.edu.tw/nol/coursesearch/myschedule.php"
) {
return;
}
let locationTimeIdx = null;
let table = null;
/**
* Get course name and the information link from the row of the course table.
*
* @param {HTMLElement} row
* @returns {[String, String]}
*/
function getCourseNameAndLinkFromRow(row) {
/* assume the first link in the row is the course name */
const courseNameElm = row.getElementsByTagName("a")[0];
const url = new URL(courseNameElm.href);
const keys = Array.from(url.searchParams.keys());
keys.forEach((key) => {
if (!["course_id", "semester"].includes(key)) {
url.searchParams.delete(key);
}
});
return [courseNameElm.innerText, url.href];
}
/**
* Get the link of COOL platform from the row of the course table.
*
* @param {HTMLElement} row
* @returns {String=}
*/
function getCoolLink(row) {
return row.querySelector('a[href*="cool.ntu.edu.tw"]')?.href;
}
/**
* Parse the locations and time from the row of the course table.
*
* @param {HTMLElement} row
* @returns {[String, String[], String][]}
*/
function getLocationAndTime(row) {
const timeAndLocationPattern =
/([一二三四五六日])(([0-9]|10|[A-D])(,([0-9]|10|[A-D]))*)\((.+?)\)/g;
if (locationTimeIdx == null) {
locationTimeIdx = -1;
Array.from(row.getElementsByTagName("td")).some((elm) => {
locationTimeIdx += 1;
const istimeAndLocation = timeAndLocationPattern.test(elm.innerText);
timeAndLocationPattern.lastIndex = 0;
return istimeAndLocation;
});
}
return [
...row
.getElementsByTagName("td")
[locationTimeIdx].innerText.matchAll(timeAndLocationPattern),
].map((res) => [res[1], res[2].split(","), res[6]]);
}
/**
* Convert the orders of the class to numbers.
* E.g. "1" => 1, "A" => 11
*
* @param {String[]} classNo
* @returns {Number[]}
*/
function parseClassNo(classNo) {
const parsedClassNo = classNo.map((c) => {
if (/^[ABCD]$/.test(c)) {
return c.charCodeAt(0) - "A".charCodeAt(0) + 11;
}
return parseInt(c);
});
for (let i = 1; i < parsedClassNo.length; ++i) {
if (parsedClassNo[i] !== parsedClassNo[i - 1] + 1) {
throw new Error(
`Class No. are not strictly increasing: [${parsedClassNo}]`
);
}
}
return parsedClassNo;
}
/**
* Compose Google Calendar Event link from the information of the course.
*
* @param {String} courseName
* @param {String} link
* @param {String=} coolLink
* @param {[String, String[], String][]} locationAndTimePairs
* @param {String=} calendarID
* @returns {URL[]}
*/
function composeUrl(courseName, link, coolLink, locationAndTimePairs, calendarID) {
return locationAndTimePairs.map(([weekDay, classNo, location]) => {
const setToStartOfClass = (dateObj, classNoIdx) => {
const classStartTimeTable = [
[7, 10],
[8, 10],
[9, 10],
[10, 20],
[11, 20],
[12, 20],
[13, 20],
[14, 20],
[15, 30],
[16, 30],
[17, 30],
[18, 25],
[19, 20],
[20, 15],
[21, 00],
];
dateObj.setHours(classStartTimeTable[classNoIdx][0]);
dateObj.setMinutes(classStartTimeTable[classNoIdx][1]);
};
const parsedClassNo = parseClassNo(classNo);
const classStartTime = new Date(firstDate);
classStartTime.setDate(classStartTime.getDate() + dayOffsetMap[weekDay]);
setToStartOfClass(classStartTime, parsedClassNo[0]);
const classEndTime = new Date(classStartTime);
setToStartOfClass(classEndTime, parsedClassNo[parsedClassNo.length - 1]);
classEndTime.setMinutes(classEndTime.getMinutes() + 50);
const url = new URL(
"https://www.google.com/calendar/event?action=TEMPLATE"
);
url.searchParams.set("text", courseName);
const toGoogleCalendarDateString = (time) => {
const year = time.getUTCFullYear();
const month = `${time.getUTCMonth() + 1}`.padStart(2, "0");
const date = `${time.getUTCDate()}`.padStart(2, "0");
const hour = `${time.getUTCHours()}`.padStart(2, "0");
const minute = `${time.getUTCMinutes()}`.padStart(2, "0");
return `${year}${month}${date}T${hour}${minute}00Z`;
};
url.searchParams.set(
"dates",
`${toGoogleCalendarDateString(
classStartTime
)}/${toGoogleCalendarDateString(classEndTime)}`
);
url.searchParams.set("location", location);
let description = link;
if (coolLink != null) {
description += `\n\ncool: ${coolLink}`;
}
url.searchParams.set("details", description);
url.searchParams.set("recur", "RRULE:FREQ=WEEKLY;COUNT=16");
if (calendarID !== null) {
url.searchParams.set("src", calendarID);
}
return url;
});
}
/**
* @param {String} html representing a single element
* @return {Element}
*/
function htmlToElement(html) {
const template = document.createElement("template");
html = html.trim(); /* Never return a text node of whitespace as the result */
template.innerHTML = html;
return template.content.firstChild;
}
if (window.googleCalendarLinksHaveAdded != null) return;
let firstDate = prompt(
"Please enter the first day of this semester:",
"20220905"
);
if (firstDate == null || firstDate == "") {
return;
}
firstDate = new Date(
firstDate.substring(0, 4),
firstDate.substring(4, 6) - 1,
firstDate.substring(6, 8)
);
if (isNaN(firstDate)) {
/* invalid input */
return;
}
const firstDay = firstDate.getDay();
const dayOffsetMap = {
日: (7 - firstDay) % 7,
一: (8 - firstDay) % 7,
二: (9 - firstDay) % 7,
三: (10 - firstDay) % 7,
四: (11 - firstDay) % 7,
五: (12 - firstDay) % 7,
六: (13 - firstDay) % 7,
};
const calendarID = prompt(
"Target calendar ID (optional):",
""
);
if (calendarID == null) {
return;
}
table = document.getElementsByTagName("table")[0];
let rows = table.getElementsByTagName("tr");
/* Update header */
const header = rows[0];
const addToGoogleCalendarCell = `
<td>
加到
<br>
Google
<br>
日曆
</td>
`;
header.insertBefore(
htmlToElement(addToGoogleCalendarCell),
header.children[header.children.length - 1]
);
/* Update foot */
const foot = rows[rows.length - 1];
const footCell = foot.getElementsByTagName("td")[0];
footCell.setAttribute("colspan", footCell.getAttribute("colspan") - -1);
rows = Array.from(rows);
rows.shift();
rows.pop();
rows.forEach((row) => {
const [courseName, link] = getCourseNameAndLinkFromRow(row);
const coolLink = getCoolLink(row);
const locationAndTimePairs = getLocationAndTime(row);
console.debug(courseName, link, coolLink, locationAndTimePairs);
const urls = composeUrl(courseName, link, coolLink, locationAndTimePairs, calendarID);
const newCell = document.createElement("td");
if (urls.length === 1) {
newCell.append(
htmlToElement(`<a target="_blank" href="${urls[0].href}">活動</a>`)
);
} else {
urls.forEach((url, idx) => {
if (idx > 0) {
newCell.append(document.createElement("br"));
}
newCell.append(
htmlToElement(
`<a target="_blank" href="${urls[idx].href}">活動${idx + 1}</a>`
)
);
});
}
row.insertBefore(newCell, row.children[row.children.length - 1]);
});
window.googleCalendarLinksHaveAdded = true;
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment