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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160import type { MppContainer } from "./Mpp14Reader.ts";
export interface MppVariant { family: "modern"; majorVersion: number; rootPath: string; formatPropsPath: string | null; projectPropsPath: string; summaryInformationPath: string | null; taskTablePath: string; resourceTablePath: string; assignmentTablePath: string; calendarTablePath: string; relationTablePath: string;}
export function detectMppVariant(container: MppContainer): MppVariant { const formatPropsPath = detectFormatPropsPath(container); const rootPath = detectRootPath(container); const majorVersion = parseVersionSuffix(formatPropsPath) ?? inferVersionFromRootPath(rootPath) ?? 14;
if (majorVersion < 14) { throw new Error( `Unsupported MPP file version: detected version ${majorVersion}. ` + `Only MPP14 and later (Microsoft Project 2010+) are supported.`, ); }
return { family: "modern", majorVersion, rootPath, formatPropsPath, projectPropsPath: `${rootPath}/Props`, summaryInformationPath: findFirstPath(container, (path) => path.endsWith("/\u0005SummaryInformation"), ), taskTablePath: `${rootPath}/TBkndTask`, resourceTablePath: `${rootPath}/TBkndRsc`, assignmentTablePath: `${rootPath}/TBkndAssn`, calendarTablePath: `${rootPath}/TBkndCal`, relationTablePath: `${rootPath}/TBkndCons`, };}
function detectFormatPropsPath(container: MppContainer): string | null { return ( [...container.streams.keys()] .filter((path) => /\/Props\d+$/u.test(path)) .sort(compareVersionedPaths) .at(-1) ?? null );}
function detectRootPath(container: MppContainer): string { const candidates = new Map<string, number>();
for (const path of container.streams.keys()) { const match = /^(.*)\/TBknd(Task|Rsc|Assn|Cal|Cons)\/Fixed(?:2)?Data$/u.exec(path); if (!match) { continue; }
const rootPath = match[1]; if (!rootPath) { continue; }
const currentScore = candidates.get(rootPath) ?? 0; candidates.set(rootPath, currentScore + 1); }
const ranked = [...candidates.entries()].sort((left, right) => { if (left[1] !== right[1]) { return left[1] - right[1]; }
const leftVersion = inferVersionFromRootPath(left[0]) ?? -1; const rightVersion = inferVersionFromRootPath(right[0]) ?? -1; if (leftVersion !== rightVersion) { return leftVersion - rightVersion; }
return left[0].localeCompare(right[0]); });
const rootPath = ranked.at(-1)?.[0]; if (!rootPath) { throw new Error( "Unsupported MPP file: no TBknd table streams found. " + "This file may use an older format (MPP8, MPP9, or MPP12). " + "Only MPP14 and later (Microsoft Project 2010+) are supported.", ); }
const requiredTables = [ "TBkndTask", "TBkndRsc", "TBkndAssn", "TBkndCal", "TBkndCons", ]; for (const table of requiredTables) { const fixedDataPath = `${rootPath}/${table}/FixedData`; if (!container.streams.has(fixedDataPath)) { throw new Error( `Unsupported MPP file: missing ${fixedDataPath} under detected root ${rootPath}`, ); } }
return rootPath;}
function findFirstPath( container: MppContainer, predicate: (path: string) => boolean,): string | null { return [...container.streams.keys()].find(predicate) ?? null;}
function compareVersionedPaths(left: string, right: string): number { const leftVersion = parseVersionSuffix(left) ?? -1; const rightVersion = parseVersionSuffix(right) ?? -1; if (leftVersion !== rightVersion) { return leftVersion - rightVersion; } return left.localeCompare(right);}
function parseVersionSuffix(path: string | null): number | null { if (!path) { return null; } const match = /Props(\d+)$/u.exec(path); return match?.[1] ? Number(match[1]) : null;}
function inferVersionFromRootPath(path: string): number | null { const segment = path.split("/").at(-1)?.trim(); if (!segment) { return null; }
const numericValue = Number(segment); if (!Number.isFinite(numericValue)) { return null; }
if (numericValue >= 100 && numericValue < 200) { return numericValue - 100; }
return null;}