1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
|
import EventInfo from "../models/EventInfo";
import Lab from "../models/Lab";
import PeerTeacher from "../models/PeerTeacher";
import { labStore, ptStore } from "../stores";
import { PeerTeacherImportError } from "./error";
import { get } from "svelte/store"
interface LabSchedule {
data: {
courseNumber: string,
sequenceNumber: string,
faculty: {
bannerId: string,
courseReferenceNumber: string,
displayName: string,
emailAddress: string,
}[],
meetingsFaculty: {
meetingTime: {
beginTime: string | null,
building: string,
endTime: string | null,
friday: boolean,
meetingType: string,
monday: boolean,
room: string,
thursday: boolean,
tuesday: boolean,
wednesday: boolean
}
}[],
sectionAttributes: {
description: string
}[]
}[]
};
interface DatabaseFile {
labs: {
id: number,
course: number,
section: number,
event: {
days: string,
start: number,
end: number
},
faculty: {
bannerId: string,
courseReferenceNumber: string,
displayName: string,
emailAddress: string,
}[],
building: string,
room: string,
assigned: boolean
}[],
peerTeachers: PeerTeacher[]
}
/**
* Parses a peer teacher schedule
* @param schedule The schedule to parse
* @returns A peer teacher
*/
export function parsePTSchedule(schedule: string) {
// namePatter: <firstname> <lastname> <uin>
const namePattern = /^(.*?)\s(.*)\s(\d{9})/;
// eventPattern (24hr time): MTWRF hh:mm - hh:mm
const eventPattern = /^(M?T?W?R?F?)\s(\d{1,2}:\d{2})\s?-\s?(\d{1,2}:\d{2})/;
const lines = schedule.split("\n").filter(line => line.trim());
const nameLine = lines.find(line => line.match(namePattern));
if (nameLine === undefined) {
throw new PeerTeacherImportError(`No peer teacher in schedule`);
}
const [, firstname, lastname, uin] = nameLine.match(namePattern) as RegExpMatchArray;
// TODO email should be parsed from the student's file too I guess, or just end up changing the parser to use survey results for everything, then parse schedules separately.
const email = "undefined"
const peerTeacher = new PeerTeacher(uin, firstname, lastname, email);
const events = lines
.filter(line => line.match(eventPattern))
.map(line => {
let [, days, start, end] = line.match(eventPattern) as RegExpMatchArray;
start = start.replace(":", "");
end = end.replace(":", "");
return new EventInfo(days, start, end);
});
peerTeacher.events = events;
return peerTeacher;
}
/**
* Parses the course schedule into labs attended by PTs
* @param schedule The course schedule object from Howdy
* @returns An array of labs
*/
export function parseLabSchedule(schedule: LabSchedule): Lab[] {
const taughtCourses = ['110', '111', '120', '121', '206', '221', '312', '313', '315', '331'];
const results: Lab[] = [];
const courses = schedule.data;
for (const course of courses) {
if (!taughtCourses.includes(course.courseNumber) || course.sectionAttributes[0].description === "McAllen") {
continue;
}
for (const meeting of course.meetingsFaculty) {
const { meetingTime } = meeting;
if (meetingTime.meetingType !== "LAB") {
continue;
}
let days = "";
days += meetingTime.monday ? 'M' : '';
days += meetingTime.tuesday ? 'T' : '';
days += meetingTime.wednesday ? 'W' : '';
days += meetingTime.thursday ? 'R' : '';
days += meetingTime.friday ? 'F' : '';
const start = meetingTime.beginTime === null ? -1 : meetingTime.beginTime;
const end = meetingTime.endTime === null ? -1 : meetingTime.endTime;
const { courseNumber, sequenceNumber } = course;
const { building, room } = meetingTime;
results.push(new Lab(courseNumber, sequenceNumber, new EventInfo(days, start, end), building, room, false, course.faculty));
}
}
return results;
}
/**
* Parses a database file into maps of Lab and PeerTeacher objects
* @param database The database object from a database file
* @returns And object with lab and peer teacher maps
*/
export function parseDatabase(database: DatabaseFile) {
const result = {
labs: new Map<number, Lab>(),
peerTeachers: new Map<number, PeerTeacher>()
}
database.labs.forEach(lab => {
result.labs.set(lab.id, Lab.fromJSON(lab));
});
database.peerTeachers.forEach(pt => {
result.peerTeachers.set(pt.id, PeerTeacher.fromJSON(pt));
});
return result;
}
/**
* Parses a JSON database into maps of Lab and Peer Teachers
* and updates local storage
* @param database The database object from a db file
*/
export function parseDatabaseLocalStorage(database_string: string) {
const database = JSON.parse(database_string)
const result = {
labs: new Map<number, Lab>(),
peerTeachers: new Map<number, PeerTeacher>()
}
database.labs.forEach(lab => {
result.labs.set(lab.id, Lab.fromJSON(lab));
});
database.peerTeachers.forEach(pt => {
result.peerTeachers.set(pt.id, PeerTeacher.fromJSON(pt));
});
labStore.set(result.labs);
ptStore.set(result.peerTeachers)
}
/**
* @param csv csv representation of PT Quentionairre
* The questionairre collects all sorts of information
* from Peer Teachers. The `attributes` variable below
* houses all of the attributes we want to collect.
* It looks for those attributes in the header row of the
* questionnairre to begin populating PT data.
*/
export function parseQuestionnaireCSV(csv: string) {
const attributes = [
"Timestamp",
"UIN",
"First Name",
"Last Name",
"Email Address",
"Phone Number",
"Gender",
"Racial Background",
"When are you graduating? (Day does not matter)",
"Classes you CAN peer teach for",
"Classes you PREFER to peer teach for",
"Number of hours you prefer to work",
"Are you a new hire or returning?",
"Profile picture for website (image)",
"Schedule (text file)"
]
const sheet = parseCSV(csv);
const m = mapAttributeToIndex(attributes, sheet[0]);
const pts = get(ptStore);
const data = sheet.slice(1, sheet.length);
data.forEach((row) => {
const uin = row[m["UIN"]];
const u = parseInt(uin, 10);
if (pts.has(u)) {
const pt = pts.get(u);
pt.email = row[m["Email Address"]];
pt.phone_number = row[m["Phone Number"]];
pt.gender = row[m["Gender"]];
pt.ethnicity = row[m["Racial Background"]];
pt.graduation = row[m["When are you graduating? (Day does not matter)"]]
const c_teach = row[m["Classes you CAN peer teach for"]].split(",").map((val) => parseInt(val));
// TODO The answers to the questionnaire question "what classes can you peer teach for" needs logic for parsing.It's basically trying to ParseInt("CSCE 110").
pt.can_teach = new Set(c_teach)
const p_teach = row[m["Classes you PREFER to peer teach for"]].split(",").map((val) => parseInt(val));
pt.pref_teach = new Set(p_teach);
const hours_work: number = parseInt(row[m["Number of hours you prefer to work"]]);
pt.pref_work = hours_work;
pt.new_ret = row[m["Are you a new hire or returning?"]]
pt.prof_pic_url = row[m["Profile picture for website (image)"]];
pt.schedule_url = row[m["Schedule (text file)"]];
}
})
}
/**
*
* @param csv a string in csv format
* @returns 2d array representation of CSV file
*/
function parseCSV(csv: string): string[][] {
const t = csv.split("\n");
return t.map((val) => {
const reg = new RegExp(",(?=(?:[^\"]*\"[^\"]*\")*(?![^\"]*\"))");
return val.split(reg);
});
}
/**
* @param csv csv representation of a Strawpoll output
* Specific function, should be updated to parse whatever office hour format is being used
* The current format is a Strawpoll output in csv format
* It should be a messy function, but it has to be
*
*/
export function parseOfficeHours(csv: string) {
const sheet = parseCSV(csv);
const begin = sheet.findIndex((row) => row[0].trim().toLowerCase() == "name");
const end = sheet.findIndex((row) => row[0].trim() == "Total ✓ Votes");
const pts = get(ptStore);
for (let i = begin + 1; i < end; i++) {
const pt_uin = parseInt(sheet[i][0]);
const pt = pts.get(pt_uin);
if (pt != undefined && pt != null)
pt.office_hours = parseStrawpollTimesEntry(sheet[i], sheet[begin]);
}
// Merge office hours that take place during the same time of day
pts.forEach((pt) => {
if (pt.office_hours != undefined && pt.office_hours != null) {
const flag = {};
const unique: EventInfo[] = [];
pt.office_hours.forEach((curr_event) => {
const key = `${curr_event.start}${curr_event.end}`;
if (!flag[key]) {
flag[key] = true;
unique.push(curr_event);
}
else {
const head_event = unique.find((val) => {
return val.start === curr_event.start && val.end === curr_event.end;
})
head_event.days += curr_event.days;
}
})
pt.office_hours = unique;
}
});
}
/**
*
* @param pt_slots Strawpoll: peer teacher's chosen hours row
* @param time_slots Strawpoll: available office hours row (same row as cell 'Name')
* @returns List of office hours (events)
*/
function parseStrawpollTimesEntry(pt_slots: string[], time_slots: string[]): EventInfo[] {
let res = new Array<EventInfo>;
for (let i = 0; i < pt_slots.length; i++) {
if (pt_slots[i] == "1") {
const e_i = timeslotToEvent(time_slots[i]);
while (pt_slots[i + 1] == "1") ++i;
// * Hacky bugfix for last timeslot never being used
const e_f = i == pt_slots.length - 2 ? timeslotToEvent(time_slots[i + 1]) : timeslotToEvent(time_slots[i]);
res.push(new EventInfo(e_i.days, e_i.start, e_f.end));
}
};
return res
}
function timeslotToEvent(time_slot: string): EventInfo {
const regex = /\(([^)]*)\)/; // find value between parantheses eg ($1) find $1
const match = time_slot.match(regex)
const strawpoll_date = time_slot.split(" ")[0] + `T${match[1].split(" ")[0]}`;
const start = match[1].split(" ")[0].replace(":", "");
const end = match[1].split(" ")[2].replace(":", "");
const date = new Date(strawpoll_date);
const map_UTC_day = { 0: "U", 1: "M", 2: "T", 3: "W", 4: "R", 5: "F", 6: "S" };
const day = map_UTC_day[date.getDay()];
return new EventInfo(day, start, end)
}
/**
* @param {Array} attributes Strings of attributes to look for
* @param {Array} title_row Title row of sheet (usually first row: data[0])
* @return {Object} [key: attribute, value: index]
*/
function mapAttributeToIndex(attributes: string[], title_row: string[]) {
return attributes.reduce((prev, curr) => ({
...prev,
[curr]: title_row.findIndex((cell) => cell.toString().toLocaleLowerCase().split(" ").join("").trim() == curr.toLocaleLowerCase().split(" ").join("").trim())
}), {})
}
|