@nowline/layout 0.0.0-dev.20261009042000.g52d5946 → 0.0.0-dev.20261012061520.ge3b2479
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.
|
@@ -4,12 +4,24 @@ import { type WorkingCalendar } from './working-calendar.js';
|
|
|
4
4
|
export interface ResolvedCalendar {
|
|
5
5
|
/** The preset or `calendar` block day counts. */
|
|
6
6
|
config: CalendarConfig;
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* The working calendar of the preset and the file's declarations (its
|
|
9
|
+
* own and its merged includes'). A `non-working` declaration named
|
|
10
|
+
* `weekend` replaces the preset's rules. Its `rules` are in a canonical
|
|
11
|
+
* order, so nothing downstream depends on file or include order.
|
|
12
|
+
*/
|
|
8
13
|
working: WorkingCalendar;
|
|
9
14
|
}
|
|
10
15
|
/**
|
|
11
16
|
* Resolve the calendar a roadmap declares (`calendar:` on the roadmap plus
|
|
12
|
-
* the `config` block's `calendar` section
|
|
17
|
+
* the `config` block's `calendar` section, business when it declares none)
|
|
18
|
+
* and the non-working days its declarations add or take away.
|
|
19
|
+
*
|
|
20
|
+
* A malformed declaration is left out (the validator reports it in the root
|
|
21
|
+
* file; includes are not validated). Rules that leave an unbounded stretch
|
|
22
|
+
* with no working day (an include that closes the whole week; the root file
|
|
23
|
+
* would have had NL.E0906) fall back to the preset calendar rather than
|
|
24
|
+
* failing layout or export.
|
|
13
25
|
*/
|
|
14
26
|
export declare function resolveWorkingCalendar(file: NowlineFile, resolved: ResolveResult): ResolvedCalendar;
|
|
15
27
|
//# sourceMappingURL=calendar-resolver.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"calendar-resolver.d.ts","sourceRoot":"","sources":["../src/calendar-resolver.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"calendar-resolver.d.ts","sourceRoot":"","sources":["../src/calendar-resolver.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAGR,WAAW,EACX,aAAa,EAEhB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,KAAK,cAAc,EAAmB,MAAM,eAAe,CAAC;AAErE,OAAO,EAIH,KAAK,eAAe,EACvB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,gBAAgB;IAC7B,iDAAiD;IACjD,MAAM,EAAE,cAAc,CAAC;IACvB;;;;;OAKG;IACH,OAAO,EAAE,eAAe,CAAC;CAC5B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CAClC,IAAI,EAAE,WAAW,EACjB,QAAQ,EAAE,aAAa,GACxB,gBAAgB,CASlB"}
|
|
@@ -3,17 +3,171 @@
|
|
|
3
3
|
// same `CalendarConfig` (duration arithmetic) and `WorkingCalendar`
|
|
4
4
|
// (non-working days), so the chart and every export agree on what `1w` is.
|
|
5
5
|
//
|
|
6
|
+
// It is also the one place the file's `non-working` and `working`
|
|
7
|
+
// declarations (its own and its merged includes', specs/working-calendar.md
|
|
8
|
+
// §4) become `CalendarRule`s, so every consumer picks them up unchanged.
|
|
9
|
+
//
|
|
6
10
|
// It lives apart from `calendar.ts` because `working-calendar.ts` already
|
|
7
11
|
// imports values from there; building a `WorkingCalendar` in `calendar.ts`
|
|
8
12
|
// would close a value-import cycle.
|
|
9
13
|
import { resolveCalendar } from './calendar.js';
|
|
10
|
-
import {
|
|
14
|
+
import { formatIsoDate, parseDate, propValues } from './dsl-utils.js';
|
|
15
|
+
import { fromCalendarConfig, presetRules, } from './working-calendar.js';
|
|
11
16
|
/**
|
|
12
17
|
* Resolve the calendar a roadmap declares (`calendar:` on the roadmap plus
|
|
13
|
-
* the `config` block's `calendar` section
|
|
18
|
+
* the `config` block's `calendar` section, business when it declares none)
|
|
19
|
+
* and the non-working days its declarations add or take away.
|
|
20
|
+
*
|
|
21
|
+
* A malformed declaration is left out (the validator reports it in the root
|
|
22
|
+
* file; includes are not validated). Rules that leave an unbounded stretch
|
|
23
|
+
* with no working day (an include that closes the whole week; the root file
|
|
24
|
+
* would have had NL.E0906) fall back to the preset calendar rather than
|
|
25
|
+
* failing layout or export.
|
|
14
26
|
*/
|
|
15
27
|
export function resolveWorkingCalendar(file, resolved) {
|
|
16
28
|
const config = resolveCalendar(file, resolved.config.calendar);
|
|
17
|
-
|
|
29
|
+
const rules = calendarRules(config, resolved.config.calendarDeclarations);
|
|
30
|
+
try {
|
|
31
|
+
return { config, working: fromCalendarConfig(config, rules) };
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
if (!(err instanceof RangeError))
|
|
35
|
+
throw err;
|
|
36
|
+
return { config, working: fromCalendarConfig(config) };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** UTC weekday numbers (Sunday is 0) of the names `every:` takes. */
|
|
40
|
+
const WEEKDAY_INDEX = {
|
|
41
|
+
sun: 0,
|
|
42
|
+
mon: 1,
|
|
43
|
+
tue: 2,
|
|
44
|
+
wed: 3,
|
|
45
|
+
thu: 4,
|
|
46
|
+
fri: 5,
|
|
47
|
+
sat: 6,
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The preset's rules, unless a `non-working` declaration is named `weekend`
|
|
51
|
+
* (decision 14), plus one rule per well-formed declaration, in canonical
|
|
52
|
+
* order.
|
|
53
|
+
*/
|
|
54
|
+
function calendarRules(config, declarations) {
|
|
55
|
+
const replacesWeekend = declarations.some((decl) => decl.$type === 'NonWorkingDeclaration' && decl.name === 'weekend');
|
|
56
|
+
const rules = replacesWeekend ? [] : presetRules(config.mode);
|
|
57
|
+
for (const decl of declarations) {
|
|
58
|
+
const rule = declarationRule(decl);
|
|
59
|
+
if (rule)
|
|
60
|
+
rules.push(rule);
|
|
61
|
+
}
|
|
62
|
+
return rules.sort(compareRules);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The rule one declaration stands for, or undefined when its shape or a
|
|
66
|
+
* value is malformed. Only the first `date:`, `start:`, `end:` and `every:`
|
|
67
|
+
* count; any other key is the validator's NL.E0901 and changes nothing here.
|
|
68
|
+
*/
|
|
69
|
+
function declarationRule(decl) {
|
|
70
|
+
const props = decl.properties;
|
|
71
|
+
const has = (key) => props.some((p) => stripColon(p.key) === key);
|
|
72
|
+
const rule = { working: decl.$type === 'WorkingDeclaration' };
|
|
73
|
+
if (decl.name)
|
|
74
|
+
rule.id = decl.name;
|
|
75
|
+
if (decl.title !== undefined)
|
|
76
|
+
rule.title = decl.title;
|
|
77
|
+
if (has('date')) {
|
|
78
|
+
if (has('every') || has('start') || has('end'))
|
|
79
|
+
return undefined;
|
|
80
|
+
const dates = propValues(props, 'date').map(realDate);
|
|
81
|
+
if (dates.length === 0 || dates.some((date) => date === undefined))
|
|
82
|
+
return undefined;
|
|
83
|
+
rule.dates = dates;
|
|
84
|
+
return rule;
|
|
85
|
+
}
|
|
86
|
+
if (!has('every') && !(has('start') && has('end')))
|
|
87
|
+
return undefined;
|
|
88
|
+
for (const key of ['start', 'end']) {
|
|
89
|
+
if (!has(key))
|
|
90
|
+
continue;
|
|
91
|
+
const date = singleDate(props, key);
|
|
92
|
+
if (!date)
|
|
93
|
+
return undefined;
|
|
94
|
+
rule[key] = date;
|
|
95
|
+
}
|
|
96
|
+
if (rule.start && rule.end && rule.end < rule.start)
|
|
97
|
+
return undefined;
|
|
98
|
+
if (has('every')) {
|
|
99
|
+
const weekdays = propValues(props, 'every').map((name) => WEEKDAY_INDEX[name]);
|
|
100
|
+
if (weekdays.length === 0 || weekdays.some((day) => day === undefined))
|
|
101
|
+
return undefined;
|
|
102
|
+
rule.every = new Set(weekdays);
|
|
103
|
+
}
|
|
104
|
+
return rule;
|
|
105
|
+
}
|
|
106
|
+
/** The one date a `start:` / `end:` names; a one-element bracket is that date. */
|
|
107
|
+
function singleDate(props, key) {
|
|
108
|
+
const values = propValues(props, key);
|
|
109
|
+
return values.length === 1 ? realDate(values[0]) : undefined;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* A `YYYY-MM-DD` that names a real calendar day, at UTC midnight. `parseDate`
|
|
113
|
+
* rolls `2026-02-30` over to March 2; the validator rejects it (NL.E0405), so
|
|
114
|
+
* the resolver does too.
|
|
115
|
+
*/
|
|
116
|
+
function realDate(raw) {
|
|
117
|
+
const date = parseDate(raw);
|
|
118
|
+
return date && formatIsoDate(date) === raw ? date : undefined;
|
|
119
|
+
}
|
|
120
|
+
function stripColon(key) {
|
|
121
|
+
return key.endsWith(':') ? key.slice(0, -1) : key;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Canonical rule order (decision 18): first date (none first), then title,
|
|
125
|
+
* id, `working`, the `every` mask, start and end, then the remaining dates.
|
|
126
|
+
* The engine breaks ties between titled runs by input order, so sorting
|
|
127
|
+
* here keeps `rules`, run titles and every downstream list independent of
|
|
128
|
+
* file and include order.
|
|
129
|
+
*/
|
|
130
|
+
function compareRules(a, b) {
|
|
131
|
+
return (compareOptional(firstDay(a), firstDay(b)) ||
|
|
132
|
+
compareOptional(a.title, b.title) ||
|
|
133
|
+
compareOptional(a.id, b.id) ||
|
|
134
|
+
Number(a.working) - Number(b.working) ||
|
|
135
|
+
compareOptional(everyMask(a), everyMask(b)) ||
|
|
136
|
+
compareOptional(a.start?.getTime(), b.start?.getTime()) ||
|
|
137
|
+
compareOptional(a.end?.getTime(), b.end?.getTime()) ||
|
|
138
|
+
compareDates(a.dates ?? [], b.dates ?? []));
|
|
139
|
+
}
|
|
140
|
+
/** The earliest date a rule names (its first `date:` or its `start:`); undefined for none. */
|
|
141
|
+
function firstDay(rule) {
|
|
142
|
+
if (rule.dates)
|
|
143
|
+
return Math.min(...rule.dates.map((date) => date.getTime()));
|
|
144
|
+
return rule.start?.getTime();
|
|
145
|
+
}
|
|
146
|
+
function everyMask(rule) {
|
|
147
|
+
if (!rule.every)
|
|
148
|
+
return undefined;
|
|
149
|
+
let mask = 0;
|
|
150
|
+
for (const weekday of rule.every)
|
|
151
|
+
mask |= 1 << weekday;
|
|
152
|
+
return mask;
|
|
153
|
+
}
|
|
154
|
+
/** Undefined sorts first; strings compare by code unit, never by locale. */
|
|
155
|
+
function compareOptional(a, b) {
|
|
156
|
+
if (a === b)
|
|
157
|
+
return 0;
|
|
158
|
+
if (a === undefined)
|
|
159
|
+
return -1;
|
|
160
|
+
if (b === undefined)
|
|
161
|
+
return 1;
|
|
162
|
+
return a < b ? -1 : 1;
|
|
163
|
+
}
|
|
164
|
+
function compareDates(a, b) {
|
|
165
|
+
const left = a.map((date) => date.getTime()).sort((x, y) => x - y);
|
|
166
|
+
const right = b.map((date) => date.getTime()).sort((x, y) => x - y);
|
|
167
|
+
for (let i = 0; i < Math.min(left.length, right.length); i++) {
|
|
168
|
+
if (left[i] !== right[i])
|
|
169
|
+
return left[i] - right[i];
|
|
170
|
+
}
|
|
171
|
+
return left.length - right.length;
|
|
18
172
|
}
|
|
19
173
|
//# sourceMappingURL=calendar-resolver.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"calendar-resolver.js","sourceRoot":"","sources":["../src/calendar-resolver.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,yEAAyE;AACzE,oEAAoE;AACpE,2EAA2E;AAC3E,EAAE;AACF,0EAA0E;AAC1E,2EAA2E;AAC3E,oCAAoC;
|
|
1
|
+
{"version":3,"file":"calendar-resolver.js","sourceRoot":"","sources":["../src/calendar-resolver.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowline/layout",
|
|
3
|
-
"version": "0.0.0-dev.
|
|
3
|
+
"version": "0.0.0-dev.20261012061520.ge3b2479",
|
|
4
4
|
"description": "Nowline layout engine — works out where everything goes on the page before it's drawn.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"engines": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"src/"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@nowline/core": "0.0.0-dev.
|
|
25
|
+
"@nowline/core": "0.0.0-dev.20261012061520.ge3b2479",
|
|
26
26
|
"d3-scale": "^4.0.2",
|
|
27
27
|
"d3-time": "^3.1.0"
|
|
28
28
|
},
|
package/src/calendar-resolver.ts
CHANGED
|
@@ -3,29 +3,203 @@
|
|
|
3
3
|
// same `CalendarConfig` (duration arithmetic) and `WorkingCalendar`
|
|
4
4
|
// (non-working days), so the chart and every export agree on what `1w` is.
|
|
5
5
|
//
|
|
6
|
+
// It is also the one place the file's `non-working` and `working`
|
|
7
|
+
// declarations (its own and its merged includes', specs/working-calendar.md
|
|
8
|
+
// §4) become `CalendarRule`s, so every consumer picks them up unchanged.
|
|
9
|
+
//
|
|
6
10
|
// It lives apart from `calendar.ts` because `working-calendar.ts` already
|
|
7
11
|
// imports values from there; building a `WorkingCalendar` in `calendar.ts`
|
|
8
12
|
// would close a value-import cycle.
|
|
9
13
|
|
|
10
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
EntityProperty,
|
|
16
|
+
NonWorkingDeclaration,
|
|
17
|
+
NowlineFile,
|
|
18
|
+
ResolveResult,
|
|
19
|
+
WorkingDeclaration,
|
|
20
|
+
} from '@nowline/core';
|
|
11
21
|
import { type CalendarConfig, resolveCalendar } from './calendar.js';
|
|
12
|
-
import {
|
|
22
|
+
import { formatIsoDate, parseDate, propValues } from './dsl-utils.js';
|
|
23
|
+
import {
|
|
24
|
+
type CalendarRule,
|
|
25
|
+
fromCalendarConfig,
|
|
26
|
+
presetRules,
|
|
27
|
+
type WorkingCalendar,
|
|
28
|
+
} from './working-calendar.js';
|
|
13
29
|
|
|
14
30
|
export interface ResolvedCalendar {
|
|
15
31
|
/** The preset or `calendar` block day counts. */
|
|
16
32
|
config: CalendarConfig;
|
|
17
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* The working calendar of the preset and the file's declarations (its
|
|
35
|
+
* own and its merged includes'). A `non-working` declaration named
|
|
36
|
+
* `weekend` replaces the preset's rules. Its `rules` are in a canonical
|
|
37
|
+
* order, so nothing downstream depends on file or include order.
|
|
38
|
+
*/
|
|
18
39
|
working: WorkingCalendar;
|
|
19
40
|
}
|
|
20
41
|
|
|
21
42
|
/**
|
|
22
43
|
* Resolve the calendar a roadmap declares (`calendar:` on the roadmap plus
|
|
23
|
-
* the `config` block's `calendar` section
|
|
44
|
+
* the `config` block's `calendar` section, business when it declares none)
|
|
45
|
+
* and the non-working days its declarations add or take away.
|
|
46
|
+
*
|
|
47
|
+
* A malformed declaration is left out (the validator reports it in the root
|
|
48
|
+
* file; includes are not validated). Rules that leave an unbounded stretch
|
|
49
|
+
* with no working day (an include that closes the whole week; the root file
|
|
50
|
+
* would have had NL.E0906) fall back to the preset calendar rather than
|
|
51
|
+
* failing layout or export.
|
|
24
52
|
*/
|
|
25
53
|
export function resolveWorkingCalendar(
|
|
26
54
|
file: NowlineFile,
|
|
27
55
|
resolved: ResolveResult,
|
|
28
56
|
): ResolvedCalendar {
|
|
29
57
|
const config = resolveCalendar(file, resolved.config.calendar);
|
|
30
|
-
|
|
58
|
+
const rules = calendarRules(config, resolved.config.calendarDeclarations);
|
|
59
|
+
try {
|
|
60
|
+
return { config, working: fromCalendarConfig(config, rules) };
|
|
61
|
+
} catch (err) {
|
|
62
|
+
if (!(err instanceof RangeError)) throw err;
|
|
63
|
+
return { config, working: fromCalendarConfig(config) };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type CalendarDeclaration = NonWorkingDeclaration | WorkingDeclaration;
|
|
68
|
+
|
|
69
|
+
/** UTC weekday numbers (Sunday is 0) of the names `every:` takes. */
|
|
70
|
+
const WEEKDAY_INDEX: Readonly<Record<string, number>> = {
|
|
71
|
+
sun: 0,
|
|
72
|
+
mon: 1,
|
|
73
|
+
tue: 2,
|
|
74
|
+
wed: 3,
|
|
75
|
+
thu: 4,
|
|
76
|
+
fri: 5,
|
|
77
|
+
sat: 6,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The preset's rules, unless a `non-working` declaration is named `weekend`
|
|
82
|
+
* (decision 14), plus one rule per well-formed declaration, in canonical
|
|
83
|
+
* order.
|
|
84
|
+
*/
|
|
85
|
+
function calendarRules(
|
|
86
|
+
config: CalendarConfig,
|
|
87
|
+
declarations: ReadonlyArray<CalendarDeclaration>,
|
|
88
|
+
): CalendarRule[] {
|
|
89
|
+
const replacesWeekend = declarations.some(
|
|
90
|
+
(decl) => decl.$type === 'NonWorkingDeclaration' && decl.name === 'weekend',
|
|
91
|
+
);
|
|
92
|
+
const rules = replacesWeekend ? [] : presetRules(config.mode);
|
|
93
|
+
for (const decl of declarations) {
|
|
94
|
+
const rule = declarationRule(decl);
|
|
95
|
+
if (rule) rules.push(rule);
|
|
96
|
+
}
|
|
97
|
+
return rules.sort(compareRules);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The rule one declaration stands for, or undefined when its shape or a
|
|
102
|
+
* value is malformed. Only the first `date:`, `start:`, `end:` and `every:`
|
|
103
|
+
* count; any other key is the validator's NL.E0901 and changes nothing here.
|
|
104
|
+
*/
|
|
105
|
+
function declarationRule(decl: CalendarDeclaration): CalendarRule | undefined {
|
|
106
|
+
const props = decl.properties;
|
|
107
|
+
const has = (key: string) => props.some((p) => stripColon(p.key) === key);
|
|
108
|
+
const rule: CalendarRule = { working: decl.$type === 'WorkingDeclaration' };
|
|
109
|
+
if (decl.name) rule.id = decl.name;
|
|
110
|
+
if (decl.title !== undefined) rule.title = decl.title;
|
|
111
|
+
|
|
112
|
+
if (has('date')) {
|
|
113
|
+
if (has('every') || has('start') || has('end')) return undefined;
|
|
114
|
+
const dates = propValues(props, 'date').map(realDate);
|
|
115
|
+
if (dates.length === 0 || dates.some((date) => date === undefined)) return undefined;
|
|
116
|
+
rule.dates = dates as Date[];
|
|
117
|
+
return rule;
|
|
118
|
+
}
|
|
119
|
+
if (!has('every') && !(has('start') && has('end'))) return undefined;
|
|
120
|
+
|
|
121
|
+
for (const key of ['start', 'end'] as const) {
|
|
122
|
+
if (!has(key)) continue;
|
|
123
|
+
const date = singleDate(props, key);
|
|
124
|
+
if (!date) return undefined;
|
|
125
|
+
rule[key] = date;
|
|
126
|
+
}
|
|
127
|
+
if (rule.start && rule.end && rule.end < rule.start) return undefined;
|
|
128
|
+
|
|
129
|
+
if (has('every')) {
|
|
130
|
+
const weekdays = propValues(props, 'every').map((name) => WEEKDAY_INDEX[name]);
|
|
131
|
+
if (weekdays.length === 0 || weekdays.some((day) => day === undefined)) return undefined;
|
|
132
|
+
rule.every = new Set(weekdays);
|
|
133
|
+
}
|
|
134
|
+
return rule;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The one date a `start:` / `end:` names; a one-element bracket is that date. */
|
|
138
|
+
function singleDate(props: EntityProperty[], key: 'start' | 'end'): Date | undefined {
|
|
139
|
+
const values = propValues(props, key);
|
|
140
|
+
return values.length === 1 ? realDate(values[0]) : undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A `YYYY-MM-DD` that names a real calendar day, at UTC midnight. `parseDate`
|
|
145
|
+
* rolls `2026-02-30` over to March 2; the validator rejects it (NL.E0405), so
|
|
146
|
+
* the resolver does too.
|
|
147
|
+
*/
|
|
148
|
+
function realDate(raw: string): Date | undefined {
|
|
149
|
+
const date = parseDate(raw);
|
|
150
|
+
return date && formatIsoDate(date) === raw ? date : undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function stripColon(key: string): string {
|
|
154
|
+
return key.endsWith(':') ? key.slice(0, -1) : key;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Canonical rule order (decision 18): first date (none first), then title,
|
|
159
|
+
* id, `working`, the `every` mask, start and end, then the remaining dates.
|
|
160
|
+
* The engine breaks ties between titled runs by input order, so sorting
|
|
161
|
+
* here keeps `rules`, run titles and every downstream list independent of
|
|
162
|
+
* file and include order.
|
|
163
|
+
*/
|
|
164
|
+
function compareRules(a: CalendarRule, b: CalendarRule): number {
|
|
165
|
+
return (
|
|
166
|
+
compareOptional(firstDay(a), firstDay(b)) ||
|
|
167
|
+
compareOptional(a.title, b.title) ||
|
|
168
|
+
compareOptional(a.id, b.id) ||
|
|
169
|
+
Number(a.working) - Number(b.working) ||
|
|
170
|
+
compareOptional(everyMask(a), everyMask(b)) ||
|
|
171
|
+
compareOptional(a.start?.getTime(), b.start?.getTime()) ||
|
|
172
|
+
compareOptional(a.end?.getTime(), b.end?.getTime()) ||
|
|
173
|
+
compareDates(a.dates ?? [], b.dates ?? [])
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The earliest date a rule names (its first `date:` or its `start:`); undefined for none. */
|
|
178
|
+
function firstDay(rule: CalendarRule): number | undefined {
|
|
179
|
+
if (rule.dates) return Math.min(...rule.dates.map((date) => date.getTime()));
|
|
180
|
+
return rule.start?.getTime();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function everyMask(rule: CalendarRule): number | undefined {
|
|
184
|
+
if (!rule.every) return undefined;
|
|
185
|
+
let mask = 0;
|
|
186
|
+
for (const weekday of rule.every) mask |= 1 << weekday;
|
|
187
|
+
return mask;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Undefined sorts first; strings compare by code unit, never by locale. */
|
|
191
|
+
function compareOptional<T extends number | string>(a: T | undefined, b: T | undefined): number {
|
|
192
|
+
if (a === b) return 0;
|
|
193
|
+
if (a === undefined) return -1;
|
|
194
|
+
if (b === undefined) return 1;
|
|
195
|
+
return a < b ? -1 : 1;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function compareDates(a: ReadonlyArray<Date>, b: ReadonlyArray<Date>): number {
|
|
199
|
+
const left = a.map((date) => date.getTime()).sort((x, y) => x - y);
|
|
200
|
+
const right = b.map((date) => date.getTime()).sort((x, y) => x - y);
|
|
201
|
+
for (let i = 0; i < Math.min(left.length, right.length); i++) {
|
|
202
|
+
if (left[i] !== right[i]) return left[i] - right[i];
|
|
203
|
+
}
|
|
204
|
+
return left.length - right.length;
|
|
31
205
|
}
|