Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 216 additions & 0 deletions src/lib/wcif/validation/personAssignmentValidation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,222 @@ describe('validatePersonAssignmentScheduleConflicts', () => {
const errors = validatePersonAssignmentScheduleConflicts(wcif);
expect(errors).toHaveLength(0);
});

it('should not detect conflicts for grouped activities in cumulative time-limit rounds', () => {
const wcif: Competition = {
...mockWcif,
events: [
{
id: '444bf',
rounds: [
{
id: '444bf-r1',
timeLimit: { centiseconds: 540000, cumulativeRoundIds: ['444bf-r1', '555bf-r1'] },
},
],
},
{
id: '555bf',
rounds: [
{
id: '555bf-r1',
timeLimit: { centiseconds: 540000, cumulativeRoundIds: ['444bf-r1', '555bf-r1'] },
},
],
},
] as Competition['events'],
schedule: {
...mockWcif.schedule,
venues: [
{
...mockWcif.schedule.venues[0],
rooms: [
{
...mockWcif.schedule.venues[0].rooms[0],
activities: [
{
id: 11,
name: '4BLD Round 1',
activityCode: '444bf-r1',
startTime: '2024-01-01T09:00:00.000Z',
endTime: '2024-01-01T10:00:00.000Z',
childActivities: [
{
id: 142,
name: '4BLD Round 1, Group 1',
activityCode: '444bf-r1-g1',
startTime: '2024-01-01T09:00:00.000Z',
endTime: '2024-01-01T10:00:00.000Z',
childActivities: [],
extensions: [],
},
],
extensions: [],
},
{
id: 12,
name: '5BLD Round 1',
activityCode: '555bf-r1',
startTime: '2024-01-01T09:00:00.000Z',
endTime: '2024-01-01T10:00:00.000Z',
childActivities: [
{
id: 143,
name: '5BLD Round 1, Group 1',
activityCode: '555bf-r1-g1',
startTime: '2024-01-01T09:00:00.000Z',
endTime: '2024-01-01T10:00:00.000Z',
childActivities: [],
extensions: [],
},
],
extensions: [],
},
],
},
],
},
],
},
persons: [
createPerson({
assignments: [
{ activityId: 142, stationNumber: null, assignmentCode: 'competitor' },
{ activityId: 143, stationNumber: null, assignmentCode: 'competitor' },
],
}),
],
};

const errors = validatePersonAssignmentScheduleConflicts(wcif);
expect(errors).toHaveLength(0);
});

it('should not detect conflicts for same-event rounds that share cumulative time limits', () => {
const wcif: Competition = {
...mockWcif,
events: [
{
id: '333fm',
rounds: [
{
id: '333fm-r1',
timeLimit: { centiseconds: 360000, cumulativeRoundIds: ['333fm-r1', '333fm-r2'] },
},
{
id: '333fm-r2',
timeLimit: { centiseconds: 360000, cumulativeRoundIds: ['333fm-r1', '333fm-r2'] },
},
],
},
] as Competition['events'],
schedule: {
...mockWcif.schedule,
venues: [
{
...mockWcif.schedule.venues[0],
rooms: [
{
...mockWcif.schedule.venues[0].rooms[0],
activities: [
{
id: 21,
name: 'FMC Round 1',
activityCode: '333fm-r1-a1',
startTime: '2024-01-01T09:00:00.000Z',
endTime: '2024-01-01T10:00:00.000Z',
childActivities: [],
extensions: [],
},
{
id: 22,
name: 'FMC Round 2',
activityCode: '333fm-r2-a1',
startTime: '2024-01-01T09:00:00.000Z',
endTime: '2024-01-01T10:00:00.000Z',
childActivities: [],
extensions: [],
},
],
},
],
},
],
},
persons: [
createPerson({
assignments: [
{ activityId: 21, stationNumber: null, assignmentCode: 'competitor' },
{ activityId: 22, stationNumber: null, assignmentCode: 'competitor' },
],
}),
],
};

const errors = validatePersonAssignmentScheduleConflicts(wcif);
expect(errors).toHaveLength(0);
});

it('should still detect conflicts within the same cumulative time-limit round', () => {
const wcif: Competition = {
...mockWcif,
events: [
{
id: '444bf',
rounds: [
{
id: '444bf-r1',
timeLimit: { centiseconds: 540000, cumulativeRoundIds: ['444bf-r1', '555bf-r1'] },
},
],
},
] as Competition['events'],
schedule: {
...mockWcif.schedule,
venues: [
{
...mockWcif.schedule.venues[0],
rooms: [
{
...mockWcif.schedule.venues[0].rooms[0],
activities: [
{
id: 31,
name: '4BLD Round 1, Group 1',
activityCode: '444bf-r1-g1',
startTime: '2024-01-01T09:00:00.000Z',
endTime: '2024-01-01T10:00:00.000Z',
childActivities: [],
extensions: [],
},
{
id: 32,
name: '4BLD Round 1, Group 2',
activityCode: '444bf-r1-g2',
startTime: '2024-01-01T09:30:00.000Z',
endTime: '2024-01-01T10:30:00.000Z',
childActivities: [],
extensions: [],
},
],
},
],
},
],
},
persons: [
createPerson({
assignments: [
{ activityId: 31, stationNumber: null, assignmentCode: 'competitor' },
{ activityId: 32, stationNumber: null, assignmentCode: 'staff-judge' },
],
}),
],
};

const errors = validatePersonAssignmentScheduleConflicts(wcif);
expect(errors).toHaveLength(1);
});
});

describe('validatePersonAssignments', () => {
Expand Down
54 changes: 27 additions & 27 deletions src/lib/wcif/validation/personAssignmentValidation.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,38 @@
import {
acceptedRegistrations,
activitiesOverlap,
findActivityById,
findAllActivities,
parseActivityCode,
roomByActivity,
} from '../../domain';
import { acceptedRegistrations } from '../../domain';
import {
MISSING_ACTIVITY_FOR_PERSON_ASSIGNMENT,
PERSON_ASSIGNMENT_SCHEDULE_CONFLICT,
type ConflictingAssignment,
type ValidationError,
} from './types';
import type { Competition, Person } from '@wca/helpers';
import type { Competition, Person, Round } from '@wca/helpers';

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

const roundsShareCumulativeTimeLimit = (
const roundIdForActivity = (activityCode: string) => {
const { eventId, roundNumber } = parseActivityCode(activityCode);

if (!eventId || !roundNumber) {
return null;
}

return `${eventId}-r${roundNumber}`;
};

const findRoundById = (wcif: Competition, roundId: string): Round | undefined =>
wcif.events.flatMap((event) => event.rounds || []).find((round) => round.id === roundId);

const cumulativeRoundIdsFor = (round?: Round) => round?.timeLimit?.cumulativeRoundIds || [];

const activitiesShareCumulativeTimeLimit = (
wcif: Competition,
activityIdA: number,
activityIdB: number
Expand All @@ -29,34 +44,17 @@ const roundsShareCumulativeTimeLimit = (
return false;
}

const roundA = parseActivityCode(activityA.activityCode);
const roundB = parseActivityCode(activityB.activityCode);
if (
!roundA.eventId ||
!roundA.roundNumber ||
!roundB.eventId ||
!roundB.roundNumber ||
roundA.eventId === roundB.eventId
) {
return false;
}
const roundIdA = roundIdForActivity(activityA.activityCode);
const roundIdB = roundIdForActivity(activityB.activityCode);

const eventA = wcif.events.find((event) => event.id === roundA.eventId);
const eventB = wcif.events.find((event) => event.id === roundB.eventId);
if (!eventA || !eventB) {
if (!roundIdA || !roundIdB || roundIdA === roundIdB) {
return false;
}

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

return (
roundAData.timeLimit.cumulativeRoundIds.includes(roundBData.id) &&
roundBData.timeLimit.cumulativeRoundIds.includes(roundAData.id)
);
return cumulativeRoundIdsA.includes(roundIdB) || cumulativeRoundIdsB.includes(roundIdA);
};

/**
Expand Down Expand Up @@ -117,7 +115,9 @@ const findConflictingAssignmentsForPerson = (
return;
}

if (roundsShareCumulativeTimeLimit(wcif, assignment.activityId, otherAssignment.activityId)) {
if (
activitiesShareCumulativeTimeLimit(wcif, assignment.activityId, otherAssignment.activityId)
) {
return;
}

Expand Down
2 changes: 1 addition & 1 deletion src/store/actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ describe('store actions', () => {
expect(dispatch.mock.calls[2][0]).toEqual({
type: ActionType.UPDATE_WCIF_ERRORS,
errors: [],
replace: false,
replace: true,
});
expect(dispatch.mock.calls[3][0]).toEqual({
type: ActionType.FETCHING_WCIF,
Expand Down
2 changes: 1 addition & 1 deletion src/store/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export const fetchWCIF = (competitionId: string) => async (dispatch: Dispatch) =
};

dispatch(updateWCIF(updatedWcif));
dispatch(updateWcifErrors(validateWcif(updatedWcif)));
dispatch(updateWcifErrors(validateWcif(updatedWcif), true));
} catch (e) {
dispatch(updateWcifErrors([e as ValidationError], true));
}
Expand Down
Loading