Skip to content

Commit 93f21e4

Browse files
authored
Merge pull request #82 from coder13/codex/fix-cumulative-conflict-validation
Fix cumulative time-limit conflict validation
2 parents 9a78f06 + 4fba828 commit 93f21e4

4 files changed

Lines changed: 245 additions & 29 deletions

File tree

src/lib/wcif/validation/personAssignmentValidation.test.ts

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,222 @@ describe('validatePersonAssignmentScheduleConflicts', () => {
387387
const errors = validatePersonAssignmentScheduleConflicts(wcif);
388388
expect(errors).toHaveLength(0);
389389
});
390+
391+
it('should not detect conflicts for grouped activities in cumulative time-limit rounds', () => {
392+
const wcif: Competition = {
393+
...mockWcif,
394+
events: [
395+
{
396+
id: '444bf',
397+
rounds: [
398+
{
399+
id: '444bf-r1',
400+
timeLimit: { centiseconds: 540000, cumulativeRoundIds: ['444bf-r1', '555bf-r1'] },
401+
},
402+
],
403+
},
404+
{
405+
id: '555bf',
406+
rounds: [
407+
{
408+
id: '555bf-r1',
409+
timeLimit: { centiseconds: 540000, cumulativeRoundIds: ['444bf-r1', '555bf-r1'] },
410+
},
411+
],
412+
},
413+
] as Competition['events'],
414+
schedule: {
415+
...mockWcif.schedule,
416+
venues: [
417+
{
418+
...mockWcif.schedule.venues[0],
419+
rooms: [
420+
{
421+
...mockWcif.schedule.venues[0].rooms[0],
422+
activities: [
423+
{
424+
id: 11,
425+
name: '4BLD Round 1',
426+
activityCode: '444bf-r1',
427+
startTime: '2024-01-01T09:00:00.000Z',
428+
endTime: '2024-01-01T10:00:00.000Z',
429+
childActivities: [
430+
{
431+
id: 142,
432+
name: '4BLD Round 1, Group 1',
433+
activityCode: '444bf-r1-g1',
434+
startTime: '2024-01-01T09:00:00.000Z',
435+
endTime: '2024-01-01T10:00:00.000Z',
436+
childActivities: [],
437+
extensions: [],
438+
},
439+
],
440+
extensions: [],
441+
},
442+
{
443+
id: 12,
444+
name: '5BLD Round 1',
445+
activityCode: '555bf-r1',
446+
startTime: '2024-01-01T09:00:00.000Z',
447+
endTime: '2024-01-01T10:00:00.000Z',
448+
childActivities: [
449+
{
450+
id: 143,
451+
name: '5BLD Round 1, Group 1',
452+
activityCode: '555bf-r1-g1',
453+
startTime: '2024-01-01T09:00:00.000Z',
454+
endTime: '2024-01-01T10:00:00.000Z',
455+
childActivities: [],
456+
extensions: [],
457+
},
458+
],
459+
extensions: [],
460+
},
461+
],
462+
},
463+
],
464+
},
465+
],
466+
},
467+
persons: [
468+
createPerson({
469+
assignments: [
470+
{ activityId: 142, stationNumber: null, assignmentCode: 'competitor' },
471+
{ activityId: 143, stationNumber: null, assignmentCode: 'competitor' },
472+
],
473+
}),
474+
],
475+
};
476+
477+
const errors = validatePersonAssignmentScheduleConflicts(wcif);
478+
expect(errors).toHaveLength(0);
479+
});
480+
481+
it('should not detect conflicts for same-event rounds that share cumulative time limits', () => {
482+
const wcif: Competition = {
483+
...mockWcif,
484+
events: [
485+
{
486+
id: '333fm',
487+
rounds: [
488+
{
489+
id: '333fm-r1',
490+
timeLimit: { centiseconds: 360000, cumulativeRoundIds: ['333fm-r1', '333fm-r2'] },
491+
},
492+
{
493+
id: '333fm-r2',
494+
timeLimit: { centiseconds: 360000, cumulativeRoundIds: ['333fm-r1', '333fm-r2'] },
495+
},
496+
],
497+
},
498+
] as Competition['events'],
499+
schedule: {
500+
...mockWcif.schedule,
501+
venues: [
502+
{
503+
...mockWcif.schedule.venues[0],
504+
rooms: [
505+
{
506+
...mockWcif.schedule.venues[0].rooms[0],
507+
activities: [
508+
{
509+
id: 21,
510+
name: 'FMC Round 1',
511+
activityCode: '333fm-r1-a1',
512+
startTime: '2024-01-01T09:00:00.000Z',
513+
endTime: '2024-01-01T10:00:00.000Z',
514+
childActivities: [],
515+
extensions: [],
516+
},
517+
{
518+
id: 22,
519+
name: 'FMC Round 2',
520+
activityCode: '333fm-r2-a1',
521+
startTime: '2024-01-01T09:00:00.000Z',
522+
endTime: '2024-01-01T10:00:00.000Z',
523+
childActivities: [],
524+
extensions: [],
525+
},
526+
],
527+
},
528+
],
529+
},
530+
],
531+
},
532+
persons: [
533+
createPerson({
534+
assignments: [
535+
{ activityId: 21, stationNumber: null, assignmentCode: 'competitor' },
536+
{ activityId: 22, stationNumber: null, assignmentCode: 'competitor' },
537+
],
538+
}),
539+
],
540+
};
541+
542+
const errors = validatePersonAssignmentScheduleConflicts(wcif);
543+
expect(errors).toHaveLength(0);
544+
});
545+
546+
it('should still detect conflicts within the same cumulative time-limit round', () => {
547+
const wcif: Competition = {
548+
...mockWcif,
549+
events: [
550+
{
551+
id: '444bf',
552+
rounds: [
553+
{
554+
id: '444bf-r1',
555+
timeLimit: { centiseconds: 540000, cumulativeRoundIds: ['444bf-r1', '555bf-r1'] },
556+
},
557+
],
558+
},
559+
] as Competition['events'],
560+
schedule: {
561+
...mockWcif.schedule,
562+
venues: [
563+
{
564+
...mockWcif.schedule.venues[0],
565+
rooms: [
566+
{
567+
...mockWcif.schedule.venues[0].rooms[0],
568+
activities: [
569+
{
570+
id: 31,
571+
name: '4BLD Round 1, Group 1',
572+
activityCode: '444bf-r1-g1',
573+
startTime: '2024-01-01T09:00:00.000Z',
574+
endTime: '2024-01-01T10:00:00.000Z',
575+
childActivities: [],
576+
extensions: [],
577+
},
578+
{
579+
id: 32,
580+
name: '4BLD Round 1, Group 2',
581+
activityCode: '444bf-r1-g2',
582+
startTime: '2024-01-01T09:30:00.000Z',
583+
endTime: '2024-01-01T10:30:00.000Z',
584+
childActivities: [],
585+
extensions: [],
586+
},
587+
],
588+
},
589+
],
590+
},
591+
],
592+
},
593+
persons: [
594+
createPerson({
595+
assignments: [
596+
{ activityId: 31, stationNumber: null, assignmentCode: 'competitor' },
597+
{ activityId: 32, stationNumber: null, assignmentCode: 'staff-judge' },
598+
],
599+
}),
600+
],
601+
};
602+
603+
const errors = validatePersonAssignmentScheduleConflicts(wcif);
604+
expect(errors).toHaveLength(1);
605+
});
390606
});
391607

392608
describe('validatePersonAssignments', () => {

src/lib/wcif/validation/personAssignmentValidation.ts

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,38 @@
11
import {
2+
acceptedRegistrations,
23
activitiesOverlap,
34
findActivityById,
45
findAllActivities,
56
parseActivityCode,
67
roomByActivity,
78
} from '../../domain';
8-
import { acceptedRegistrations } from '../../domain';
99
import {
1010
MISSING_ACTIVITY_FOR_PERSON_ASSIGNMENT,
1111
PERSON_ASSIGNMENT_SCHEDULE_CONFLICT,
1212
type ConflictingAssignment,
1313
type ValidationError,
1414
} from './types';
15-
import type { Competition, Person } from '@wca/helpers';
15+
import type { Competition, Person, Round } from '@wca/helpers';
1616

1717
const pluralizeWord = (count: number, singular: string, plural?: string) =>
1818
count === 1 ? singular : plural || singular + 's';
1919

20-
const roundsShareCumulativeTimeLimit = (
20+
const roundIdForActivity = (activityCode: string) => {
21+
const { eventId, roundNumber } = parseActivityCode(activityCode);
22+
23+
if (!eventId || !roundNumber) {
24+
return null;
25+
}
26+
27+
return `${eventId}-r${roundNumber}`;
28+
};
29+
30+
const findRoundById = (wcif: Competition, roundId: string): Round | undefined =>
31+
wcif.events.flatMap((event) => event.rounds || []).find((round) => round.id === roundId);
32+
33+
const cumulativeRoundIdsFor = (round?: Round) => round?.timeLimit?.cumulativeRoundIds || [];
34+
35+
const activitiesShareCumulativeTimeLimit = (
2136
wcif: Competition,
2237
activityIdA: number,
2338
activityIdB: number
@@ -29,34 +44,17 @@ const roundsShareCumulativeTimeLimit = (
2944
return false;
3045
}
3146

32-
const roundA = parseActivityCode(activityA.activityCode);
33-
const roundB = parseActivityCode(activityB.activityCode);
34-
if (
35-
!roundA.eventId ||
36-
!roundA.roundNumber ||
37-
!roundB.eventId ||
38-
!roundB.roundNumber ||
39-
roundA.eventId === roundB.eventId
40-
) {
41-
return false;
42-
}
47+
const roundIdA = roundIdForActivity(activityA.activityCode);
48+
const roundIdB = roundIdForActivity(activityB.activityCode);
4349

44-
const eventA = wcif.events.find((event) => event.id === roundA.eventId);
45-
const eventB = wcif.events.find((event) => event.id === roundB.eventId);
46-
if (!eventA || !eventB) {
50+
if (!roundIdA || !roundIdB || roundIdA === roundIdB) {
4751
return false;
4852
}
4953

50-
const roundAData = eventA.rounds?.find((round) => round.id === `${roundA.eventId}-r${roundA.roundNumber}`);
51-
const roundBData = eventB.rounds?.find((round) => round.id === `${roundB.eventId}-r${roundB.roundNumber}`);
52-
if (!roundAData?.timeLimit?.cumulativeRoundIds || !roundBData?.timeLimit?.cumulativeRoundIds) {
53-
return false;
54-
}
54+
const cumulativeRoundIdsA = cumulativeRoundIdsFor(findRoundById(wcif, roundIdA));
55+
const cumulativeRoundIdsB = cumulativeRoundIdsFor(findRoundById(wcif, roundIdB));
5556

56-
return (
57-
roundAData.timeLimit.cumulativeRoundIds.includes(roundBData.id) &&
58-
roundBData.timeLimit.cumulativeRoundIds.includes(roundAData.id)
59-
);
57+
return cumulativeRoundIdsA.includes(roundIdB) || cumulativeRoundIdsB.includes(roundIdA);
6058
};
6159

6260
/**
@@ -117,7 +115,9 @@ const findConflictingAssignmentsForPerson = (
117115
return;
118116
}
119117

120-
if (roundsShareCumulativeTimeLimit(wcif, assignment.activityId, otherAssignment.activityId)) {
118+
if (
119+
activitiesShareCumulativeTimeLimit(wcif, assignment.activityId, otherAssignment.activityId)
120+
) {
121121
return;
122122
}
123123

src/store/actions.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ describe('store actions', () => {
238238
expect(dispatch.mock.calls[2][0]).toEqual({
239239
type: ActionType.UPDATE_WCIF_ERRORS,
240240
errors: [],
241-
replace: false,
241+
replace: true,
242242
});
243243
expect(dispatch.mock.calls[3][0]).toEqual({
244244
type: ActionType.FETCHING_WCIF,

src/store/actions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ export const fetchWCIF = (competitionId: string) => async (dispatch: Dispatch) =
136136
};
137137

138138
dispatch(updateWCIF(updatedWcif));
139-
dispatch(updateWcifErrors(validateWcif(updatedWcif)));
139+
dispatch(updateWcifErrors(validateWcif(updatedWcif), true));
140140
} catch (e) {
141141
dispatch(updateWcifErrors([e as ValidationError], true));
142142
}

0 commit comments

Comments
 (0)