Skip to content

Commit 46332cd

Browse files
committed
fix: coalesce intervals if gap is 1 min, and do wrap-around coalescing (#190)
* fix: coalesce intervals if gap is 1 min, and do wrap-around coalescing * fix: unparseable tokens should be ignored * chore: remove extraneous comment * fix: redo the merging algo
1 parent 37f72e7 commit 46332cd

File tree

5 files changed

+255
-29
lines changed

5 files changed

+255
-29
lines changed

src/containers/timeBuilder.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,21 @@ export function getTimeRangesFromString(rowHTML: Element) {
2828
}
2929

3030
function getTimeAttributesFromRow(rowHTML: Element) {
31-
const { day, date, timeSlots } = tokenizeTimeRow(rowHTML);
32-
return getTimeInfoWithRawAttributes([day, date, ...timeSlots]);
31+
return getTimeInfoWithRawAttributes(tokenizeTimeRow(rowHTML));
3332
}
3433

3534
function tokenizeTimeRow(rowHTML: Element) {
3635
const $ = load(rowHTML);
3736
let day = $("strong").text();
3837
const dataStr = $.text().replace(/\s\s+/g, " ").replace(day, "").trim();
3938
let [date, time] = dataStr.split(/,(.+)/);
39+
if (date === undefined || time === undefined) return [];
4040

4141
day = (day.charAt(0).toUpperCase() + day.slice(1).toLowerCase()).trim();
4242
date = (date.charAt(0).toUpperCase() + date.slice(1).toLowerCase()).trim();
4343
time = time.toUpperCase().trim();
4444
const timeSlots = time.split(/[,;]/).map((slot) => slot.trim());
45-
return { day, date, timeSlots };
45+
return [day, date, ...timeSlots];
4646
}
4747

4848
function getTimeInfoWithRawAttributes(tokens: string[]) {
@@ -92,6 +92,13 @@ function resolveAttributeConflicts(
9292
closed: input.closed,
9393
};
9494
}
95+
if (input.twentyFour) {
96+
return {
97+
day: input.day,
98+
date: input.date,
99+
times: [{ start: { hour: 0, minute: 0 }, end: { hour: 23, minute: 59 } }],
100+
};
101+
}
95102
if (input.times && input.times.length > 0) {
96103
return {
97104
day: input.day,
@@ -102,18 +109,22 @@ function resolveAttributeConflicts(
102109
return {
103110
day: input.day,
104111
date: input.date,
105-
times: [{ start: { hour: 0, minute: 0 }, end: { hour: 23, minute: 59 } }],
112+
times: [],
106113
};
107114
}
108115

109116
function getTimeRangesFromTimeRow(time: ITimeRowAttributes) {
110117
if (time.day === undefined) {
111-
throw new Error("Cannot convert when day is not set");
118+
notifySlack(
119+
`<!channel> Cannot convert time attribute: ${JSON.stringify(
120+
time
121+
)} since day is not set`
122+
);
123+
return [];
112124
}
113125
const allRanges: ITimeRange[] = [];
114126
for (const range of time.times ?? []) {
115-
rollBack12AmEndTime(range);
116-
127+
rollBack12AmEndTime(range); // not sure why this was added, but it doesn't hurt I guess (I suppose the only case this actively helps is if the time string is 12:00 AM - 12:00 AM)
117128
const shouldSpillToNextDay =
118129
range.start.hour * 60 + range.start.minute >
119130
range.end.hour * 60 + range.end.minute;

src/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ export interface ISpecial {
1717
description: string;
1818
}
1919

20-
export interface ITimeMoment {
20+
export interface ITimeSlot {
2121
day: DayOfTheWeek;
2222
hour: number;
2323
minute: number;
2424
}
2525

2626
export interface ITimeRange {
27-
start: ITimeMoment;
28-
end: ITimeMoment;
27+
start: ITimeSlot;
28+
end: ITimeSlot;
2929
}
3030
export interface ICoordinate {
3131
lat: number;

src/utils/diff.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ type T = { [key: string]: T } | T[] | string | number | undefined;
44
export function getObjDiffs(
55
prevObject: T,
66
newObject: T,
7-
path: string = "~"
7+
path: string = "~" // ~uwu senpai
88
): string[] {
99
let diffs: string[] = [];
1010
if (typeof prevObject === "object" && typeof newObject === "object") {

src/utils/timeUtils.ts

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DayOfTheWeek, ITimeMoment, ITimeRange } from "types";
1+
import { DayOfTheWeek, ITimeSlot, ITimeRange } from "types";
22

33
export function getNextDay(day: DayOfTheWeek): DayOfTheWeek {
44
const weekdays: DayOfTheWeek[] = [
@@ -13,42 +13,66 @@ export function getNextDay(day: DayOfTheWeek): DayOfTheWeek {
1313
return weekdays[(weekdays.indexOf(day) + 1) % 7];
1414
}
1515

16-
export function getMinutesSinceStartOfSunday(timeMoment: ITimeMoment) {
17-
return timeMoment.day * (24 * 60) + timeMoment.hour * 60 + timeMoment.minute;
16+
export function getMinutesSinceStartOfSunday(timeSlot: ITimeSlot) {
17+
return timeSlot.day * (24 * 60) + timeSlot.hour * 60 + timeSlot.minute;
1818
}
1919
/**
2020
*
21-
* @param moment1
22-
* @param moment2
21+
* @param timeSlot1
22+
* @param timeSlot2
2323
* @returns Delta in minutes of moment1 - moment2
2424
*/
25-
export function compareTimeMoments(moment1: ITimeMoment, moment2: ITimeMoment) {
25+
export function compareTimeSlots(timeSlot1: ITimeSlot, timeSlot2: ITimeSlot) {
2626
return (
27-
getMinutesSinceStartOfSunday(moment1) -
28-
getMinutesSinceStartOfSunday(moment2)
27+
getMinutesSinceStartOfSunday(timeSlot1) -
28+
getMinutesSinceStartOfSunday(timeSlot2)
2929
);
3030
}
3131

3232
export function sortAndMergeTimeRanges(timeRanges: ITimeRange[]) {
33-
timeRanges.sort((range1, range2) =>
34-
compareTimeMoments(range1.start, range2.start)
35-
);
33+
const MINUTES_IN_A_WEEK = 60 * 24 * 7;
34+
const unwrappedTimeRanges = timeRanges
35+
.flatMap((rng) => {
36+
if (compareTimeSlots(rng.start, rng.end) > 0) {
37+
// unwrap the wrapped interval
38+
return [
39+
{ start: { day: 0, hour: 0, minute: 0 }, end: rng.end },
40+
{ start: rng.start, end: { day: 6, hour: 23, minute: 59 } },
41+
];
42+
} else {
43+
return [rng];
44+
}
45+
})
46+
.sort((range1, range2) => compareTimeSlots(range1.start, range2.start));
3647
const mergedRanges: ITimeRange[] = [];
3748

38-
for (const timeRange of timeRanges) {
49+
for (const timeRange of unwrappedTimeRanges) {
3950
const lastTimeRange = mergedRanges.length
4051
? mergedRanges[mergedRanges.length - 1]
4152
: undefined;
4253
if (
4354
lastTimeRange &&
44-
compareTimeMoments(lastTimeRange.end, timeRange.start) >= 0
55+
compareTimeSlots(lastTimeRange.end, timeRange.start) >= -1 // we overlap 1-minute disjoint intervals as well (ex. 2:00 PM - 2:59 PM will get merged with 3:00PM - 4:00PM as 2:00PM - 4:00PM)
4556
) {
46-
if (compareTimeMoments(timeRange.end, lastTimeRange.end) > 0) {
57+
if (compareTimeSlots(timeRange.end, lastTimeRange.end) > 0) {
4758
lastTimeRange.end = timeRange.end; // join current range with last range
4859
}
4960
} else {
5061
mergedRanges.push(timeRange);
5162
}
5263
}
64+
// merge the last day with the first day if needed
65+
if (mergedRanges.length >= 2) {
66+
const lastRange = mergedRanges[mergedRanges.length - 1];
67+
const firstRange = mergedRanges[0];
68+
if (
69+
getMinutesSinceStartOfSunday(lastRange.end) === MINUTES_IN_A_WEEK - 1 &&
70+
getMinutesSinceStartOfSunday(firstRange.start) === 0
71+
) {
72+
lastRange.end = firstRange.end;
73+
mergedRanges.shift();
74+
}
75+
}
76+
5377
return mergedRanges;
5478
}

tests/integration.test.ts

Lines changed: 195 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,160 @@ describe("time edge cases", () => {
8484
[Thur]: "24 hRs",
8585
[Fri]: "24 hours",
8686
});
87+
await queryParserAndAssertTimingsCorrect([[Tue, 0, 0, Fri, 23, 59]]);
88+
});
89+
test("all day every day", async () => {
90+
setUpTimingTest({
91+
[Mon]: "OPEN 24 HOURS",
92+
[Tue]: "OPEN 24 HOURS",
93+
[Wed]: "OPEN 24 HRS",
94+
[Thur]: "24 hRs",
95+
[Fri]: "24 hours",
96+
[Sat]: "24 hours",
97+
[Sun]: "24 hours",
98+
});
99+
await queryParserAndAssertTimingsCorrect([[Sun, 0, 0, Sat, 23, 59]]); // this is the reason why we leave the other return type that represents open every day as this. (backwards compatibility, mostly)
100+
});
101+
test("all day every day but slightly different", async () => {
102+
setUpTimingTest({
103+
[Mon]: "OPEN 24 HOURS",
104+
[Tue]: "OPEN 24 HOURS",
105+
[Wed]: "OPEN 24 HRS",
106+
[Thur]: "12:00 AM - 11:59 PM",
107+
[Fri]: "24 hours",
108+
[Sat]: "12:00 AM - 2:59 AM, 3:00 AM - 11:59 PM",
109+
[Sun]: "24 hours",
110+
});
111+
await queryParserAndAssertTimingsCorrect([[Sun, 0, 0, Sat, 23, 59]]);
112+
});
113+
test("empty string", async () => {
114+
setUpTimingTest({
115+
[Mon]: "OPEN 24 HOURS",
116+
[Tue]: "",
117+
[Wed]: "",
118+
[Thur]: "",
119+
[Fri]: "",
120+
[Sat]: "",
121+
[Sun]: "24 hours",
122+
});
123+
await queryParserAndAssertTimingsCorrect([[Sun, 0, 0, Mon, 23, 59]]);
124+
});
125+
test("loop-back time coalescing (wrapping on saturday, but it overlaps with sunday)", async () => {
126+
setUpTimingTest({
127+
[Mon]: "",
128+
[Tue]: "",
129+
[Wed]: "",
130+
[Thur]: "",
131+
[Fri]: "",
132+
[Sat]: "7:00 AM - 2:00 AM",
133+
[Sun]: "1:00 AM - 5:00 PM", // sunday is represented as 0
134+
});
135+
await queryParserAndAssertTimingsCorrect([[Sat, 7, 0, Sun, 17, 0]]);
136+
});
137+
test("loop-back time coalescing (wrapping on saturday, but it overlaps with multiple ranges on sunday)", async () => {
138+
setUpTimingTest({
139+
[Mon]: "",
140+
[Tue]: "",
141+
[Wed]: "",
142+
[Thur]: "",
143+
[Fri]: "",
144+
[Sat]: "7:00 AM - 2:00 AM",
145+
[Sun]: "12:00AM - 12:35 AM, 1:00 AM - 5:00 PM", // sunday is represented as 0
146+
});
147+
await queryParserAndAssertTimingsCorrect([[Sat, 7, 0, Sun, 17, 0]]);
148+
});
149+
test("open all week, gone wrong", async () => {
150+
setUpTimingTest({
151+
[Sun]: "OPEN 24 HOURS",
152+
[Mon]: "OPEN 24 HOURS",
153+
[Tue]: "OPEN 24 HOURS",
154+
[Wed]: "OPEN 24 HOURS",
155+
[Thur]: "OPEN 24 HOURS",
156+
[Fri]: "OPEN 24 HOURS",
157+
[Sat]: "12:00 AM - 10:00 AM, 9:00 AM - 2:00 AM",
158+
});
159+
await queryParserAndAssertTimingsCorrect([[Sun, 0, 0, Sat, 23, 59]]); // this should be the default return value if it's open all week
160+
});
161+
test("open all week, gone wrong", async () => {
162+
setUpTimingTest({
163+
[Sun]: "12:00 AM - 12:05 AM, 12:10 AM - 11:59 PM",
164+
[Mon]: "OPEN 24 HOURS",
165+
[Tue]: "OPEN 24 HOURS",
166+
[Wed]: "OPEN 24 HOURS",
167+
[Thur]: "OPEN 24 HOURS",
168+
[Fri]: "OPEN 24 HOURS",
169+
[Sat]: "12:00 AM - 10:00 AM, 9:00 AM - 2:00 AM",
170+
});
171+
await queryParserAndAssertTimingsCorrect([[Sun, 0, 0, Sat, 23, 59]]); // this should be the default return value if it's open all week
172+
});
173+
test("wrapping on thursday, but it overlaps with friday", async () => {
174+
setUpTimingTest({
175+
[Mon]: "",
176+
[Tue]: "",
177+
[Wed]: "",
178+
[Thur]: "7:00 AM - 2:00 AM",
179+
[Fri]: "1:00 AM - 5:00 PM",
180+
[Sat]: "",
181+
[Sun]: "",
182+
});
183+
await queryParserAndAssertTimingsCorrect([[Thur, 7, 0, Fri, 17, 0]]);
184+
});
185+
test("some combination of wrap-around", async () => {
186+
setUpTimingTest({
187+
[Mon]: "",
188+
[Tue]: "",
189+
[Wed]: "open 24 hours",
190+
[Thur]: "7:00 AM - 2:00 AM",
191+
[Fri]: "1:00 AM - 5:00 PM",
192+
[Sat]: "",
193+
[Sun]: "open 24 hours",
194+
});
87195
await queryParserAndAssertTimingsCorrect([
88-
[Tue, 0, 0, Tue, 23, 59],
196+
[Sun, 0, 0, Sun, 23, 59],
89197
[Wed, 0, 0, Wed, 23, 59],
90-
[Thur, 0, 0, Thur, 23, 59],
91-
[Fri, 0, 0, Fri, 23, 59],
198+
[Thur, 7, 0, Fri, 17, 0],
199+
]);
200+
});
201+
test("open nearly all week, but Dining Services has truly lost it", async () => {
202+
setUpTimingTest({
203+
[Sun]: "12:00 AM - 12:05 AM, 9:00 AM - 11:59 PM",
204+
[Mon]: "12:00 AM - 3:05 AM, 3:00 AM - 2:00 AM",
205+
[Tue]: "1:00 AM - 9:00 PM, 9:01 PM - 12:00 AM, 12:00 AM - 3:00 PM",
206+
[Wed]: "OPEN 24 HOURS",
207+
[Thur]: "OPEN 24 HOURS",
208+
[Fri]: "OPEN 24 HOURS, mooo",
209+
[Sat]: "12:00 AM - 10:00 AM, 9:00 AM - 7:05 AM",
210+
});
211+
await queryParserAndAssertTimingsCorrect([[Sun, 9, 0, Sun, 7, 5]]);
212+
}); // tests literally everything
213+
test("open nearly all week, but Dining Services has truly lost it", async () => {
214+
setUpTimingTest({
215+
[Sun]: "12:05 AM - 12:10 AM, 9:00 AM - 11:59 PM",
216+
[Mon]: "12:00 AM - 3:05 AM, 3:00 AM - 2:00 AM",
217+
[Tue]: "1:00 AM - 9:00 PM, 9:01 PM - 12:00 AM, 12:00 AM - 3:00 PM",
218+
[Wed]: "OPEN 24 HOURS",
219+
[Thur]: "OPEN 24 HOURS",
220+
[Fri]: "OPEN 24 HOURS, mooo",
221+
[Sat]: "12:00 AM - 10:00 AM, 9:00 AM - 12:02 AM",
222+
});
223+
await queryParserAndAssertTimingsCorrect([
224+
[Sun, 0, 5, Sun, 0, 10],
225+
[Sun, 9, 0, Sun, 0, 2],
226+
]);
227+
}); // tests literally everything
228+
test("degenerate open times", async () => {
229+
setUpTimingTest({
230+
[Mon]: "",
231+
[Tue]: "",
232+
[Wed]: "",
233+
[Thur]: "2:00 AM - 2:00 AM",
234+
[Fri]: "1:00 AM - 1:00 AM",
235+
[Sat]: "",
236+
[Sun]: "",
237+
});
238+
await queryParserAndAssertTimingsCorrect([
239+
[Thur, 2, 0, Thur, 2, 0],
240+
[Fri, 1, 0, Fri, 1, 0],
92241
]);
93242
});
94243
test("single time", async () => {
@@ -136,7 +285,7 @@ describe("time edge cases", () => {
136285
[Sat, 16, 0, Sat, 21, 0],
137286
]);
138287
});
139-
test("12AM", async () => {
288+
test("12AM (tests the 12:00 AM -> 11:59 PM shift)", async () => {
140289
setUpTimingTest({
141290
[Mon]: "12:00 AM - 12:00 AM",
142291
[Tue]: "2:00 AM - 12:00 AM",
@@ -182,4 +331,46 @@ describe("time edge cases", () => {
182331
[Fri, 19, 0, Fri, 23, 59],
183332
]);
184333
});
334+
test("partial all day", async () => {
335+
setUpTimingTest({
336+
[Wed]: "open 24 hours",
337+
[Thur]: "open 24 hours",
338+
[Fri]: "open 24 hours",
339+
});
340+
await queryParserAndAssertTimingsCorrect([[Wed, 0, 0, Fri, 23, 59]]);
341+
});
342+
test("partial all day, over the weekend", async () => {
343+
setUpTimingTest({
344+
[Sat]: "open 24 hours",
345+
[Sun]: "open 24 hours",
346+
[Mon]: "open 24 hours",
347+
});
348+
await queryParserAndAssertTimingsCorrect([[Sat, 0, 0, Mon, 23, 59]]);
349+
});
350+
test("partial all day, over the weekend", async () => {
351+
setUpTimingTest({
352+
[Sat]: "7:00 AM - 12:01 AM",
353+
[Sun]: "open 24 hours",
354+
[Mon]: "open 24 hours",
355+
});
356+
await queryParserAndAssertTimingsCorrect([[Sat, 7, 0, Mon, 23, 59]]);
357+
});
358+
test("another one", async () => {
359+
setUpTimingTest({
360+
[Sat]: "7:00 AM - 12:01 AM",
361+
});
362+
await queryParserAndAssertTimingsCorrect([[Sat, 7, 0, Sun, 0, 1]]);
363+
});
364+
test("unparseable token", async () => {
365+
setUpTimingTest({
366+
[Mon]: "mooooo",
367+
});
368+
await queryParserAndAssertTimingsCorrect([]);
369+
});
370+
test("24 hours should override other times", async () => {
371+
setUpTimingTest({
372+
[Mon]: "OPEN 24 HOURS, 2:00 AM - 3:00 AM",
373+
});
374+
await queryParserAndAssertTimingsCorrect([[Mon, 0, 0, Mon, 23, 59]]);
375+
});
185376
});

0 commit comments

Comments
 (0)