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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286import type { Assignment } from "../model/Assignment.ts";import type { Calendar } from "../model/Calendar.ts";import type { ProjectFile } from "../model/Project.ts";import type { Resource } from "../model/Resource.ts";import type { Task } from "../model/Task.ts";import { formatProjectDate } from "../dateTime.ts";import { RelationType, ResourceType } from "../model/types.ts";import { durationContextFromProject, formatLinkLag, formatXsdDuration,} from "./XsdDuration.ts";
export interface MspdiWriterOptions { saveVersion?: number;}
export class MspdiWriter { write(project: ProjectFile, options: MspdiWriterOptions = {}): string { const saveVersion = options.saveVersion ?? 14; const durationContext = durationContextFromProject(project.properties); const lines = [ '<?xml version="1.0" encoding="UTF-8"?>', '<Project xmlns="http://schemas.microsoft.com/project">', tag("SaveVersion", String(saveVersion), 1), tag("Title", project.properties.title, 1), tag("Author", project.properties.author, 1), tag("StartDate", formatProjectDate(project.properties.startDate), 1), tag("FinishDate", formatProjectDate(project.properties.finishDate), 1), tag("StatusDate", formatProjectDate(project.properties.statusDate), 1), tag("MinutesPerDay", nullableNumber(project.properties.minutesPerDay), 1), tag( "MinutesPerWeek", nullableNumber(project.properties.minutesPerWeek), 1, ), tag("DaysPerMonth", nullableNumber(project.properties.daysPerMonth), 1), tag( "CalendarUID", nullableNumber(project.properties.defaultCalendarUniqueId), 1, ), " <Calendars>", ...project.calendars.flatMap((calendar) => this.writeCalendar(calendar)), " </Calendars>", " <Tasks>", ...project.tasks.flatMap((task) => this.writeTask(task, durationContext)), " </Tasks>", " <Resources>", ...project.resources.flatMap((resource) => this.writeResource(resource, durationContext), ), " </Resources>", " <Assignments>", ...project.assignments.flatMap((assignment) => this.writeAssignment(assignment, durationContext), ), " </Assignments>", "</Project>", ];
return lines.filter(Boolean).join("\n") + "\n"; }
private writeCalendar(calendar: Calendar): string[] { const lines = [" <Calendar>"]; lines.push(tag("UID", nullableNumber(calendar.uniqueId), 3)); lines.push(tag("Name", calendar.name, 3));
if (calendar.weekDays.length > 0) { lines.push(" <WeekDays>"); for (const weekDay of calendar.weekDays) { lines.push(" <WeekDay>"); lines.push(tag("DayType", String(weekDay.dayType), 5)); lines.push(tag("DayWorking", weekDay.working ? "1" : "0", 5)); if (weekDay.workingTimes.length > 0) { lines.push(" <WorkingTimes>"); for (const workingTime of weekDay.workingTimes) { lines.push(" <WorkingTime>"); lines.push(tag("FromTime", workingTime.from, 7)); lines.push(tag("ToTime", workingTime.to, 7)); lines.push(" </WorkingTime>"); } lines.push(" </WorkingTimes>"); } lines.push(" </WeekDay>"); } lines.push(" </WeekDays>"); }
if (calendar.exceptions.length > 0) { lines.push(" <Exceptions>"); for (const exception of calendar.exceptions) { lines.push(" <Exception>"); lines.push(tag("Name", exception.name, 5)); lines.push(tag("FromDate", formatProjectDate(exception.fromDate), 5)); lines.push(tag("ToDate", formatProjectDate(exception.toDate), 5)); if (exception.working !== null) { lines.push(tag("Working", exception.working ? "1" : "0", 5)); } lines.push(" </Exception>"); } lines.push(" </Exceptions>"); }
lines.push(" </Calendar>"); return lines; }
private writeTask( task: Task, durationContext: ReturnType<typeof durationContextFromProject>, ): string[] { const lines = [" <Task>"]; lines.push(tag("UID", nullableNumber(task.uniqueId), 3)); lines.push(tag("ID", nullableNumber(task.id), 3)); lines.push(tag("Name", task.name, 3)); lines.push(tag("WBS", task.wbs, 3)); lines.push(tag("OutlineLevel", nullableNumber(task.outlineLevel), 3)); lines.push(tag("Start", formatProjectDate(task.start), 3)); lines.push(tag("Finish", formatProjectDate(task.finish), 3)); lines.push( tag("Duration", formatXsdDuration(task.duration, durationContext), 3), ); lines.push(tag("Work", formatXsdDuration(task.work, durationContext), 3)); lines.push( tag("ActualWork", formatXsdDuration(task.actualWork, durationContext), 3), ); lines.push(tag("PercentComplete", nullableNumber(task.percentComplete), 3)); lines.push(tag("Summary", formatBoolean(task.summary), 3)); lines.push(tag("Milestone", formatBoolean(task.milestone), 3)); lines.push(tag("Critical", formatBoolean(task.critical), 3)); lines.push(tag("Notes", task.notes, 3)); lines.push(tag("Priority", nullableNumber(task.priority), 3)); lines.push(tag("Cost", nullableNumber(task.cost), 3)); lines.push(tag("ActualStart", formatProjectDate(task.actualStart), 3)); lines.push(tag("ActualFinish", formatProjectDate(task.actualFinish), 3)); lines.push(tag("BaselineStart", formatProjectDate(task.baselineStart), 3)); lines.push( tag("BaselineFinish", formatProjectDate(task.baselineFinish), 3), ); lines.push( tag( "BaselineDuration", formatXsdDuration(task.baselineDuration, durationContext), 3, ), ); lines.push( tag("FreeSlack", formatXsdDuration(task.freeSlack, durationContext), 3), ); lines.push( tag("TotalSlack", formatXsdDuration(task.totalSlack, durationContext), 3), ); lines.push(tag("EarlyStart", formatProjectDate(task.earlyStart), 3)); lines.push(tag("EarlyFinish", formatProjectDate(task.earlyFinish), 3)); lines.push(tag("LateStart", formatProjectDate(task.lateStart), 3)); lines.push(tag("LateFinish", formatProjectDate(task.lateFinish), 3)); lines.push( tag( "LevelingDelay", formatXsdDuration(task.levelingDelay, durationContext), 3, ), ); lines.push(tag("Deadline", formatProjectDate(task.deadline), 3)); for (const predecessor of task.predecessors) { const lag = formatLinkLag(predecessor.lag, durationContext); lines.push(" <PredecessorLink>"); lines.push( tag( "PredecessorUID", nullableNumber(predecessor.predecessorUniqueId), 4, ), ); lines.push(tag("Type", formatRelationType(predecessor.type), 4)); lines.push(tag("LinkLag", lag.linkLag, 4)); lines.push(tag("LagFormat", lag.lagFormat, 4)); lines.push(" </PredecessorLink>"); } lines.push(" </Task>"); return lines; }
private writeResource( resource: Resource, durationContext: ReturnType<typeof durationContextFromProject>, ): string[] { return [ " <Resource>", tag("UID", nullableNumber(resource.uniqueId), 3), tag("ID", nullableNumber(resource.id), 3), tag("Name", resource.name, 3), tag("Type", formatResourceType(resource.type), 3), tag("EmailAddress", resource.email, 3), tag("Group", resource.group, 3), tag("MaxUnits", formatUnits(resource.maxUnits), 3), tag("Cost", nullableNumber(resource.cost), 3), tag("Work", formatXsdDuration(resource.work, durationContext), 3), " </Resource>", ]; }
private writeAssignment( assignment: Assignment, durationContext: ReturnType<typeof durationContextFromProject>, ): string[] { return [ " <Assignment>", tag("TaskUID", nullableNumber(assignment.taskUniqueId), 3), tag("ResourceUID", nullableNumber(assignment.resourceUniqueId), 3), tag("Work", formatXsdDuration(assignment.work, durationContext), 3), tag("Units", formatUnits(assignment.units), 3), tag("Start", formatProjectDate(assignment.start), 3), tag("Finish", formatProjectDate(assignment.finish), 3), tag( "ActualWork", formatXsdDuration(assignment.actualWork, durationContext), 3, ), tag( "RemainingWork", formatXsdDuration(assignment.remainingWork, durationContext), 3, ), " </Assignment>", ]; }}
function tag(name: string, value: string | null, indentLevel: number): string { if (value === null || value === "") { return ""; } const indent = " ".repeat(indentLevel); return `${indent}<${name}>${escapeXml(value)}</${name}>`;}
function escapeXml(value: string): string { return value .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'");}
function formatBoolean(value: boolean | null): string | null { return value === null ? null : value ? "1" : "0";}
function nullableNumber(value: number | null): string | null { return value === null ? null : String(value);}
function formatUnits(value: number | null): string | null { return value === null ? null : String(value / 100);}
function formatRelationType(type: RelationType): string { switch (type) { case RelationType.StartToStart: return "1"; case RelationType.FinishToFinish: return "2"; case RelationType.StartToFinish: return "3"; case RelationType.FinishToStart: default: return "0"; }}
function formatResourceType(type: ResourceType): string { switch (type) { case ResourceType.Material: return "0"; case ResourceType.Cost: return "2"; case ResourceType.Work: default: return "1"; }}