149 lines
4.9 KiB
C#
149 lines
4.9 KiB
C#
namespace HSchool.People;
|
|
|
|
/// <summary>
|
|
/// Builds a roster from a catalog, a map and a school seed. Deterministic: same inputs, same people.
|
|
/// </summary>
|
|
public static class RosterGenerator
|
|
{
|
|
/// <summary>
|
|
/// Matches <c>SimulationOptions.DefaultStartDate</c> so tests without a clock still sit in
|
|
/// the default school year. Callers with a live clock must pass <paramref name="asOf"/>.
|
|
/// </summary>
|
|
public static readonly DateTime DefaultAsOf = new(2012, 3, 31, 6, 0, 0, DateTimeKind.Utc);
|
|
|
|
public static Roster Generate(
|
|
DefCatalog catalog,
|
|
MapLayout map,
|
|
int schoolSeed,
|
|
string nameSetId,
|
|
DateTime? asOf = null)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(catalog);
|
|
ArgumentNullException.ThrowIfNull(map);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(nameSetId);
|
|
|
|
if (!catalog.NameSets.TryGetValue(nameSetId, out var names))
|
|
{
|
|
throw new ArgumentException($"Unknown name set '{nameSetId}'.", nameof(nameSetId));
|
|
}
|
|
|
|
var when = DateTime.SpecifyKind(asOf ?? DefaultAsOf, DateTimeKind.Utc);
|
|
var yearStart = SchoolYears.StartOn(when);
|
|
var demand = SchoolDemand.From(catalog, map);
|
|
var plans = FamilyPlanner.Plan(schoolSeed, ShuffleSeats(demand.Seats, schoolSeed));
|
|
|
|
var people = new List<Person>();
|
|
var families = new List<Family>(plans.Count);
|
|
foreach (var plan in plans)
|
|
{
|
|
var (family, members) = FamilyFactory.Create(catalog, names, schoolSeed, plan, yearStart, when);
|
|
families.Add(family);
|
|
people.AddRange(members);
|
|
}
|
|
|
|
var nextFamily = plans.Count;
|
|
var adults = people.Count(person => !person.IsStudent);
|
|
while (adults < demand.Staff.Count)
|
|
{
|
|
var (family, member) = FamilyFactory.CreateStaffOnly(catalog, names, schoolSeed, nextFamily, when);
|
|
families.Add(family);
|
|
people.Add(member);
|
|
nextFamily++;
|
|
adults++;
|
|
}
|
|
|
|
var staffed = AssignStaff(people, demand.Staff);
|
|
var classes = FillClasses(demand.Classes, staffed);
|
|
return new Roster(staffed, families, classes);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Seats leave <see cref="SchoolDemand"/> grouped by classroom, and a family takes a run of
|
|
/// consecutive seats — so without this every pair of siblings landed in the same class, the
|
|
/// same year and the same twelve-month birth window. Shuffling is seeded, so the roster stays
|
|
/// reproducible.
|
|
/// </summary>
|
|
private static IReadOnlyList<PupilSeat> ShuffleSeats(IReadOnlyList<PupilSeat> seats, int schoolSeed)
|
|
{
|
|
var shuffled = seats.ToArray();
|
|
var rng = new Random(Seed.ForSchool(schoolSeed, Seed.SeatShuffleSalt));
|
|
for (var i = shuffled.Length - 1; i > 0; i--)
|
|
{
|
|
var j = rng.Next(i + 1);
|
|
(shuffled[i], shuffled[j]) = (shuffled[j], shuffled[i]);
|
|
}
|
|
|
|
return shuffled;
|
|
}
|
|
|
|
private static IReadOnlyList<Person> AssignStaff(List<Person> people, IReadOnlyList<StaffOpening> openings)
|
|
{
|
|
if (openings.Count == 0)
|
|
{
|
|
return people;
|
|
}
|
|
|
|
var jobs = new Dictionary<string, StaffOpening>(StringComparer.Ordinal);
|
|
var index = 0;
|
|
foreach (var person in people)
|
|
{
|
|
if (person.IsStudent || index >= openings.Count)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
jobs[person.Id] = openings[index];
|
|
index++;
|
|
}
|
|
|
|
if (jobs.Count == 0)
|
|
{
|
|
return people;
|
|
}
|
|
|
|
var result = new Person[people.Count];
|
|
for (var i = 0; i < people.Count; i++)
|
|
{
|
|
var person = people[i];
|
|
result[i] = jobs.TryGetValue(person.Id, out var job)
|
|
? person with
|
|
{
|
|
IsStaff = true,
|
|
Position = job.Position,
|
|
WorkplaceRoomId = job.RoomId,
|
|
}
|
|
: person;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static IReadOnlyList<SchoolClass> FillClasses(
|
|
IReadOnlyList<SchoolClass> classes,
|
|
IReadOnlyList<Person> people)
|
|
{
|
|
var pupils = new Dictionary<string, List<string>>(StringComparer.Ordinal);
|
|
foreach (var schoolClass in classes)
|
|
{
|
|
pupils[schoolClass.Id] = new List<string>(schoolClass.Capacity);
|
|
}
|
|
|
|
foreach (var person in people)
|
|
{
|
|
if (person.ClassId is { } classId && pupils.TryGetValue(classId, out var list))
|
|
{
|
|
list.Add(person.Id);
|
|
}
|
|
}
|
|
|
|
var filled = new SchoolClass[classes.Count];
|
|
for (var i = 0; i < classes.Count; i++)
|
|
{
|
|
var schoolClass = classes[i];
|
|
filled[i] = schoolClass with { PupilIds = pupils[schoolClass.Id] };
|
|
}
|
|
|
|
return filled;
|
|
}
|
|
}
|