Something went wrong. Try again.
A functional toolkit for authoring resource-leveling algorithms — compose constraint and scoring blocks, enumerate feasible schedules
Something went wrong. Try again.
4.3 kB · 124 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125// Schedule ──materialize──> ProjectFile.// Carries forward every source field except the ones the schedule decides// (start, finish, duration). Tasks that don't appear in `schedule.tasks`// pass through unchanged — useful when only a subset of tasks were// scheduled (e.g. mid-pipeline partial schedules).
import { Duration } from "../model/Duration.ts";import { TimeUnit } from "../model/types.ts";import type { ProjectFile, ProjectProperties } from "../schema/project.ts";import type { Task } from "../schema/task.ts";
import { countWorkingDays, dayToDate, resolveWorkingCalendar,} from "./calendarDays.ts";import type { Schedule, ScheduledTask, WorkingCalendar } from "./types.ts";
function workingDaysPerWeek(properties: ProjectProperties): number { const wpd = properties.minutesPerDay; const wpw = properties.minutesPerWeek; if (wpd <= 0 || wpw <= 0) return 5; return wpw / wpd;}
// Re-emit duration in the original unit using properties-derived// conversions; falls back to Days when the source had no unit.function workingDaysToDuration( workingDays: number, original: Duration | null, properties: ProjectProperties,): Duration | null { if (!original) return Duration.from(workingDays, TimeUnit.Days); const minutesPerDay = properties.minutesPerDay; const wpw = workingDaysPerWeek(properties); const dpm = properties.daysPerMonth; switch (original.unit) { case TimeUnit.Days: return Duration.from(workingDays, TimeUnit.Days); case TimeUnit.Weeks: return Duration.from( wpw > 0 ? workingDays / wpw : workingDays, TimeUnit.Weeks, ); case TimeUnit.Hours: return Duration.from((workingDays * minutesPerDay) / 60, TimeUnit.Hours); case TimeUnit.Minutes: return Duration.from(workingDays * minutesPerDay, TimeUnit.Minutes); case TimeUnit.Months: return Duration.from( dpm > 0 ? workingDays / dpm : workingDays, TimeUnit.Months, ); case TimeUnit.Percent: return original; }}
function applyScheduledTask( task: Task, scheduled: ScheduledTask, cal: WorkingCalendar, properties: ProjectProperties,): Task { // A non-null modeId means a ModeSelection mode was chosen; the duration is // already encoded in start→finish, and the realized resource demand is // re-applied to project.assignments in `materialize` below. const workingDays = countWorkingDays( cal, scheduled.startDay, scheduled.finishDay, ); return { ...task, start: dayToDate(cal, scheduled.startDay), finish: dayToDate(cal, scheduled.finishDay), duration: workingDaysToDuration(workingDays, task.duration, properties), };}
export function materialize(schedule: Schedule): ProjectFile { const source = schedule.resolved.source; const properties = source.properties;
const byUniqueId = new Map<number, ScheduledTask>(); for (const t of schedule.tasks) byUniqueId.set(t.uniqueId, t); const resolvedTaskById = new Map( schedule.resolved.tasks.map((t) => [t.uniqueId, t]), );
const tasks = source.tasks.map((task) => { if (task.uniqueId === null) return task; const scheduled = byUniqueId.get(task.uniqueId); if (!scheduled) return task; const cal = resolveWorkingCalendar( schedule.resolved, resolvedTaskById.get(task.uniqueId)?.calendarUniqueId ?? null, ); return applyScheduledTask(task, scheduled, cal, properties); });
// Re-attribute assignments for ModeSelection-governed tasks to the realized // mode's resource demand, so the materialized file agrees with the schedule's // resource load (the chosen crew, not the static home assignment). const realized = schedule.realizedAssignments; if (!realized || realized.length === 0) return { ...source, tasks }; const overridden = new Set(realized.map((a) => a.taskUniqueId)); const assignments = [ ...source.assignments.filter( (a) => a.taskUniqueId === null || !overridden.has(a.taskUniqueId), ), ...realized.map((a) => ({ taskUniqueId: a.taskUniqueId, resourceUniqueId: a.resourceUniqueId, units: a.units, work: null, start: null, finish: null, actualWork: null, remainingWork: null, })), ]; return { ...source, tasks, assignments };}