some renaming of variables
All checks were successful
Build, test and publish / verify (push) Successful in 5m55s

This commit is contained in:
2026-11-03 12:58:15 +01:00
parent a7f7730ea0
commit 3a5203eb47
17 changed files with 98 additions and 54 deletions

View File

@@ -154,8 +154,8 @@ dancers; deleting a school also deletes its dancers, groups and shows.
Shows have a name, zero to six dated dayparts (`MORNING`, `AFTERNOON`, `EVENING`), Shows have a name, zero to six dated dayparts (`MORNING`, `AFTERNOON`, `EVENING`),
with unique date/type combinations. The displayed date range comes from these with unique date/type combinations. The displayed date range comes from these
dayparts. Each show stores `preferredDayParts` (x), defaulting to two or one for dayparts. Each show stores `preferredDayParts`, defaulting to two or one for
one daypart. Ordinary groups, including PRE_EDUCATION, perform in exactly x one daypart. Ordinary groups, including PRE_EDUCATION, perform in exactly preferredDayParts
dayparts; preschool groups once; showcases every daypart. See dayparts; preschool groups once; showcases every daypart. See
[show-scheduling-rules.md](show-scheduling-rules.md) for the full hard constraints, [show-scheduling-rules.md](show-scheduling-rules.md) for the full hard constraints,
coach exceptions, ordering and optimization rules. coach exceptions, ordering and optimization rules.
@@ -178,10 +178,10 @@ status. The UI supports ordering and moving performances, and displays violation
the same body to link an existing coach. `DELETE .../coaches/{coachId}/dancer` the same body to link an existing coach. `DELETE .../coaches/{coachId}/dancer`
removes the identity link. The dancers screen supports maintaining these links. removes the identity link. The dancers screen supports maintaining these links.
School JSON exports use format version **3**, including x, schedule status and School JSON exports use format version **3**, including preferredDayParts, schedule status and
coach-to-dancer links. Version 2 remains importable; its schedules migrate to coach-to-dancer links. Version 2 remains importable; its schedules migrate to
drafts with default x. Version 1 is unsupported. Legacy `appearances` fields are drafts with default preferredDayParts. Version 1 is unsupported. Legacy `appearances` fields are
retained for file compatibility; S18 scheduling uses x and fixed type exceptions. retained for file compatibility; S18 scheduling uses preferredDayParts and fixed type exceptions.
The REST `appearances` input is deprecated; use `preferredDayParts`. The REST `appearances` input is deprecated; use `preferredDayParts`.
Run the HTTP integration tests with: Run the HTTP integration tests with:

View File

@@ -14,7 +14,7 @@ public final class ScheduleRules {
public static List<String> violations(final SchedulingInput input, final List<List<Long>> parts) { public static List<String> violations(final SchedulingInput input, final List<List<Long>> parts) {
final List<String> errors = new ArrayList<>(); final List<String> errors = new ArrayList<>();
if (input.dayParts() == 0 || input.preferredDayParts() < 1 || input.preferredDayParts() > input.dayParts()) { if (input.dayParts() == 0 || input.preferredDayParts() < 1 || input.preferredDayParts() > input.dayParts()) {
errors.add("Configure dayparts and x between 1 and the number of dayparts."); errors.add("Configure dayparts and preferredDayParts between 1 and the number of dayparts.");
} }
if (parts == null || parts.size() != input.dayParts() || parts.stream().anyMatch(java.util.Objects::isNull)) { if (parts == null || parts.size() != input.dayParts() || parts.stream().anyMatch(java.util.Objects::isNull)) {
errors.add("Assignments must contain one list for every daypart."); errors.add("Assignments must contain one list for every daypart.");
@@ -86,7 +86,7 @@ public final class ScheduleRules {
private static void attendance( private static void attendance(
final SchedulingInput input, final List<List<Long>> parts, final List<String> errors) { final SchedulingInput input, final List<List<Long>> parts, final List<String> errors) {
final Set<Long> exempt = new HashSet<>(input.coachIds()); final Set<Long> exempt = new HashSet<>(input.coachDancerIds());
input.groups().stream() input.groups().stream()
.filter(GroupRequirement::showcase) .filter(GroupRequirement::showcase)
.forEach((final GroupRequirement g) -> exempt.addAll(g.dancerIds())); .forEach((final GroupRequirement g) -> exempt.addAll(g.dancerIds()));
@@ -96,7 +96,8 @@ public final class ScheduleRules {
} }
attendance.forEach((final Long dancer, final Set<Integer> dayparts) -> { attendance.forEach((final Long dancer, final Set<Integer> dayparts) -> {
if (!exempt.contains(dancer) && dayparts.size() > input.preferredDayParts()) { if (!exempt.contains(dancer) && dayparts.size() > input.preferredDayParts()) {
errors.add("Dancer " + dancer + " exceeds x=" + input.preferredDayParts() + " dayparts."); errors.add(
"Dancer " + dancer + " exceeds preferredDayParts=" + input.preferredDayParts() + " dayparts.");
} }
}); });
} }

View File

@@ -3,16 +3,21 @@ package nl.agilecc.agiledancers.model.scheduling;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
/**
* Immutable solver snapshot.
*
* @param coachDancerIds dancer registration IDs linked to coaches, used for participation exemptions
*/
public record SchedulingInput( public record SchedulingInput(
long showId, long showId,
long revision, long revision,
int dayParts, int dayParts,
List<GroupRequirement> groups, List<GroupRequirement> groups,
int preferredDayParts, int preferredDayParts,
Set<Long> coachIds) { Set<Long> coachDancerIds) {
public SchedulingInput { public SchedulingInput {
groups = List.copyOf(groups); groups = List.copyOf(groups);
coachIds = Set.copyOf(coachIds); coachDancerIds = Set.copyOf(coachDancerIds);
} }
public SchedulingInput( public SchedulingInput(

View File

@@ -26,7 +26,7 @@ public final class SchedulingInputs {
} }
public static SchedulingInput of(final DanceSchool school, final Show show, final long revision) { public static SchedulingInput of(final DanceSchool school, final Show show, final long revision) {
final int x = preferred(show); final int preferredDayParts = preferred(show);
final LocalDate date = show.getDayParts().stream() final LocalDate date = show.getDayParts().stream()
.map(DayPart::getDate) .map(DayPart::getDate)
.min(LocalDate::compareTo) .min(LocalDate::compareTo)
@@ -40,7 +40,8 @@ public final class SchedulingInputs {
.filter(java.util.Objects::nonNull) .filter(java.util.Objects::nonNull)
.map(Dancer::getId) .map(Dancer::getId)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
return new SchedulingInput(show.getId(), revision, show.getDayParts().size(), groups, x, coaches); return new SchedulingInput(
show.getId(), revision, show.getDayParts().size(), groups, preferredDayParts, coaches);
} }
private static GroupRequirement requirement(final DanceGroup group, final Show show, final LocalDate date) { private static GroupRequirement requirement(final DanceGroup group, final Show show, final LocalDate date) {

View File

@@ -60,7 +60,8 @@ public class SchoolManagementService {
"Scheduling requires at least one school group and one daypart."); "Scheduling requires at least one school group and one daypart.");
} }
if (SchedulingInputs.preferred(show) > show.getDayParts().size()) { if (SchedulingInputs.preferred(show) > show.getDayParts().size()) {
throw new ManagementException(ManagementExceptionKind.INVALID_INPUT, "Correct x before scheduling."); throw new ManagementException(
ManagementExceptionKind.INVALID_INPUT, "Correct preferredDayParts before scheduling.");
} }
running.add(showId); running.add(showId);
return SchedulingInputs.of(school(schoolId), show, revisions.getOrDefault(showId, 0L)); return SchedulingInputs.of(school(schoolId), show, revisions.getOrDefault(showId, 0L));

View File

@@ -21,25 +21,26 @@ public class ShowSchedules {
this.schools = schools; this.schools = schools;
} }
/** Retains assignments by date/type; removing dayparts may retain an invalid x in the draft. */ /** Retains assignments by date/type; removing dayparts may retain an invalid preferredDayParts in the draft. */
public Show configure(final long schoolId, final Long showId, final Show settings) { public Show configure(final long schoolId, final Long showId, final Show settings) {
synchronized (schools) { synchronized (schools) {
ShowConfiguration.validateParts(settings.getDayParts()); ShowConfiguration.validateParts(settings.getDayParts());
final Show existing = showId == null ? null : schools.show(schoolId, showId); final Show existing = showId == null ? null : schools.show(schoolId, showId);
final int x = settings.getPreferredDayParts() == null final int preferredDayParts = settings.getPreferredDayParts() == null
? (existing == null ? (existing == null
? (settings.getDayParts().size() == 1 ? 1 : 2) ? (settings.getDayParts().size() == 1 ? 1 : 2)
: SchedulingInputs.preferred(existing)) : SchedulingInputs.preferred(existing))
: settings.getPreferredDayParts(); : settings.getPreferredDayParts();
validateX(x, existing, settings.getDayParts().size()); validatePreferredDayParts(
preferredDayParts, existing, settings.getDayParts().size());
final Show show = existing == null final Show show = existing == null
? schools.createShow(schoolId, settings.getName(), settings.getDayParts(), null) ? schools.createShow(schoolId, settings.getName(), settings.getDayParts(), null)
: existing; : existing;
final boolean changed = x != SchedulingInputs.preferred(show) final boolean changed = preferredDayParts != SchedulingInputs.preferred(show)
|| !keys(show.getDayParts()).equals(keys(settings.getDayParts())); || !keys(show.getDayParts()).equals(keys(settings.getDayParts()));
show.setDayParts(ShowConfiguration.retainAssignments(show, settings.getDayParts())); show.setDayParts(ShowConfiguration.retainAssignments(show, settings.getDayParts()));
show.setName(settings.getName()); show.setName(settings.getName());
show.setPreferredDayParts(x); show.setPreferredDayParts(preferredDayParts);
if (changed) { if (changed) {
schools.touch(show); schools.touch(show);
} }
@@ -53,13 +54,16 @@ public class ShowSchedules {
.toList(); .toList();
} }
private static void validateX(final int x, final Show existing, final int parts) { private static void validatePreferredDayParts(final int preferredDayParts, final Show existing, final int parts) {
if (x < 1 if (preferredDayParts < 1
|| x > 6 || preferredDayParts > 6
|| (parts > 0 && x > parts && (existing == null || x != SchedulingInputs.preferred(existing)))) { || (parts > 0
&& preferredDayParts > parts
&& (existing == null || preferredDayParts != SchedulingInputs.preferred(existing)))) {
throw new ManagementException( throw new ManagementException(
ManagementExceptionKind.INVALID_INPUT, ManagementExceptionKind.INVALID_INPUT,
"x must be between 1 and the number of dayparts. Retained x must be corrected before scheduling."); "preferredDayParts must be between 1 and the number of dayparts. Retained preferredDayParts must"
+ " be corrected before scheduling.");
} }
} }

View File

@@ -205,8 +205,9 @@ public record SchoolSnapshot(
.build()); .build());
} }
ShowConfiguration.validateParts(parts); ShowConfiguration.validateParts(parts);
final int x = show.preferredDayParts() == null ? (parts.size() == 1 ? 1 : 2) : show.preferredDayParts(); final int preferredDayParts =
check(x >= 1 && x <= 6, "x moet tussen 1 en 6 liggen"); show.preferredDayParts() == null ? (parts.size() == 1 ? 1 : 2) : show.preferredDayParts();
check(preferredDayParts >= 1 && preferredDayParts <= 6, "preferredDayParts moet tussen 1 en 6 liggen");
final Show entity = Show.builder() final Show entity = Show.builder()
.name(show.name()) .name(show.name())
.dayParts(parts) .dayParts(parts)
@@ -214,7 +215,7 @@ public record SchoolSnapshot(
show.appearances() == null show.appearances() == null
? ShowConfiguration.defaults(parts.size()) ? ShowConfiguration.defaults(parts.size())
: Map.copyOf(show.appearances())) : Map.copyOf(show.appearances()))
.preferredDayParts(x) .preferredDayParts(preferredDayParts)
.finalized(false) .finalized(false)
.build(); .build();
entity.setId((long) showIds.size()); entity.setId((long) showIds.size());

View File

@@ -24,7 +24,7 @@ public final class SchedulingObjectives {
input.groups().stream() input.groups().stream()
.filter(GroupRequirement::showcase) .filter(GroupRequirement::showcase)
.forEach((final GroupRequirement g) -> showcases.addAll(g.dancerIds())); .forEach((final GroupRequirement g) -> showcases.addAll(g.dancerIds()));
dancers.removeAll(input.coachIds()); dancers.removeAll(input.coachDancerIds());
final LinearExprBuilder penalty = LinearExpr.newBuilder(); final LinearExprBuilder penalty = LinearExpr.newBuilder();
for (final long dancer : dancers) { for (final long dancer : dancers) {
final IntVar[] parts = dancerParts(model, input, vars, dancer); final IntVar[] parts = dancerParts(model, input, vars, dancer);

View File

@@ -40,11 +40,11 @@ class S18LifecycleTest {
.toList(); .toList();
} }
private Show settings(final int n, final Integer x) { private Show settings(final int n, final Integer preferredDayParts) {
return Show.builder() return Show.builder()
.name("Show") .name("Show")
.dayParts(parts(n)) .dayParts(parts(n))
.preferredDayParts(x) .preferredDayParts(preferredDayParts)
.build(); .build();
} }

View File

@@ -242,7 +242,7 @@ test('links a coach and finalizes performances spread across multiple dates', as
data: { assignments: groupIds.map((id) => [id]) }, data: { assignments: groupIds.map((id) => [id]) },
}); });
await page.goto(`/schools/${school.id}/shows`); await page.goto(`/schools/${school.id}/shows`);
await expect(page.getByText(/exceeds x=1/)).toBeVisible(); await expect(page.getByText(/exceeds preferredDayParts=1/)).toBeVisible();
await tab(page, 'Dansers'); await tab(page, 'Dansers');
await page.getByText('Coaches koppelen aan dansers', { exact: true }).click(); await page.getByText('Coaches koppelen aan dansers', { exact: true }).click();
await page.getByLabel('Danser als coach registreren').selectOption({ label: 'Dancing coach' }); await page.getByLabel('Danser als coach registreren').selectOption({ label: 'Dancing coach' });

View File

@@ -352,7 +352,8 @@
<div class="show-details"> <div class="show-details">
<h3>{{ show.name }}</h3> <h3>{{ show.name }}</h3>
<p> <p>
{{ show.finalized ? 'Definitief' : 'Concept' }} · x = {{ show.preferredDayParts }} {{ show.finalized ? 'Definitief' : 'Concept' }} · preferredDayParts =
{{ show.preferredDayParts }}
</p> </p>
@for (violation of show.violations; track $index) { @for (violation of show.violations; track $index) {
<p class="alert error">{{ violation }}</p> <p class="alert error">{{ violation }}</p>

View File

@@ -80,7 +80,7 @@ describe('EntityEditor', () => {
appearances: { REGULAR: 2, SELECTION: 2, PRE_SCHOOLER: 1, SHOW_CASE: 2, PRE_EDUCATION: 2 }, appearances: { REGULAR: 2, SELECTION: 2, PRE_SCHOOLER: 1, SHOW_CASE: 2, PRE_EDUCATION: 2 },
}; };
it('defaults x to two and rejects duplicate dated dayparts and fractional x', () => { it('defaults preferredDayParts to two and rejects duplicate dated dayparts and fractional preferredDayParts', () => {
const component = editor({ kind: 'show' }); const component = editor({ kind: 'show' });
component.name = 'Show'; component.name = 'Show';
expect(component.validDetails()).toBe(true); expect(component.validDetails()).toBe(true);
@@ -99,7 +99,27 @@ describe('EntityEditor', () => {
expect(component.validDetails()).toBe(false); expect(component.validDetails()).toBe(false);
}); });
it('retains x when dayparts are removed so it can be explicitly corrected', () => { it('preserves an explicitly chosen preferredDayParts when adding dayparts to a new show', async () => {
api.saveShow.mockReturnValue(of(show));
const component = editor({ kind: 'show' });
component.name = 'Show';
component.addPart();
component.dayParts[0].date = '2027-06-01';
component.addPart();
component.dayParts[1].date = '2027-06-02';
component.preferredDayParts = 1;
component.addPart();
component.dayParts[2].date = '2027-06-03';
expect(component.preferredDayParts).toBe(1);
await component.save();
expect(api.saveShow).toHaveBeenCalledWith(
7,
expect.objectContaining({ preferredDayParts: 1 }),
undefined,
);
});
it('retains preferredDayParts when dayparts are removed so it can be explicitly corrected', () => {
const component = editor({ kind: 'show', value: show }); const component = editor({ kind: 'show', value: show });
component.addPart(); component.addPart();
component.dayParts[2].date = '2027-06-03'; component.dayParts[2].date = '2027-06-03';
@@ -144,7 +164,7 @@ describe('EntityEditor', () => {
} }
}); });
it('displays one editable x field with a default of one for a one-daypart show', async () => { it('displays one editable preferredDayParts field with a default of one for a one-daypart show', async () => {
// jsdom does not implement native dialog opening. // jsdom does not implement native dialog opening.
const descriptor = Object.getOwnPropertyDescriptor(HTMLDialogElement.prototype, 'showModal'); const descriptor = Object.getOwnPropertyDescriptor(HTMLDialogElement.prototype, 'showModal');
Object.defineProperty(HTMLDialogElement.prototype, 'showModal', { Object.defineProperty(HTMLDialogElement.prototype, 'showModal', {

View File

@@ -185,7 +185,16 @@ export class EntityEditor implements OnInit {
readonly today = new Date().toLocaleDateString('en-CA'); readonly today = new Date().toLocaleDateString('en-CA');
private readonly api = inject(SchoolApi); private readonly api = inject(SchoolApi);
dayParts: DayPart[] = []; dayParts: DayPart[] = [];
preferredDayParts = 2; private preferredDayPartsValue = 2;
private preferredDayPartsEdited = false;
get preferredDayParts(): number {
return this.preferredDayPartsValue;
}
set preferredDayParts(value: number) {
this.preferredDayPartsValue = value;
this.preferredDayPartsEdited = true;
}
name = ''; name = '';
date = ''; date = '';
type: GroupType = 'REGULAR'; type: GroupType = 'REGULAR';
@@ -218,7 +227,8 @@ export class EntityEditor implements OnInit {
addPart(): void { addPart(): void {
if (this.dayParts.length >= 6) return; if (this.dayParts.length >= 6) return;
this.dayParts.push({ date: '', type: 'AFTERNOON', danceGroupIds: [] }); this.dayParts.push({ date: '', type: 'AFTERNOON', danceGroupIds: [] });
if (!this.target().value) this.preferredDayParts = Math.min(2, this.dayParts.length); if (!this.target().value && !this.preferredDayPartsEdited)
this.preferredDayPartsValue = Math.min(2, this.dayParts.length);
} }
removePart(index: number): void { removePart(index: number): void {
this.dayParts.splice(index, 1); this.dayParts.splice(index, 1);

View File

@@ -16,7 +16,7 @@ import { DialogFrame } from './dialog-frame';
(dismissed)="closed.emit()" (dismissed)="closed.emit()"
> >
<p> <p>
{{ !dirty() && current()?.finalized ? 'Definitief' : 'Concept' }} · x = {{ !dirty() && current()?.finalized ? 'Definitief' : 'Concept' }} · preferredDayParts =
{{ show().preferredDayParts }} {{ show().preferredDayParts }}
</p> </p>
<p> <p>

View File

@@ -17,7 +17,7 @@ Validated on 2026-11-03 against the confirmed S18 specification.
Focused solver tests cover showcase bookends (including extra showcases), single Focused solver tests cover showcase bookends (including extra showcases), single
and absent showcases, changing gaps, coach exemptions, infeasibility, the strict and absent showcases, changing gaps, coach exemptions, infeasibility, the strict
priority of showcase attendance over balance, and style/age scores. Lifecycle priority of showcase attendance over balance, and style/age scores. Lifecycle
and HTTP tests cover skipped empty groups, first-show-date ages, retained x after and HTTP tests cover skipped empty groups, first-show-date ages, retained preferredDayParts after
daypart removal, atomic structural validation, invalid drafts, final status daypart removal, atomic structural validation, invalid drafts, final status
invalidation, stale solver results, timeout preservation, coach identity links, invalidation, stale solver results, timeout preservation, coach identity links,
and ordered JSON round trips including version-2 migration. and ordered JSON round trips including version-2 migration.
@@ -36,7 +36,7 @@ assessed against `CODING_STANDARDS.md` and `docs/backend-review.md`:
- `ScheduleRules` has cyclomatic signals for independent configuration checks - `ScheduleRules` has cyclomatic signals for independent configuration checks
and showcase cases. Each rule family is isolated in a short method and shared and showcase cases. Each rule family is isolated in a short method and shared
between finalization and solver-result validation. between finalization and solver-result validation.
- `ShowSchedules` retains branching for nullable initial settings, retained x, - `ShowSchedules` retains branching for nullable initial settings, retained preferredDayParts,
and structural draft validation. It stages complete replacement lists before and structural draft validation. It stages complete replacement lists before
mutation and uses the existing aggregate lock. It separates schedule lifecycle mutation and uses the existing aggregate lock. It separates schedule lifecycle
from the already broad `SchoolManagementService` aggregate owner. from the already broad `SchoolManagementService` aggregate owner.

View File

@@ -5,12 +5,12 @@ calendar dates. Counts do not restart on a new date.
## Required performances ## Required performances
The manager chooses **x**, the desired number of distinct dayparts, per show. The manager chooses **preferredDayParts**, the desired number of distinct dayparts, per show.
It defaults to two, or one for a show with only one daypart. A show can be saved It defaults to two, or one for a show with only one daypart. A show can be saved
without dayparts but cannot be scheduled. If dayparts are removed, x is retained without dayparts but cannot be scheduled. If dayparts are removed, preferredDayParts is retained
and must be corrected before scheduling when it exceeds the remaining count. and must be corrected before scheduling when it exceeds the remaining count.
Every nonempty regular, selection and pre-education group performs in exactly x Every nonempty regular, selection and pre-education group performs in exactly preferredDayParts
dayparts, once in each. Each preschool group performs exactly once in the whole dayparts, once in each. Each preschool group performs exactly once in the whole
show. Every showcase performs once in every daypart. Groups without dancers are show. Every showcase performs once in every daypart. Groups without dancers are
skipped completely, including for showcase positions and balancing. skipped completely, including for showcase positions and balancing.
@@ -22,17 +22,17 @@ twice in the same daypart. Without showcases there is no opening/closing rule.
## People and changing clothes ## People and changing clothes
An ordinary dancer must perform in no more than x distinct dayparts across all An ordinary dancer must perform in no more than preferredDayParts distinct dayparts across all
of their groups. The same dancer registration must be used in every group. of their groups. The same dancer registration must be used in every group.
Showcase participants necessarily perform in every daypart. For their other Showcase participants necessarily perform in every daypart. For their other
performances, exceeding x dayparts is allowed but discouraged. We count their performances, exceeding preferredDayParts dayparts is allowed but discouraged. We count their
non-showcase dayparts, subtract x, and count only positive excesses. The planner's non-showcase dayparts, subtract preferredDayParts, and count only positive excesses. The planner's
first preference is to minimize the sum of these excesses across dancers. first preference is to minimize the sum of these excesses across dancers.
A coach registration must be explicitly linked to the corresponding dancer; A coach registration must be explicitly linked to the corresponding dancer;
names are not used to guess identity. Coaches are available throughout the show names are not used to guess identity. Coaches are available throughout the show
and are exempt from both the x limit and the excess penalty. A coach only performs and are exempt from both the preferredDayParts limit and the excess penalty. A coach only performs
with a group if included in its dancer membership. Being a group's coach alone with a group if included in its dancer membership. Being a group's coach alone
never adds a performance or forces that group into extra dayparts. never adds a performance or forces that group into extra dayparts.
@@ -85,5 +85,5 @@ Preferences alone never block finalization.
Manual edits and relevant membership, dancer-age, coach-link or show-setting Manual edits and relevant membership, dancer-age, coach-link or show-setting
changes return final schedules to draft. Existing schedules and imported schedules changes return final schedules to draft. Existing schedules and imported schedules
are drafts. Import retains assignment order and recalculates violations. Export are drafts. Import retains assignment order and recalculates violations. Export
is available for both statuses and records status, x and coach identity links; is available for both statuses and records status, preferredDayParts and coach identity links;
reimport always requires a fresh finalization. reimport always requires a fresh finalization.

View File

@@ -16,11 +16,11 @@ See [validation evidence](../../docs/s18-validation.md) and
- A show consists of dayparts and can span multiple dates, typically two days. - A show consists of dayparts and can span multiple dates, typically two days.
All counts below apply across the entire show, not separately per date. All counts below apply across the entire show, not separately per date.
- Each show has one configurable integer `x`, between 1 and the number of - Each show has one configurable integer `preferredDayParts`, between 1 and the number of
dayparts inclusive. Its default is 2, or 1 for a one-daypart show. dayparts inclusive. Its default is 2, or 1 for a one-daypart show.
- This replaces the existing configurable appearance counts per group type. - This replaces the existing configurable appearance counts per group type.
- A show without dayparts can be stored but cannot be scheduled. - A show without dayparts can be stored but cannot be scheduled.
- If reducing the number of dayparts makes `x` invalid, require the user to - If reducing the number of dayparts makes `preferredDayParts` invalid, require the user to
correct it before scheduling again; do not silently change it. correct it before scheduling again; do not silently change it.
- Groups without dancers are excluded from scheduling and from all performance - Groups without dancers are excluded from scheduling and from all performance
counts and showcase-position requirements. counts and showcase-position requirements.
@@ -28,9 +28,9 @@ See [validation evidence](../../docs/s18-validation.md) and
## Required group appearances ## Required group appearances
- Each nonempty REGULAR, SELECTION and PRE_EDUCATION group performs in exactly - Each nonempty REGULAR, SELECTION and PRE_EDUCATION group performs in exactly
`x` distinct dayparts, once within each selected daypart. `preferredDayParts` distinct dayparts, once within each selected daypart.
- Each nonempty PRE_SCHOOLER group performs exactly once in the complete show, - Each nonempty PRE_SCHOOLER group performs exactly once in the complete show,
regardless of `x`. At most two such groups are expected. They can be assigned regardless of `preferredDayParts`. At most two such groups are expected. They can be assigned
freely to dayparts; no additional placement restriction applies. freely to dayparts; no additional placement restriction applies.
- Each nonempty SHOW_CASE group performs exactly once in every daypart. - Each nonempty SHOW_CASE group performs exactly once in every daypart.
- With at least two showcase groups, each daypart starts with one showcase and - With at least two showcase groups, each daypart starts with one showcase and
@@ -42,12 +42,12 @@ See [validation evidence](../../docs/s18-validation.md) and
## Dancer participation and coaches ## Dancer participation and coaches
- A dancer without a showcase membership performs in at most `x` distinct - A dancer without a showcase membership performs in at most `preferredDayParts` distinct
dayparts across the complete show. This is a hard constraint. dayparts across the complete show. This is a hard constraint.
- A showcase participant necessarily performs in all dayparts. Their other - A showcase participant necessarily performs in all dayparts. Their other
group performances may span more than `x` dayparts, but this is undesirable. group performances may span more than `preferredDayParts` dayparts, but this is undesirable.
- For each non-coach showcase participant, count the distinct dayparts in which - For each non-coach showcase participant, count the distinct dayparts in which
they perform outside showcases. Their excess is `max(0, count - x)`. they perform outside showcases. Their excess is `max(0, count - preferredDayParts)`.
Minimize the sum of these excesses as the first optimization priority. Minimize the sum of these excesses as the first optimization priority.
- A person dancing in several groups must use the same dancer registration in - A person dancing in several groups must use the same dancer registration in
every group, including when accompanying preschoolers. Up to five group every group, including when accompanying preschoolers. Up to five group