UMN Schedule to Google Calendar

Updated Fall 2019

This script ports the class schedule from MyU to your google calendar. To use:

  1. Go to MyU (myu.umn.edu) and click the Academics tab. Make sure the "My Classes" tab is selected and use the arrows navigate to the first week of classes.
  2. Right click anywhere on the page and click "Inspect". On Firefox it's called "Inspect Element" and on Safari you can follow these instructions.
  3. Scroll to the Console tab on the window that pops up. Copy and paste the following code directly into the console and press enter. Note: Never paste random code from the internet into your console! Make sure you trust me before continuing. A malicious attacker in my place could perform a myriad of actions including installing malware, unenrolling you from all of your classes and paying your tuition.
    // to run: go to myU and to the schedule page, then inspect element and paste this in the console
        function getWeekDateString(){
            let nextWeekButton = document.querySelector('[title=\'Next Week\']');
            let dateH = nextWeekButton.parentElement.children[1];
            let fullString = dateH.innerText;
            return fullString.match(/\d\d\/\d\d\/\d\d\d\d/g)[0];
        }
        
        function getClassDataForWeek(){
            let days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'];
            let weekData = {};
            for(let d of days){
                weekData[d] = [];
            }
            for(let day of days){
                let dayGroup = document.querySelector('[data-day=\''+day+'\']');
                if(!dayGroup){
                    return weekData;
                }
                // each class is an  element
                let classElements = [];
                for(let el of dayGroup.childNodes){
                    if(el.nodeName === 'A'){
                        classElements.push(el);
                    }
                }
        
                let dayClassInfo = [];
                for(let c of classElements){
                    let name = c.children[0].innerText;
                    let details = c.children[1].innerText;
                    let [type, time, place] = details.split('\n');
                    weekData[day].push({name:name,time:time,place:place,type:type});
                }
            }
            return weekData;
        }
        
        function isWeekEmpty(data){
            for(let key in data){
                if(data[key].length > 0){
                    return false;
                }
            }
            return true;
        }
        
        function nextWeek(){
            let nextWeekButton = document.querySelector('[title=\'Next Week\']');
            nextWeekButton.click();
        }
        
        Date.prototype.addDays = function(days) {
            var date = new Date(this.valueOf());
            date.setDate(date.getDate() + days);
            return date;
        }
        
        function mod(n, m) {
            return ((n % m) + m) % m;
        }
        
        function toMilitary(hour, isAM){
            if(isAM){
                if(hour === 12){
                    return 0;
                }
                return hour;
            }
            if(hour === 12){
                return 12;
            }
            return hour + 12;
        }
        
        
        let weeks = [];
        let weeksEmpty = 0;
        
        function proceed(){
            let weekData = getClassDataForWeek();
            if(weeksEmpty <= 1){
                if(isWeekEmpty(weekData)){
                    weeksEmpty++;
                }else{
                    weeksEmpty = 0;
                    weeks.push({weekDate:getWeekDateString(), data:weekData});
                }
                nextWeek();
                // wait until it finishes loading
                setTimeout(proceed, 3000);
            }else{
                let csvFile = 'Subject,Start Date,Start Time,End Date,End Time,All Day Event,Description,Location,Private';
                let days = ['Monday','Tuesday','Wednesday','Thursday','Friday'];
                let dayDateConversion = {'Monday':0,'Tuesday':1,'Wednesday':2,'Thursday':3,'Friday':4};
                for(let week of weeks){
                    // map days to dates
                    let weekDate = new Date(week.weekDate);
                    for(let d of days){
                        let classes = week.data[d];
                        for(let c of classes){
                            let classDate = weekDate.addDays(dayDateConversion[d]).toLocaleDateString();
                            let className = c.name;
                            let classLocation = c.place;
                            let classTime = c.time;
                            // isolate start/end time. myU uses the hh:mm - hh:mm AM/PM format
                            let lastIsAM = !!classTime.match(/AM/g);
                            let [startTime, endTime] = classTime.match(/\d{1,2}:\d{1,2}/g);
                            let [startTimeHour, endTimeHour] = [startTime.split(':')[0], endTime.split(':')[0]].map(x=>parseInt(x));
                            
                            // we want to know if the first hour occurs in the AM or the PM.
                            // we make the assumption that no class lasts more than 12 hours (apparently the folks
                            // who made myU thought so too), so we take the option (am vs pm) in which the class length is less
                            let militaryAMOption = toMilitary(startTimeHour, true);
                            let militaryPMOption = toMilitary(startTimeHour, false);
                            let militaryEnd = toMilitary(endTimeHour, lastIsAM);
        
                            let firstIsAM;
                            if(mod(militaryEnd - militaryAMOption, 24) < mod(militaryEnd - militaryPMOption, 24)){
                                firstIsAM = true;
                            } else {
                                firstIsAM = false;
                            }
        
                            let finalStartTime = startTime + (firstIsAM ? ' AM' : ' PM');
                            let finalEndTime = endTime + (lastIsAM ? ' AM' : ' PM');
                            let classType = c.type;
        
                            // csv columns are Subject, Start Date, Start Time, End Date, End Time, All Day Event, Description, Location, Private
                            let row = '"'+className+'","'+classDate+'","'+finalStartTime+'","'+classDate+'","'+finalEndTime+'",'+'False,"'+c.type+'","'+classLocation+'",False';
                            csvFile += '\n'+row;
                        }
                    }
                }
                
                // download as csv file
                let blob = new Blob([csvFile], {type: 'text/csv;charset=utf-8;'});
                let url = URL.createObjectURL(blob);
                let link = document.createElement("a");
                link.setAttribute("href", url);
                link.setAttribute("download", "class_schedule.csv");
                link.innerHTML= "Click Here to download";
                document.body.appendChild(link); // Required for FF
                link.click();
            }
        }
        
        proceed();
                    
  4. Wait patiently while the program scrolls through all the weeks. If you are really impatient you can change the line setTimeout(proceed, 3000); to setTimeout(proceed, 1000); or even setTimeout(proceed, 500); if you have really fast internet and are feeling lucky. After it finishes it should download a csv file called class_schedule.csv.
  5. Follow step 2 from Import Events to Google Calendar.
  6. If it's spring semester, you'll have to repeat this process for the weeks after spring break (the script stops at the first full week without classes). Reload and start at step 1 except navigate to the week after spring break instead of the first week of the semester.
Note that this method produces many individual events. Unfortunately Google does not appear to support importing recurring events.

Excel Formula Tool

Harry's Tool

Typing long expressions into Google sheets or MS excel can be a challenge. A friend thought a tool that used an math expression editor to generate an excel-style formula would be helpful. I already had a half-way expression parser sitting around from Koval's 3D Grapher, so I repurposed it here. A few notes

  • Be sure to separate adjacent variables with "*". I.e. write "a*b*c" (\(a\cdot b\cdot c\)) instead of "abc" (\(abc\)).
  • Both Google Sheets and MS Excel have very strange behavior when evaluating exponents prefixed with a negative sign. Turns out that -x^n = (-x)^n in excel language. Instead write -1*x^n.