Skip to content

Commit 7733798

Browse files
committed
Add location parsing to user schedule and calendar export
1 parent 60161a3 commit 7733798

File tree

6 files changed

+115
-59
lines changed

6 files changed

+115
-59
lines changed

flow/api/calendar/calendar.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ func writeCalendar(w io.Writer, secretId string, events []*webcalEvent) {
136136

137137
const selectEventQuery = `
138138
SELECT
139-
sm.section_id, c.code, cs.section_name, sm.location,
139+
sm.section_id, c.code, cs.section_name, COALESCE(NULLIF(us.location, ''), sm.location),
140140
sm.start_date :: TEXT, sm.end_date :: TEXT,
141141
sm.start_seconds, sm.end_seconds, sm.days
142142
FROM

flow/api/parse/parse.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,8 @@ WHERE user_id = $1
132132
`
133133

134134
const insertScheduleQuery = `
135-
INSERT INTO user_schedule(user_id, section_id)
136-
SELECT $1, id FROM course_section
135+
INSERT INTO user_schedule(user_id, section_id, location)
136+
SELECT $1, id, $4 FROM course_section
137137
WHERE class_number = $2 AND term_id = $3
138138
`
139139

@@ -147,7 +147,7 @@ func saveSchedule(tx *db.Tx, summary *schedule.Summary, userId int) (*scheduleRe
147147
}
148148

149149
// Refuse to import empty schedule: we probably failed to parse it
150-
if len(summary.ClassNumbers) == 0 {
150+
if len(summary.Classes) == 0 {
151151
return nil, serde.WithStatus(
152152
http.StatusBadRequest,
153153
serde.WithEnum(serde.EmptySchedule, fmt.Errorf("empty schedule")),
@@ -165,8 +165,8 @@ func saveSchedule(tx *db.Tx, summary *schedule.Summary, userId int) (*scheduleRe
165165
}
166166

167167
var failedClasses []int
168-
for _, classNumber := range summary.ClassNumbers {
169-
tag, err := tx.Exec(insertScheduleQuery, userId, classNumber, summary.TermId)
168+
for _, class := range summary.Classes {
169+
tag, err := tx.Exec(insertScheduleQuery, userId, class.Number, summary.TermId, class.Location)
170170
if err != nil {
171171
return nil, fmt.Errorf("writing user_schedule: %w", err)
172172
}
@@ -176,17 +176,17 @@ func saveSchedule(tx *db.Tx, summary *schedule.Summary, userId int) (*scheduleRe
176176
// Most likely UW API did not provide us with all of the available classes,
177177
// or we misparsed the class.
178178
if tag.RowsAffected() == 0 {
179-
failedClasses = append(failedClasses, classNumber)
180-
log.Printf("Schedule import failed for class number %d", classNumber)
179+
failedClasses = append(failedClasses, class.Number)
180+
log.Printf("Schedule import failed for class number %d", class.Number)
181181
}
182182

183-
_, err = tx.Exec(insertCourseTakenQuery, userId, summary.TermId, classNumber)
183+
_, err = tx.Exec(insertCourseTakenQuery, userId, summary.TermId, class.Number)
184184
if err != nil {
185185
return nil, fmt.Errorf("writing user_course_taken: %w", err)
186186
}
187187
}
188188

189-
return &scheduleResponse{SectionsImported: len(summary.ClassNumbers), FailedClasses: failedClasses}, nil
189+
return &scheduleResponse{SectionsImported: len(summary.Classes), FailedClasses: failedClasses}, nil
190190
}
191191

192192
type scheduleRequest struct {

flow/api/parse/schedule/schedule.go

Lines changed: 78 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,21 @@ import (
44
"fmt"
55
"regexp"
66
"strconv"
7+
"strings"
78

89
"flow/common/util"
910
)
1011

12+
type Class struct {
13+
Number int
14+
Location string
15+
}
16+
1117
type Summary struct {
1218
// Term ids are numbers of the form 1189 (Fall 2018)
1319
TermId int
14-
// Class numbers are four digits (e.g. 4895)
15-
// and uniquely identify a section of a course within a term.
16-
ClassNumbers []int
17-
// Classrooms identify the location of the class (e.g. DWE 3422, ONLN - Online)
18-
Classrooms []string
20+
// Classes contains the parsed sections and their locations
21+
Classes []Class
1922
}
2023

2124
var (
@@ -33,6 +36,11 @@ var (
3336
classroomRegexp = regexp.MustCompile(`(?m)^([A-Z0-9]*[A-Z][A-Z0-9]*\s+\d+|TBA|ONLN - Online)$`)
3437
)
3538

39+
type match struct {
40+
pos int
41+
val string
42+
}
43+
3644
func extractTerm(text string) (int, error) {
3745
submatches := termRegexp.FindStringSubmatchIndex(text)
3846
if submatches == nil {
@@ -48,29 +56,28 @@ func extractTerm(text string) (int, error) {
4856
}
4957
}
5058

51-
func extractClassNumbers(text string) ([]int, error) {
52-
var err error
53-
// -1 corresponds to no limit on the number of matches
59+
func extractClassNumbers(text string) ([]match, error) {
5460
submatches := classNumberRegexp.FindAllStringSubmatchIndex(text, -1)
55-
classNumbers := make([]int, len(submatches))
61+
matches := make([]match, len(submatches))
5662
for i, submatch := range submatches {
57-
matchText := text[submatch[2]:submatch[3]]
58-
classNumbers[i], err = strconv.Atoi(matchText)
59-
if err != nil {
60-
return nil, fmt.Errorf("%s is not a class number: %w", matchText, err)
63+
matches[i] = match{
64+
pos: submatch[0],
65+
val: text[submatch[2]:submatch[3]],
6166
}
6267
}
63-
return classNumbers, nil
68+
return matches, nil
6469
}
6570

66-
func extractClassrooms(text string) ([]string, error) {
71+
func extractClassrooms(text string) ([]match, error) {
6772
submatches := classroomRegexp.FindAllStringSubmatchIndex(text, -1)
68-
classrooms := make([]string, len(submatches))
73+
matches := make([]match, len(submatches))
6974
for i, submatch := range submatches {
70-
matchText := text[submatch[2]:submatch[3]]
71-
classrooms[i] = matchText
75+
matches[i] = match{
76+
pos: submatch[0],
77+
val: text[submatch[2]:submatch[3]],
78+
}
7279
}
73-
return classrooms, nil
80+
return matches, nil
7481
}
7582

7683
func Parse(text string) (*Summary, error) {
@@ -86,10 +93,57 @@ func Parse(text string) (*Summary, error) {
8693
if err != nil {
8794
return nil, fmt.Errorf("extracting classrooms: %w", err)
8895
}
89-
summary := &Summary{
90-
TermId: term,
91-
ClassNumbers: classNumbers,
92-
Classrooms: classrooms,
96+
97+
var classes []Class
98+
roomIdx := 0
99+
100+
for i, cnMatch := range classNumbers {
101+
cn, err := strconv.Atoi(cnMatch.val)
102+
if err != nil {
103+
return nil, fmt.Errorf("%s is not a class number: %w", cnMatch.val, err)
104+
}
105+
106+
// Determine the end position for this class's context.
107+
// It ends where the NEXT class number begins.
108+
// If this is the last class, the context goes to the end of the text.
109+
nextPos := len(text)
110+
if i+1 < len(classNumbers) {
111+
nextPos = classNumbers[i+1].pos
112+
}
113+
114+
// Collect all classrooms that fall within (cnMatch.pos, nextPos)
115+
var locs []string
116+
for roomIdx < len(classrooms) {
117+
room := classrooms[roomIdx]
118+
if room.pos > nextPos {
119+
// This room belongs to a future class
120+
break
121+
}
122+
if room.pos > cnMatch.pos {
123+
// Only add if it appears *after* the current class number start
124+
locs = append(locs, room.val)
125+
}
126+
roomIdx++
127+
}
128+
129+
// Dedup locations
130+
seen := make(map[string]bool)
131+
var uniqueLocs []string
132+
for _, l := range locs {
133+
if !seen[l] {
134+
seen[l] = true
135+
uniqueLocs = append(uniqueLocs, l)
136+
}
137+
}
138+
139+
classes = append(classes, Class{
140+
Number: cn,
141+
Location: strings.Join(uniqueLocs, ", "),
142+
})
93143
}
94-
return summary, nil
144+
145+
return &Summary{
146+
TermId: term,
147+
Classes: classes,
148+
}, nil
95149
}

flow/api/parse/schedule/schedule_test.go

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ func TestParseSchedule(t *testing.T) {
1818
"normal",
1919
&Summary{
2020
TermId: 1199,
21-
ClassNumbers: []int{
22-
4896, 4897, 4899, 4741, 4742, 5003, 4747, 4748, 7993, 7994, 7995, 4751, 4752,
23-
},
24-
Classrooms: []string{
25-
"MC 2038", "MC 4064", "DWE 2527", "E3 2119", "CPH 3681", "CPH 3681", "CPH 3681", "CPH 3681", "CPH 3681", "MC 2034", "CPH 3681", "CPH 1346", "CPH 3681", "CPH 3681",
21+
Classes: []Class{
22+
{4896, "MC 2038"}, {4897, "MC 4064, DWE 2527"}, {4899, "E3 2119"},
23+
{4741, "CPH 3681"}, {4742, "CPH 3681"}, {5003, "CPH 3681"},
24+
{4747, "CPH 3681"}, {4748, "CPH 3681"}, {7993, "MC 2034"},
25+
{7994, "CPH 3681"}, {7995, "CPH 1346"}, {4751, "CPH 3681"},
26+
{4752, "CPH 3681"},
2627
},
2728
},
2829
},
@@ -31,11 +32,10 @@ func TestParseSchedule(t *testing.T) {
3132
"noparen",
3233
&Summary{
3334
TermId: 1199,
34-
ClassNumbers: []int{
35-
5211, 8052, 9289, 6394, 5867, 6321, 6205, 7253, 7254,
36-
},
37-
Classrooms: []string{
38-
"E7 2317", "RCH 101", "MC 2034", "TBA", "MC 2017", "TBA", "AL 124", "DC 1351", "DC 1351",
35+
Classes: []Class{
36+
{5211, "E7 2317"}, {8052, "RCH 101"}, {9289, "MC 2034"},
37+
{6394, "TBA"}, {5867, "MC 2017"}, {6321, "TBA"},
38+
{6205, "AL 124"}, {7253, "DC 1351"}, {7254, "DC 1351"},
3939
},
4040
},
4141
},
@@ -44,11 +44,11 @@ func TestParseSchedule(t *testing.T) {
4444
"old",
4545
&Summary{
4646
TermId: 1135,
47-
ClassNumbers: []int{
48-
3370, 3077, 3078, 3166, 2446, 4106, 4107, 4108, 4111, 4117, 4118, 4110,
49-
},
50-
Classrooms: []string{
51-
"MC 4040", "QNC 1502", "QNC 1502", "TBA", "STP 105", "RCH 307", "MC 2038", "MC 2038", "TBA", "TBA", "MC 2038", "TBA", "TBA", "TBA",
47+
Classes: []Class{
48+
{3370, "MC 4040"}, {3077, "QNC 1502"}, {3078, "QNC 1502"},
49+
{3166, "TBA"}, {2446, "STP 105"}, {4106, "RCH 307"},
50+
{4107, "MC 2038"}, {4108, "MC 2038"}, {4111, "TBA"},
51+
{4117, "MC 2038"}, {4118, "TBA"}, {4110, "TBA"},
5252
},
5353
},
5454
},
@@ -57,11 +57,12 @@ func TestParseSchedule(t *testing.T) {
5757
"whitespace",
5858
&Summary{
5959
TermId: 1199,
60-
ClassNumbers: []int{
61-
4669, 4658, 4660, 4699, 4655, 4656, 4661, 4662, 4850, 4664, 4666, 4936, 4639, 4668, 7634,
62-
},
63-
Classrooms: []string{
64-
"E5 3102", "E5 3102", "E5 3101", "E5 3101", "E5 3101", "E5 3101", "DWE 3518", "CPH 1346", "E5 3102", "E5 3101", "E5 3101", "MC 4063", "E5 3101", "E5 3102", "E5 3101", "E5 3101", "E3 3164", "E5 3101", "E5 3102", "MC 4060", "E2 2363", "E2 2363", "E2 2363", "E2 2363", "E2 2363", "E5 3101", "E5 3101", "E5 3101", "EV3 4412", "TBA",
60+
Classes: []Class{
61+
{4669, "E5 3102, E5 3101"}, {4658, "E5 3101"}, {4660, "DWE 3518"},
62+
{4699, "CPH 1346"}, {4655, "E5 3102, E5 3101"}, {4656, "MC 4063"},
63+
{4661, "E5 3101, E5 3102"}, {4662, "E5 3101"}, {4850, "E3 3164"},
64+
{4664, "E5 3101, E5 3102"}, {4666, "MC 4060"}, {4936, "E2 2363"},
65+
{4639, "E5 3101"}, {4668, "EV3 4412"}, {7634, "TBA"},
6566
},
6667
},
6768
},
@@ -70,11 +71,10 @@ func TestParseSchedule(t *testing.T) {
7071
"long-classnumber",
7172
&Summary{
7273
TermId: 1219,
73-
ClassNumbers: []int{
74-
4262, 11810, 9336, 6336, 6367, 10692, 10310, 8204, 10376,
75-
},
76-
Classrooms: []string{
77-
"ONLN - Online", "ONLN - Online", "ONLN - Online", "ONLN - Online", "ONLN - Online", "ONLN - Online", "ONLN - Online", "ONLN - Online", "ONLN - Online",
74+
Classes: []Class{
75+
{4262, "ONLN - Online"}, {11810, "ONLN - Online"}, {9336, "ONLN - Online"},
76+
{6336, "ONLN - Online"}, {6367, "ONLN - Online"}, {10692, "ONLN - Online"},
77+
{10310, "ONLN - Online"}, {8204, "ONLN - Online"}, {10376, "ONLN - Online"},
7878
},
7979
},
8080
},
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE user_schedule DROP COLUMN location;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE user_schedule ADD COLUMN location TEXT;

0 commit comments

Comments
 (0)