some renaming of variables
All checks were successful
Build, test and publish / verify (push) Successful in 5m55s
All checks were successful
Build, test and publish / verify (push) Successful in 5m55s
This commit is contained in:
10
README.md
10
README.md
@@ -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`),
|
||||
with unique date/type combinations. The displayed date range comes from these
|
||||
dayparts. Each show stores `preferredDayParts` (x), defaulting to two or one for
|
||||
one daypart. Ordinary groups, including PRE_EDUCATION, perform in exactly x
|
||||
dayparts. Each show stores `preferredDayParts`, defaulting to two or one for
|
||||
one daypart. Ordinary groups, including PRE_EDUCATION, perform in exactly preferredDayParts
|
||||
dayparts; preschool groups once; showcases every daypart. See
|
||||
[show-scheduling-rules.md](show-scheduling-rules.md) for the full hard constraints,
|
||||
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`
|
||||
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
|
||||
drafts with default x. Version 1 is unsupported. Legacy `appearances` fields are
|
||||
retained for file compatibility; S18 scheduling uses x and fixed type exceptions.
|
||||
drafts with default preferredDayParts. Version 1 is unsupported. Legacy `appearances` fields are
|
||||
retained for file compatibility; S18 scheduling uses preferredDayParts and fixed type exceptions.
|
||||
The REST `appearances` input is deprecated; use `preferredDayParts`.
|
||||
|
||||
Run the HTTP integration tests with:
|
||||
|
||||
@@ -14,7 +14,7 @@ public final class ScheduleRules {
|
||||
public static List<String> violations(final SchedulingInput input, final List<List<Long>> parts) {
|
||||
final List<String> errors = new ArrayList<>();
|
||||
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)) {
|
||||
errors.add("Assignments must contain one list for every daypart.");
|
||||
@@ -86,7 +86,7 @@ public final class ScheduleRules {
|
||||
|
||||
private static void attendance(
|
||||
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()
|
||||
.filter(GroupRequirement::showcase)
|
||||
.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) -> {
|
||||
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.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,16 +3,21 @@ package nl.agilecc.agiledancers.model.scheduling;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Immutable solver snapshot.
|
||||
*
|
||||
* @param coachDancerIds dancer registration IDs linked to coaches, used for participation exemptions
|
||||
*/
|
||||
public record SchedulingInput(
|
||||
long showId,
|
||||
long revision,
|
||||
int dayParts,
|
||||
List<GroupRequirement> groups,
|
||||
int preferredDayParts,
|
||||
Set<Long> coachIds) {
|
||||
Set<Long> coachDancerIds) {
|
||||
public SchedulingInput {
|
||||
groups = List.copyOf(groups);
|
||||
coachIds = Set.copyOf(coachIds);
|
||||
coachDancerIds = Set.copyOf(coachDancerIds);
|
||||
}
|
||||
|
||||
public SchedulingInput(
|
||||
|
||||
@@ -26,7 +26,7 @@ public final class SchedulingInputs {
|
||||
}
|
||||
|
||||
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()
|
||||
.map(DayPart::getDate)
|
||||
.min(LocalDate::compareTo)
|
||||
@@ -40,7 +40,8 @@ public final class SchedulingInputs {
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.map(Dancer::getId)
|
||||
.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) {
|
||||
|
||||
@@ -60,7 +60,8 @@ public class SchoolManagementService {
|
||||
"Scheduling requires at least one school group and one daypart.");
|
||||
}
|
||||
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);
|
||||
return SchedulingInputs.of(school(schoolId), show, revisions.getOrDefault(showId, 0L));
|
||||
|
||||
@@ -21,25 +21,26 @@ public class ShowSchedules {
|
||||
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) {
|
||||
synchronized (schools) {
|
||||
ShowConfiguration.validateParts(settings.getDayParts());
|
||||
final Show existing = showId == null ? null : schools.show(schoolId, showId);
|
||||
final int x = settings.getPreferredDayParts() == null
|
||||
final int preferredDayParts = settings.getPreferredDayParts() == null
|
||||
? (existing == null
|
||||
? (settings.getDayParts().size() == 1 ? 1 : 2)
|
||||
: SchedulingInputs.preferred(existing))
|
||||
: settings.getPreferredDayParts();
|
||||
validateX(x, existing, settings.getDayParts().size());
|
||||
validatePreferredDayParts(
|
||||
preferredDayParts, existing, settings.getDayParts().size());
|
||||
final Show show = existing == null
|
||||
? schools.createShow(schoolId, settings.getName(), settings.getDayParts(), null)
|
||||
: existing;
|
||||
final boolean changed = x != SchedulingInputs.preferred(show)
|
||||
final boolean changed = preferredDayParts != SchedulingInputs.preferred(show)
|
||||
|| !keys(show.getDayParts()).equals(keys(settings.getDayParts()));
|
||||
show.setDayParts(ShowConfiguration.retainAssignments(show, settings.getDayParts()));
|
||||
show.setName(settings.getName());
|
||||
show.setPreferredDayParts(x);
|
||||
show.setPreferredDayParts(preferredDayParts);
|
||||
if (changed) {
|
||||
schools.touch(show);
|
||||
}
|
||||
@@ -53,13 +54,16 @@ public class ShowSchedules {
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static void validateX(final int x, final Show existing, final int parts) {
|
||||
if (x < 1
|
||||
|| x > 6
|
||||
|| (parts > 0 && x > parts && (existing == null || x != SchedulingInputs.preferred(existing)))) {
|
||||
private static void validatePreferredDayParts(final int preferredDayParts, final Show existing, final int parts) {
|
||||
if (preferredDayParts < 1
|
||||
|| preferredDayParts > 6
|
||||
|| (parts > 0
|
||||
&& preferredDayParts > parts
|
||||
&& (existing == null || preferredDayParts != SchedulingInputs.preferred(existing)))) {
|
||||
throw new ManagementException(
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -205,8 +205,9 @@ public record SchoolSnapshot(
|
||||
.build());
|
||||
}
|
||||
ShowConfiguration.validateParts(parts);
|
||||
final int x = show.preferredDayParts() == null ? (parts.size() == 1 ? 1 : 2) : show.preferredDayParts();
|
||||
check(x >= 1 && x <= 6, "x moet tussen 1 en 6 liggen");
|
||||
final int preferredDayParts =
|
||||
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()
|
||||
.name(show.name())
|
||||
.dayParts(parts)
|
||||
@@ -214,7 +215,7 @@ public record SchoolSnapshot(
|
||||
show.appearances() == null
|
||||
? ShowConfiguration.defaults(parts.size())
|
||||
: Map.copyOf(show.appearances()))
|
||||
.preferredDayParts(x)
|
||||
.preferredDayParts(preferredDayParts)
|
||||
.finalized(false)
|
||||
.build();
|
||||
entity.setId((long) showIds.size());
|
||||
|
||||
@@ -24,7 +24,7 @@ public final class SchedulingObjectives {
|
||||
input.groups().stream()
|
||||
.filter(GroupRequirement::showcase)
|
||||
.forEach((final GroupRequirement g) -> showcases.addAll(g.dancerIds()));
|
||||
dancers.removeAll(input.coachIds());
|
||||
dancers.removeAll(input.coachDancerIds());
|
||||
final LinearExprBuilder penalty = LinearExpr.newBuilder();
|
||||
for (final long dancer : dancers) {
|
||||
final IntVar[] parts = dancerParts(model, input, vars, dancer);
|
||||
|
||||
@@ -40,11 +40,11 @@ class S18LifecycleTest {
|
||||
.toList();
|
||||
}
|
||||
|
||||
private Show settings(final int n, final Integer x) {
|
||||
private Show settings(final int n, final Integer preferredDayParts) {
|
||||
return Show.builder()
|
||||
.name("Show")
|
||||
.dayParts(parts(n))
|
||||
.preferredDayParts(x)
|
||||
.preferredDayParts(preferredDayParts)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -242,7 +242,7 @@ test('links a coach and finalizes performances spread across multiple dates', as
|
||||
data: { assignments: groupIds.map((id) => [id]) },
|
||||
});
|
||||
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 page.getByText('Coaches koppelen aan dansers', { exact: true }).click();
|
||||
await page.getByLabel('Danser als coach registreren').selectOption({ label: 'Dancing coach' });
|
||||
|
||||
@@ -352,7 +352,8 @@
|
||||
<div class="show-details">
|
||||
<h3>{{ show.name }}</h3>
|
||||
<p>
|
||||
{{ show.finalized ? 'Definitief' : 'Concept' }} · x = {{ show.preferredDayParts }}
|
||||
{{ show.finalized ? 'Definitief' : 'Concept' }} · preferredDayParts =
|
||||
{{ show.preferredDayParts }}
|
||||
</p>
|
||||
@for (violation of show.violations; track $index) {
|
||||
<p class="alert error">{{ violation }}</p>
|
||||
|
||||
@@ -80,7 +80,7 @@ describe('EntityEditor', () => {
|
||||
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' });
|
||||
component.name = 'Show';
|
||||
expect(component.validDetails()).toBe(true);
|
||||
@@ -99,7 +99,27 @@ describe('EntityEditor', () => {
|
||||
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 });
|
||||
component.addPart();
|
||||
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.
|
||||
const descriptor = Object.getOwnPropertyDescriptor(HTMLDialogElement.prototype, 'showModal');
|
||||
Object.defineProperty(HTMLDialogElement.prototype, 'showModal', {
|
||||
|
||||
@@ -185,7 +185,16 @@ export class EntityEditor implements OnInit {
|
||||
readonly today = new Date().toLocaleDateString('en-CA');
|
||||
private readonly api = inject(SchoolApi);
|
||||
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 = '';
|
||||
date = '';
|
||||
type: GroupType = 'REGULAR';
|
||||
@@ -218,7 +227,8 @@ export class EntityEditor implements OnInit {
|
||||
addPart(): void {
|
||||
if (this.dayParts.length >= 6) return;
|
||||
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 {
|
||||
this.dayParts.splice(index, 1);
|
||||
|
||||
@@ -16,7 +16,7 @@ import { DialogFrame } from './dialog-frame';
|
||||
(dismissed)="closed.emit()"
|
||||
>
|
||||
<p>
|
||||
{{ !dirty() && current()?.finalized ? 'Definitief' : 'Concept' }} · x =
|
||||
{{ !dirty() && current()?.finalized ? 'Definitief' : 'Concept' }} · preferredDayParts =
|
||||
{{ show().preferredDayParts }}
|
||||
</p>
|
||||
<p>
|
||||
|
||||
@@ -17,7 +17,7 @@ Validated on 2026-11-03 against the confirmed S18 specification.
|
||||
Focused solver tests cover showcase bookends (including extra showcases), single
|
||||
and absent showcases, changing gaps, coach exemptions, infeasibility, the strict
|
||||
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
|
||||
invalidation, stale solver results, timeout preservation, coach identity links,
|
||||
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
|
||||
and showcase cases. Each rule family is isolated in a short method and shared
|
||||
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
|
||||
mutation and uses the existing aggregate lock. It separates schedule lifecycle
|
||||
from the already broad `SchoolManagementService` aggregate owner.
|
||||
|
||||
@@ -5,12 +5,12 @@ calendar dates. Counts do not restart on a new date.
|
||||
|
||||
## 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
|
||||
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.
|
||||
|
||||
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
|
||||
show. Every showcase performs once in every daypart. Groups without dancers are
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
Showcase participants necessarily perform in every daypart. For their other
|
||||
performances, exceeding x dayparts is allowed but discouraged. We count their
|
||||
non-showcase dayparts, subtract x, and count only positive excesses. The planner's
|
||||
performances, exceeding preferredDayParts dayparts is allowed but discouraged. We count their
|
||||
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.
|
||||
|
||||
A coach registration must be explicitly linked to the corresponding dancer;
|
||||
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
|
||||
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
|
||||
changes return final schedules to draft. Existing schedules and imported schedules
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
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.
|
||||
- This replaces the existing configurable appearance counts per group type.
|
||||
- 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.
|
||||
- Groups without dancers are excluded from scheduling and from all performance
|
||||
counts and showcase-position requirements.
|
||||
@@ -28,9 +28,9 @@ See [validation evidence](../../docs/s18-validation.md) and
|
||||
## Required group appearances
|
||||
|
||||
- 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,
|
||||
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.
|
||||
- Each nonempty SHOW_CASE group performs exactly once in every daypart.
|
||||
- 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
|
||||
|
||||
- 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.
|
||||
- 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
|
||||
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.
|
||||
- A person dancing in several groups must use the same dancer registration in
|
||||
every group, including when accompanying preschoolers. Up to five group
|
||||
|
||||
Reference in New Issue
Block a user