Skip to content
This repository was archived by the owner on Feb 10, 2025. It is now read-only.

Commit e74b83b

Browse files
committed
fix: start time based on interval check
1 parent 7a52f76 commit e74b83b

5 files changed

Lines changed: 177 additions & 95 deletions

File tree

apps/ui/components/reservation-unit/utils.test.ts

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ function constructDate(d: Date, hours: number, minutes: number) {
126126
describe("getNextAvailableTime", () => {
127127
beforeAll(() => {
128128
jest.useFakeTimers({
129-
doNotFake: ['performance'],
129+
doNotFake: ["performance"],
130130
// There is some weird time zone issues (this seems to work)
131131
now: new Date(2024, 0, 1, 9, 0, 0),
132132
});
@@ -153,14 +153,21 @@ describe("getNextAvailableTime", () => {
153153
]);
154154
});
155155

156-
function mockOpenTimes(start: Date, days: number, data?: Array<{ start: Date; end: Date }>) {
156+
function mockOpenTimes(
157+
start: Date,
158+
days: number,
159+
data?: Array<{ start: Date; end: Date }>
160+
) {
157161
for (let i = 0; i < days; i++) {
158-
reservableTimes.set(dateToKey(addDays(start, i)), data ?? [
159-
{
160-
start: constructDate(addDays(start, i), 10, 0),
161-
end: constructDate(addDays(start, i), 15, 0),
162-
},
163-
]);
162+
reservableTimes.set(
163+
dateToKey(addDays(start, i)),
164+
data ?? [
165+
{
166+
start: constructDate(addDays(start, i), 10, 0),
167+
end: constructDate(addDays(start, i), 15, 0),
168+
},
169+
]
170+
);
164171
}
165172
}
166173

@@ -169,7 +176,7 @@ describe("getNextAvailableTime", () => {
169176
duration,
170177
reservationsMinDaysBefore,
171178
reservationsMaxDaysBefore,
172-
activeApplicationRounds
179+
activeApplicationRounds,
173180
}: {
174181
start: Date;
175182
duration: number;
@@ -296,7 +303,7 @@ describe("getNextAvailableTime", () => {
296303
describe("reservationsMinDaysBefore check", () => {
297304
test("finds the next available time a week from now", () => {
298305
const today = new Date();
299-
mockOpenTimes(today, 2*7);
306+
mockOpenTimes(today, 2 * 7);
300307
const input = createInput({
301308
start: today,
302309
duration: 60,
@@ -340,10 +347,12 @@ describe("getNextAvailableTime", () => {
340347
const today = new Date();
341348
mockOpenTimes(today, 30);
342349
const end = addDays(today, 7);
343-
const activeApplicationRounds: RoundPeriod[] = [{
344-
reservationPeriodBegin: addDays(today, -7).toISOString(),
345-
reservationPeriodEnd: end.toISOString(),
346-
}];
350+
const activeApplicationRounds: RoundPeriod[] = [
351+
{
352+
reservationPeriodBegin: addDays(today, -7).toISOString(),
353+
reservationPeriodEnd: end.toISOString(),
354+
},
355+
];
347356
const input = createInput({
348357
start: today,
349358
duration: 60,
@@ -353,7 +362,7 @@ describe("getNextAvailableTime", () => {
353362
expect(val).toBeInstanceOf(Date);
354363
expect(val!.getDate()).toBe(addDays(end, 1).getDate());
355364
expect(val!.getHours()).toBe(10);
356-
})
365+
});
357366

358367
test("multiple overlapping activeApplicationRounds", () => {
359368
const today = new Date();
@@ -378,7 +387,7 @@ describe("getNextAvailableTime", () => {
378387
expect(val).toBeInstanceOf(Date);
379388
expect(val!.getDate()).toBe(addDays(end, 3).getDate());
380389
expect(val!.getHours()).toBe(10);
381-
})
390+
});
382391

383392
test("finds a time between non-overlapping activeApplicationRounds", () => {
384393
const today = new Date();
@@ -409,27 +418,31 @@ describe("getNextAvailableTime", () => {
409418
const today = new Date();
410419
mockOpenTimes(today, 30);
411420
const end = addDays(today, 31);
412-
const activeApplicationRounds: RoundPeriod[] = [{
413-
reservationPeriodBegin: addDays(today, -7).toISOString(),
414-
reservationPeriodEnd: end.toISOString(),
415-
}];
421+
const activeApplicationRounds: RoundPeriod[] = [
422+
{
423+
reservationPeriodBegin: addDays(today, -7).toISOString(),
424+
reservationPeriodEnd: end.toISOString(),
425+
},
426+
];
416427
const input = createInput({
417428
start: addDays(end, 1),
418429
duration: 60,
419430
activeApplicationRounds,
420431
});
421432
const val = getNextAvailableTime(input);
422433
expect(val).toBeNull();
423-
})
434+
});
424435

425436
// TODO add more tests for application round
426437
// block 12 months using activeApplicationRounds, measure the time it takes
427438
test("performance: finds the next available time after a long application round", () => {
428439
mockOpenTimes(new Date(), 2 * 365);
429-
const activeApplicationRounds: RoundPeriod[] = [{
430-
reservationPeriodBegin: new Date().toISOString(),
431-
reservationPeriodEnd: addDays(new Date(), 365).toISOString(),
432-
}];
440+
const activeApplicationRounds: RoundPeriod[] = [
441+
{
442+
reservationPeriodBegin: new Date().toISOString(),
443+
reservationPeriodEnd: addDays(new Date(), 365).toISOString(),
444+
},
445+
];
433446
const today = new Date();
434447
const input = createInput({
435448
start: today,
@@ -442,7 +455,7 @@ describe("getNextAvailableTime", () => {
442455
const perfTime = perfEnd - perfStart;
443456
expect(val).toBeInstanceOf(Date);
444457
expect(perfTime).toBeLessThan(100);
445-
})
458+
});
446459
});
447460
});
448461
/* eslint-enable @typescript-eslint/no-non-null-assertion */

apps/ui/components/reservation-unit/utils.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { addDays, addMinutes, isAfter, isBefore, set, startOfDay } from "date-fns";
1+
import {
2+
addDays,
3+
addMinutes,
4+
isAfter,
5+
isBefore,
6+
set,
7+
startOfDay,
8+
} from "date-fns";
29
import type {
310
ReservationUnitNode,
411
ReservationUnitPageQuery,
@@ -119,7 +126,9 @@ export function getNextAvailableTime(props: AvailableTimesProps): Date | null {
119126
const endDay = possibleEndDay ? addDays(possibleEndDay, 1) : undefined;
120127
// NOTE there is still a case where application rounds have a hole but there are no reservable times
121128
// this is not a real use case but technically possible
122-
const openAfterRound: Date | undefined = props.activeApplicationRounds.reduce<Date | undefined>((acc, round) => {
129+
const openAfterRound: Date | undefined = props.activeApplicationRounds.reduce<
130+
Date | undefined
131+
>((acc, round) => {
123132
if (round.reservationPeriodEnd == null) {
124133
return acc;
125134
}

apps/ui/modules/__tests__/reservable.test.ts

Lines changed: 85 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
generateReservableMap,
1313
isRangeReservable,
1414
getDayIntervals,
15+
isStartTimeValid,
1516
} from "../reservable";
1617
import {
1718
type IsReservableFieldsFragment,
@@ -237,7 +238,67 @@ describe("generateReservableMap", () => {
237238
});
238239
});
239240

241+
describe("isStartTimeValid", () => {
242+
function mockReservableTimes(): ReservableMap {
243+
const map: ReservableMap = new Map();
244+
for (let i = 0; i < 30; i++) {
245+
const date = addDays(startOfToday(), i);
246+
const key = format(date, "yyyy-MM-dd");
247+
// TODO need to have holes in this
248+
const value = [{ start: startOfDay(date), end: endOfDay(date) }];
249+
map.set(key, value);
250+
}
251+
return map;
252+
}
253+
// TODO fuzzy this
254+
test("YES for 15 min intervals", () => {
255+
const date = startOfDay(addDays(new Date(), 1));
256+
const start = addHours(date, 9);
257+
const interval = ReservationStartInterval.Interval_15Mins;
258+
const reservableTimes = mockReservableTimes();
259+
expect(isStartTimeValid(start, reservableTimes, interval)).toBe(true);
260+
});
261+
// TODO fuzzy
262+
test("YES for 30 min intervals", () => {
263+
const date = startOfDay(addDays(new Date(), 1));
264+
const start = addHours(date, 9);
265+
const interval = ReservationStartInterval.Interval_30Mins;
266+
const reservableTimes = mockReservableTimes();
267+
expect(isStartTimeValid(start, reservableTimes, interval)).toBe(true);
268+
});
269+
test("YES for 60 min intervals", () => {
270+
const date = startOfDay(addDays(new Date(), 1));
271+
const start = addHours(date, 9);
272+
const interval = ReservationStartInterval.Interval_60Mins;
273+
const reservableTimes = mockReservableTimes();
274+
expect(isStartTimeValid(start, reservableTimes, interval)).toBe(true);
275+
});
276+
test("NO for 60 min intervals", () => {
277+
const date = startOfDay(addDays(new Date(), 1));
278+
const start = addHours(date, 9.5);
279+
const interval = ReservationStartInterval.Interval_60Mins;
280+
const reservableTimes = mockReservableTimes();
281+
expect(isStartTimeValid(start, reservableTimes, interval)).toBe(false);
282+
});
283+
test("YES for 120 min intervals", () => {
284+
const date = startOfDay(addDays(new Date(), 1));
285+
const start = addHours(date, 10);
286+
const interval = ReservationStartInterval.Interval_120Mins;
287+
const reservableTimes = mockReservableTimes();
288+
expect(isStartTimeValid(start, reservableTimes, interval)).toBe(true);
289+
});
290+
test("NO for 120 min intervals", () => {
291+
const date = startOfDay(addDays(new Date(), 1));
292+
const start = addHours(date, 9);
293+
const interval = ReservationStartInterval.Interval_120Mins;
294+
const reservableTimes = mockReservableTimes();
295+
expect(isStartTimeValid(start, reservableTimes, interval)).toBe(false);
296+
});
297+
});
298+
240299
describe("isRangeReservable", () => {
300+
// TODO mock time
301+
241302
// one month of reservable times
242303
function mockReservableTimes(): ReservableMap {
243304
const map: ReservableMap = new Map();
@@ -282,25 +343,29 @@ describe("isRangeReservable", () => {
282343
}
283344

284345
test("YES for the base case", () => {
346+
const start = addHours(startOfDay(addDays(new Date(), 1)), 10);
285347
const input = createInput({
286-
start: addHours(new Date(), 1),
287-
end: addHours(new Date(), 2),
348+
start,
349+
end: addHours(start, 2),
288350
});
289351
expect(isRangeReservable(input)).toBe(true);
290352
});
291353

292354
test("NO for starting in the past", () => {
355+
const now = new Date();
356+
const start = addHours(startOfDay(now), now.getHours() - 1);
293357
const input = createInput({
294-
start: addHours(new Date(), -1),
295-
end: addHours(new Date(), 2),
358+
start,
359+
end: addHours(start, 2),
296360
});
297361
expect(isRangeReservable(input)).toBe(false);
298362
});
299363

300364
test("NO if end < start", () => {
365+
const start = addHours(startOfDay(Date.now()), 10);
301366
const input = createInput({
302-
start: addHours(new Date(), 2),
303-
end: addHours(new Date(), 1),
367+
start,
368+
end: addHours(start, -1),
304369
});
305370
expect(isRangeReservable(input)).toBe(false);
306371
});
@@ -327,18 +392,28 @@ describe("isRangeReservable", () => {
327392
test("YES if the range is exactly an interval", () => {
328393
const date = startOfDay(addDays(new Date(), 1));
329394
const input = createInput({
330-
start: addHours(date, 9),
331-
end: addHours(date, 11),
395+
start: addHours(date, 10),
396+
end: addHours(date, 12),
332397
interval: ReservationStartInterval.Interval_120Mins,
333398
});
334399
expect(isRangeReservable(input)).toBe(true);
335400
});
336401

402+
test("NO if the range start time doesn't match interval", () => {
403+
const date = startOfDay(addDays(new Date(), 1));
404+
const input = createInput({
405+
start: addHours(date, 11),
406+
end: addHours(date, 13),
407+
interval: ReservationStartInterval.Interval_120Mins,
408+
});
409+
expect(isRangeReservable(input)).toBe(false);
410+
});
411+
337412
test("NO if the range is not divisible with interval", () => {
338413
const date = startOfDay(addDays(new Date(), 1));
339414
const input = createInput({
340-
start: addHours(date, 9),
341-
end: addHours(date, 12),
415+
start: addHours(date, 10),
416+
end: addHours(date, 13),
342417
interval: ReservationStartInterval.Interval_120Mins,
343418
});
344419
expect(isRangeReservable(input)).toBe(false);

apps/ui/modules/__tests__/reservationUnit.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ jest.mock("next-i18next", () => ({
5656
describe("getPossibleTimesForDay", () => {
5757
beforeAll(() => {
5858
jest.useFakeTimers({
59+
doNotFake: ["performance"],
5960
// use two numbers for hour so we don't need to pad with 0
6061
now: new Date(2024, 0, 1, 10, 0, 0),
6162
});
@@ -183,6 +184,24 @@ describe("getPossibleTimesForDay", () => {
183184
const input = createInput({ date, duration: 150, reservableTimes });
184185
expect(getPossibleTimesForDay(input)).toStrictEqual([]);
185186
});
187+
188+
// getPossibleTimesForDay does multiple calls to isRangeReservable which is heavy
189+
// a lot of array copying / generation
190+
test("performance", () => {
191+
const date = startOfToday();
192+
const reservableTimes = mockReservableTimes();
193+
reservableTimes.set(dateToKey(date), [
194+
{ start: addHours(date, 10), end: addHours(date, 11) },
195+
{ start: addHours(date, 12), end: addHours(date, 14) },
196+
{ start: addHours(date, 16), end: addHours(date, 18) },
197+
{ start: addHours(date, 19), end: addHours(date, 20) },
198+
]);
199+
const input = createInput({ date, reservableTimes });
200+
const start = performance.now();
201+
getPossibleTimesForDay(input);
202+
const end = performance.now();
203+
expect(end - start).toBeLessThan(20);
204+
});
186205
});
187206

188207
describe("getPrice", () => {

0 commit comments

Comments
 (0)