@nowline/export-msproj 0.8.6 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -9
- package/dist/calendar.d.ts +6 -1
- package/dist/calendar.d.ts.map +1 -1
- package/dist/calendar.js +54 -10
- package/dist/calendar.js.map +1 -1
- package/dist/duration.d.ts +5 -1
- package/dist/duration.d.ts.map +1 -1
- package/dist/duration.js +12 -43
- package/dist/duration.js.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +227 -94
- package/dist/index.js.map +1 -1
- package/dist/xml.d.ts +7 -0
- package/dist/xml.d.ts.map +1 -1
- package/dist/xml.js +9 -0
- package/dist/xml.js.map +1 -1
- package/package.json +6 -6
- package/src/calendar.ts +72 -10
- package/src/duration.ts +12 -39
- package/src/index.ts +302 -112
- package/src/xml.ts +10 -0
package/src/calendar.ts
CHANGED
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
// Minimal Standard base calendar emitted on every MS Project XML export.
|
|
2
2
|
//
|
|
3
|
-
// Spec: specs/handoffs/m2c.md § 8 + Resolution 6.
|
|
4
|
-
// - One base calendar (UID=1, Name=Standard)
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Spec: specs/handoffs/m2c.md § 8 + Resolution 6, specs/working-calendar.md §8.
|
|
4
|
+
// - One base calendar (UID=1, Name=Standard). Its working weekdays are the
|
|
5
|
+
// file calendar's open week (`WorkingCalendar.workingWeekdays`), each
|
|
6
|
+
// 08:00–12:00 / 13:00–17:00: Mon–Fri for business, which matches
|
|
7
|
+
// Microsoft's default project template, and all seven days for
|
|
8
|
+
// `calendar:full` and `calendar:custom`.
|
|
7
9
|
// - One resource calendar (UID=2, Name=Standard, BaseCalendarUID=1).
|
|
8
10
|
//
|
|
11
|
+
// - `<Exceptions>` in the base calendar (UID=1), right after `</WeekDays>`,
|
|
12
|
+
// one per `CalendarException`: a named closure, a bounded recurrence's
|
|
13
|
+
// closed days, or a working day the open week closes (m2p phase 5b).
|
|
14
|
+
// There is no `<WorkWeeks>`: a bounded recurrence becomes one exception
|
|
15
|
+
// per run of days it changes, and the exceptions never overlap.
|
|
16
|
+
//
|
|
9
17
|
// Calendar UIDs are fixed (1 / 2). No timestamps in the calendar block →
|
|
10
18
|
// deterministic across runs.
|
|
11
19
|
|
|
20
|
+
import type { CalendarException } from '@nowline/layout';
|
|
21
|
+
import { escapeXml } from './xml.js';
|
|
22
|
+
|
|
12
23
|
const DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
13
24
|
|
|
14
|
-
function dayBlock(dayIndex: number): string {
|
|
15
|
-
|
|
16
|
-
if (isWeekend) {
|
|
25
|
+
function dayBlock(dayIndex: number, isWorking: boolean): string {
|
|
26
|
+
if (!isWorking) {
|
|
17
27
|
return ` <WeekDay>
|
|
18
28
|
<DayType>${dayIndex + 1}</DayType>
|
|
19
29
|
<DayWorking>0</DayWorking>
|
|
@@ -35,8 +45,60 @@ function dayBlock(dayIndex: number): string {
|
|
|
35
45
|
</WeekDay>`;
|
|
36
46
|
}
|
|
37
47
|
|
|
38
|
-
|
|
39
|
-
|
|
48
|
+
function isoDate(date: Date): string {
|
|
49
|
+
return date.toISOString().slice(0, 10);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** One `<Exception>`: element order follows the MSPDI schema. */
|
|
53
|
+
function exceptionBlock(exception: CalendarException): string {
|
|
54
|
+
const lines = [
|
|
55
|
+
' <Exception>',
|
|
56
|
+
' <EnteredByOccurrences>0</EnteredByOccurrences>',
|
|
57
|
+
' <TimePeriod>',
|
|
58
|
+
` <FromDate>${isoDate(exception.from)}T00:00:00</FromDate>`,
|
|
59
|
+
` <ToDate>${isoDate(exception.through)}T23:59:00</ToDate>`,
|
|
60
|
+
' </TimePeriod>',
|
|
61
|
+
' <Occurrences>1</Occurrences>',
|
|
62
|
+
];
|
|
63
|
+
if (exception.titles && exception.titles.length > 0) {
|
|
64
|
+
lines.push(` <Name>${escapeXml(exception.titles.join(', '))}</Name>`);
|
|
65
|
+
}
|
|
66
|
+
lines.push(' <Type>1</Type>');
|
|
67
|
+
lines.push(` <DayWorking>${exception.working ? 1 : 0}</DayWorking>`);
|
|
68
|
+
if (exception.working) {
|
|
69
|
+
lines.push(
|
|
70
|
+
' <WorkingTimes>',
|
|
71
|
+
' <WorkingTime>',
|
|
72
|
+
' <FromTime>08:00:00</FromTime>',
|
|
73
|
+
' <ToTime>12:00:00</ToTime>',
|
|
74
|
+
' </WorkingTime>',
|
|
75
|
+
' <WorkingTime>',
|
|
76
|
+
' <FromTime>13:00:00</FromTime>',
|
|
77
|
+
' <ToTime>17:00:00</ToTime>',
|
|
78
|
+
' </WorkingTime>',
|
|
79
|
+
' </WorkingTimes>',
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
lines.push(' </Exception>');
|
|
83
|
+
return lines.join('\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function exceptionsBlock(exceptions: readonly CalendarException[]): string {
|
|
87
|
+
if (exceptions.length === 0) return '';
|
|
88
|
+
return `\n <Exceptions>\n${exceptions.map(exceptionBlock).join('\n')}\n </Exceptions>`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* `workingWeekdays`: 0 = Sunday … 6 = Saturday. `exceptions`: the days the
|
|
93
|
+
* file's calendar differs from that week (`calendarExceptions`), ascending.
|
|
94
|
+
*/
|
|
95
|
+
export function buildCalendarsBlock(
|
|
96
|
+
workingWeekdays: ReadonlySet<number>,
|
|
97
|
+
exceptions: readonly CalendarException[],
|
|
98
|
+
): string {
|
|
99
|
+
const weekDays = [0, 1, 2, 3, 4, 5, 6]
|
|
100
|
+
.map((dayIndex) => dayBlock(dayIndex, workingWeekdays.has(dayIndex)))
|
|
101
|
+
.join('\n');
|
|
40
102
|
return ` <Calendars>
|
|
41
103
|
<Calendar>
|
|
42
104
|
<UID>1</UID>
|
|
@@ -45,7 +107,7 @@ export function buildCalendarsBlock(): string {
|
|
|
45
107
|
<BaseCalendarUID>-1</BaseCalendarUID>
|
|
46
108
|
<WeekDays>
|
|
47
109
|
${weekDays}
|
|
48
|
-
</WeekDays
|
|
110
|
+
</WeekDays>${exceptionsBlock(exceptions)}
|
|
49
111
|
</Calendar>
|
|
50
112
|
<Calendar>
|
|
51
113
|
<UID>2</UID>
|
package/src/duration.ts
CHANGED
|
@@ -1,45 +1,18 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Task durations in MS Project's `PT<minutes>M0S` form.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// Size buckets resolve the same way as in @nowline/export-mermaid.
|
|
8
|
-
|
|
9
|
-
const SIZE_BUCKET_DAYS: Readonly<Record<string, number>> = {
|
|
10
|
-
xs: 1,
|
|
11
|
-
sm: 3,
|
|
12
|
-
s: 3,
|
|
13
|
-
md: 5,
|
|
14
|
-
m: 5,
|
|
15
|
-
lg: 10,
|
|
16
|
-
l: 10,
|
|
17
|
-
xl: 15,
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const NUMERIC_RE = /^(\d+(?:\.\d+)?)\s*(d|w|m|y)?$/i;
|
|
3
|
+
// The day count is engine C's `ScheduledItem.days` under the file's calendar
|
|
4
|
+
// (specs/working-calendar.md §8), so sizes, `capacity:` and `q` match the
|
|
5
|
+
// chart. Each working day is 8 hours of the Standard calendar `calendar.ts`
|
|
6
|
+
// emits.
|
|
21
7
|
|
|
22
|
-
const MINUTES_PER_WORKING_DAY = 8 * 60;
|
|
23
|
-
const WORKING_DAYS_PER_WEEK = 5;
|
|
24
|
-
const WORKING_DAYS_PER_MONTH = 22;
|
|
25
|
-
const WORKING_DAYS_PER_YEAR = 252;
|
|
8
|
+
const MINUTES_PER_WORKING_DAY = 8 * 60;
|
|
26
9
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
const match = NUMERIC_RE.exec(trimmed);
|
|
35
|
-
if (!match) return MINUTES_PER_WORKING_DAY;
|
|
36
|
-
const value = Number(match[1]);
|
|
37
|
-
const unit = (match[2] ?? 'd').toLowerCase();
|
|
38
|
-
if (!Number.isFinite(value) || value <= 0) return MINUTES_PER_WORKING_DAY;
|
|
39
|
-
let days = value;
|
|
40
|
-
if (unit === 'w') days = value * WORKING_DAYS_PER_WEEK;
|
|
41
|
-
else if (unit === 'm') days = value * WORKING_DAYS_PER_MONTH;
|
|
42
|
-
else if (unit === 'y') days = value * WORKING_DAYS_PER_YEAR;
|
|
10
|
+
/**
|
|
11
|
+
* Working minutes for a task of `days` working days. An item with no
|
|
12
|
+
* duration gets one day: a zero-length task is a milestone in MS Project.
|
|
13
|
+
*/
|
|
14
|
+
export function workingDaysToMinutes(days: number | undefined): number {
|
|
15
|
+
if (days === undefined || !(days > 0)) return MINUTES_PER_WORKING_DAY;
|
|
43
16
|
return Math.round(days * MINUTES_PER_WORKING_DAY);
|
|
44
17
|
}
|
|
45
18
|
|