@kensio/quando 0.1.0 → 1.0.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 +135 -4
- package/dist/assigned.d.ts +68 -0
- package/dist/assigned.d.ts.map +1 -0
- package/dist/assigned.js +84 -0
- package/dist/assigned.js.map +1 -0
- package/dist/build.d.ts +54 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +77 -0
- package/dist/build.js.map +1 -0
- package/dist/canonical-rule.d.ts +23 -0
- package/dist/canonical-rule.d.ts.map +1 -0
- package/dist/canonical-rule.js +139 -0
- package/dist/canonical-rule.js.map +1 -0
- package/dist/canonical.d.ts +44 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +53 -0
- package/dist/canonical.js.map +1 -0
- package/dist/cascade.d.ts +107 -0
- package/dist/cascade.d.ts.map +1 -0
- package/dist/cascade.js +53 -0
- package/dist/cascade.js.map +1 -0
- package/dist/context.d.ts +44 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +9 -0
- package/dist/context.js.map +1 -0
- package/dist/day-rules.d.ts +23 -0
- package/dist/day-rules.d.ts.map +1 -0
- package/dist/day-rules.js +102 -0
- package/dist/day-rules.js.map +1 -0
- package/dist/index.d.ts +37 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -2
- package/dist/index.js.map +1 -1
- package/dist/interpret.d.ts +31 -0
- package/dist/interpret.d.ts.map +1 -0
- package/dist/interpret.js +99 -0
- package/dist/interpret.js.map +1 -0
- package/dist/interval-stream.d.ts +11 -4
- package/dist/interval-stream.d.ts.map +1 -1
- package/dist/interval-stream.js.map +1 -1
- package/dist/merge.d.ts +43 -0
- package/dist/merge.d.ts.map +1 -0
- package/dist/merge.js +79 -0
- package/dist/merge.js.map +1 -0
- package/dist/parse-cascade.d.ts +37 -0
- package/dist/parse-cascade.d.ts.map +1 -0
- package/dist/parse-cascade.js +105 -0
- package/dist/parse-cascade.js.map +1 -0
- package/dist/parse-fields.d.ts +27 -0
- package/dist/parse-fields.d.ts.map +1 -0
- package/dist/parse-fields.js +65 -0
- package/dist/parse-fields.js.map +1 -0
- package/dist/parse-shape.d.ts +39 -0
- package/dist/parse-shape.d.ts.map +1 -0
- package/dist/parse-shape.js +77 -0
- package/dist/parse-shape.js.map +1 -0
- package/dist/parse.d.ts +22 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +93 -0
- package/dist/parse.js.map +1 -0
- package/dist/plain-forms.d.ts +22 -0
- package/dist/plain-forms.d.ts.map +1 -0
- package/dist/plain-forms.js +54 -0
- package/dist/plain-forms.js.map +1 -0
- package/dist/query.d.ts +73 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +127 -0
- package/dist/query.js.map +1 -0
- package/dist/resolve.d.ts +31 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +95 -0
- package/dist/resolve.js.map +1 -0
- package/dist/rota.d.ts +49 -0
- package/dist/rota.d.ts.map +1 -0
- package/dist/rota.js +41 -0
- package/dist/rota.js.map +1 -0
- package/dist/rule.d.ts +73 -0
- package/dist/rule.d.ts.map +1 -0
- package/dist/rule.js +20 -0
- package/dist/rule.js.map +1 -0
- package/dist/schedule.d.ts +69 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +80 -0
- package/dist/schedule.js.map +1 -0
- package/dist/tally.d.ts +76 -0
- package/dist/tally.d.ts.map +1 -0
- package/dist/tally.js +80 -0
- package/dist/tally.js.map +1 -0
- package/dist/time-rules.d.ts +18 -0
- package/dist/time-rules.d.ts.map +1 -0
- package/dist/time-rules.js +55 -0
- package/dist/time-rules.js.map +1 -0
- package/dist/valued-stream.d.ts +51 -0
- package/dist/valued-stream.d.ts.map +1 -0
- package/dist/valued-stream.js +148 -0
- package/dist/valued-stream.js.map +1 -0
- package/package.json +22 -20
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checking that incoming JSON is the shape it claims to be.
|
|
3
|
+
*
|
|
4
|
+
* Nothing here knows anything about time. These are the checks any document
|
|
5
|
+
* needs on the way in, and they say what was found instead and where. The
|
|
6
|
+
* `path` threaded through them is what turns "not a string" into
|
|
7
|
+
* `rule.rules[1].rules[0].dates[0]: expected a string, found number`.
|
|
8
|
+
*
|
|
9
|
+
* [parse-fields.ts](./parse-fields.ts) sits on top with the checks that do
|
|
10
|
+
* know about time, such as days of the week and zones.
|
|
11
|
+
*/
|
|
12
|
+
export function fail(path, problem) {
|
|
13
|
+
throw new TypeError(`${path}: ${problem}`);
|
|
14
|
+
}
|
|
15
|
+
/** What a value looks like, for an error message. */
|
|
16
|
+
export function shapeOf(value) {
|
|
17
|
+
if (value === null) {
|
|
18
|
+
return "null";
|
|
19
|
+
}
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
return "an array";
|
|
22
|
+
}
|
|
23
|
+
return typeof value;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* `expected` names what should have been there, and reads straight into the
|
|
27
|
+
* message. "a rule object", "a layer object".
|
|
28
|
+
*/
|
|
29
|
+
export function asRecord(value, path, expected) {
|
|
30
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
31
|
+
return fail(path, `expected ${expected}, found ${shapeOf(value)}`);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Refuses a field the thing being parsed does not have.
|
|
37
|
+
*
|
|
38
|
+
* Quietly ignoring one is the worse option by some distance. A rule document
|
|
39
|
+
* carrying `"zonee"` would parse as a perfectly valid rule with no zone, which
|
|
40
|
+
* is a *different schedule* read in whatever zone the query happened to use,
|
|
41
|
+
* and nothing would have said so.
|
|
42
|
+
*
|
|
43
|
+
* The cost is that a document written by a later version of Quando, carrying a
|
|
44
|
+
* field this one has not heard of, is rejected rather than tolerated. That is
|
|
45
|
+
* the right way round. A field exists to change what a document means, so
|
|
46
|
+
* ignoring an unknown one is agreeing to get the answer wrong quietly.
|
|
47
|
+
*
|
|
48
|
+
* `type` is exempt, because every caller has already read it to get here.
|
|
49
|
+
*/
|
|
50
|
+
export function checkFields(node, allowed, path, what) {
|
|
51
|
+
for (const field of Object.keys(node)) {
|
|
52
|
+
if (field !== "type" && !allowed.includes(field)) {
|
|
53
|
+
fail(`${path}.${field}`, allowed.length === 0
|
|
54
|
+
? `is not a field of ${what}, which takes none`
|
|
55
|
+
: `is not a field of ${what}. Expected ${allowed.join(", ")}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function asString(value, path) {
|
|
60
|
+
return typeof value === "string"
|
|
61
|
+
? value
|
|
62
|
+
: fail(path, `expected a string, found ${shapeOf(value)}`);
|
|
63
|
+
}
|
|
64
|
+
export function asBoolean(value, path) {
|
|
65
|
+
return typeof value === "boolean"
|
|
66
|
+
? value
|
|
67
|
+
: fail(path, `expected a boolean, found ${shapeOf(value)}`);
|
|
68
|
+
}
|
|
69
|
+
export function asStrings(value, path) {
|
|
70
|
+
if (!Array.isArray(value)) {
|
|
71
|
+
return fail(path, `expected an array, found ${shapeOf(value)}`);
|
|
72
|
+
}
|
|
73
|
+
return value.map((item, index) => typeof item === "string"
|
|
74
|
+
? item
|
|
75
|
+
: fail(`${path}[${index}]`, `expected a string, found ${shapeOf(item)}`));
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=parse-shape.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-shape.js","sourceRoot":"","sources":["../src/parse-shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,UAAU,IAAI,CAAC,IAAY,EAAE,OAAe;IAChD,MAAM,IAAI,SAAS,CAAC,GAAG,IAAI,KAAK,OAAO,EAAE,CAAC,CAAC;AAC7C,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,OAAO,CAAC,KAAc;IACpC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,UAAU,CAAC;IACpB,CAAC;IACD,OAAO,OAAO,KAAK,CAAC;AACtB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,QAAQ,CACtB,KAAc,EACd,IAAY,EACZ,QAAgB;IAEhB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACxE,OAAO,IAAI,CAAC,IAAI,EAAE,YAAY,QAAQ,WAAW,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACrE,CAAC;IACD,OAAO,KAAgC,CAAC;AAC1C,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,WAAW,CACzB,IAA6B,EAC7B,OAA0B,EAC1B,IAAY,EACZ,IAAY;IAEZ,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACtC,IAAI,KAAK,KAAK,MAAM,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YACjD,IAAI,CACF,GAAG,IAAI,IAAI,KAAK,EAAE,EAClB,OAAO,CAAC,MAAM,KAAK,CAAC;gBAClB,CAAC,CAAC,qBAAqB,IAAI,oBAAoB;gBAC/C,CAAC,CAAC,qBAAqB,IAAI,cAAc,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAChE,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,KAAc,EAAE,IAAY;IACnD,OAAO,OAAO,KAAK,KAAK,QAAQ;QAC9B,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,4BAA4B,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;AAC/D,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAc,EAAE,IAAY;IACpD,OAAO,OAAO,KAAK,KAAK,SAAS;QAC/B,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,6BAA6B,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;AAChE,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAc,EAAE,IAAY;IACpD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC,IAAI,EAAE,4BAA4B,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAC/B,OAAO,IAAI,KAAK,QAAQ;QACtB,CAAC,CAAC,IAAI;QACN,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,IAAI,KAAK,GAAG,EAAE,4BAA4B,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAC3E,CAAC;AACJ,CAAC"}
|
package/dist/parse.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning arbitrary JSON back into a rule.
|
|
3
|
+
*
|
|
4
|
+
* This is the boundary. Rules are meant to be stored, sent and edited, so what
|
|
5
|
+
* comes back is whatever a database row, an API body or a form actually holds
|
|
6
|
+
* — and the useful thing to do with a bad one is say precisely what is wrong
|
|
7
|
+
* and where, rather than fail somewhere further in with the reason lost.
|
|
8
|
+
*
|
|
9
|
+
* Shape and vocabulary are checked here: is it an object, is the type one that
|
|
10
|
+
* exists, are those really days of the week, does that parse as a time. What a
|
|
11
|
+
* rule *means* is not — `interpret.ts` owns that, and duplicating it here would
|
|
12
|
+
* only give the two somewhere to disagree.
|
|
13
|
+
*/
|
|
14
|
+
import type { Rule } from "./rule.js";
|
|
15
|
+
/**
|
|
16
|
+
* A rule from unknown JSON, or a `TypeError` saying what is wrong and where.
|
|
17
|
+
*
|
|
18
|
+
* The `path` is what appears in front of every message, so a rule nested six
|
|
19
|
+
* deep reports as `rule.rules[2].rules[0].days[3]` rather than as a puzzle.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseRule(value: unknown, path?: string): Rule;
|
|
22
|
+
//# sourceMappingURL=parse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAwBtC;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,SAAS,GAAG,IAAI,CA+D7D"}
|
package/dist/parse.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning arbitrary JSON back into a rule.
|
|
3
|
+
*
|
|
4
|
+
* This is the boundary. Rules are meant to be stored, sent and edited, so what
|
|
5
|
+
* comes back is whatever a database row, an API body or a form actually holds
|
|
6
|
+
* — and the useful thing to do with a bad one is say precisely what is wrong
|
|
7
|
+
* and where, rather than fail somewhere further in with the reason lost.
|
|
8
|
+
*
|
|
9
|
+
* Shape and vocabulary are checked here: is it an object, is the type one that
|
|
10
|
+
* exists, are those really days of the week, does that parse as a time. What a
|
|
11
|
+
* rule *means* is not — `interpret.ts` owns that, and duplicating it here would
|
|
12
|
+
* only give the two somewhere to disagree.
|
|
13
|
+
*/
|
|
14
|
+
import { asDates, asDays, asTime, zonePart } from "./parse-fields.js";
|
|
15
|
+
import { asRecord, checkFields, fail, shapeOf } from "./parse-shape.js";
|
|
16
|
+
/**
|
|
17
|
+
* Every rule type, and the fields it is allowed to carry. One table rather than
|
|
18
|
+
* two, so the list of known types and the list of known fields cannot drift.
|
|
19
|
+
*/
|
|
20
|
+
const FIELDS = new Map([
|
|
21
|
+
["always", []],
|
|
22
|
+
["never", []],
|
|
23
|
+
["daysOfWeek", ["days", "zone"]],
|
|
24
|
+
["timeOfDay", ["from", "to", "zone"]],
|
|
25
|
+
["dates", ["dates", "zone"]],
|
|
26
|
+
["all", ["rules"]],
|
|
27
|
+
["any", ["rules"]],
|
|
28
|
+
["not", ["rule"]],
|
|
29
|
+
]);
|
|
30
|
+
function asRules(value, path) {
|
|
31
|
+
if (!Array.isArray(value)) {
|
|
32
|
+
return fail(path, `expected an array of rules, found ${shapeOf(value)}`);
|
|
33
|
+
}
|
|
34
|
+
return value.map((rule, index) => parseRule(rule, `${path}[${index}]`));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A rule from unknown JSON, or a `TypeError` saying what is wrong and where.
|
|
38
|
+
*
|
|
39
|
+
* The `path` is what appears in front of every message, so a rule nested six
|
|
40
|
+
* deep reports as `rule.rules[2].rules[0].days[3]` rather than as a puzzle.
|
|
41
|
+
*/
|
|
42
|
+
export function parseRule(value, path = "rule") {
|
|
43
|
+
const node = asRecord(value, path, "a rule object");
|
|
44
|
+
const type = node["type"];
|
|
45
|
+
if (typeof type !== "string") {
|
|
46
|
+
return fail(`${path}.type`, `expected a string, found ${shapeOf(type)}`);
|
|
47
|
+
}
|
|
48
|
+
const allowed = FIELDS.get(type);
|
|
49
|
+
if (allowed === undefined) {
|
|
50
|
+
return fail(`${path}.type`, `"${type}" is not a rule type. Expected one of ${[...FIELDS.keys()].join(", ")}`);
|
|
51
|
+
}
|
|
52
|
+
checkFields(node, allowed, path, `a ${type} rule`);
|
|
53
|
+
switch (type) {
|
|
54
|
+
case "always": {
|
|
55
|
+
return { type: "always" };
|
|
56
|
+
}
|
|
57
|
+
case "never": {
|
|
58
|
+
return { type: "never" };
|
|
59
|
+
}
|
|
60
|
+
case "daysOfWeek": {
|
|
61
|
+
return {
|
|
62
|
+
type: "daysOfWeek",
|
|
63
|
+
days: asDays(node["days"], `${path}.days`),
|
|
64
|
+
...zonePart(node, path),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
case "dates": {
|
|
68
|
+
return {
|
|
69
|
+
type: "dates",
|
|
70
|
+
dates: asDates(node["dates"], `${path}.dates`),
|
|
71
|
+
...zonePart(node, path),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
case "timeOfDay": {
|
|
75
|
+
return {
|
|
76
|
+
type: "timeOfDay",
|
|
77
|
+
from: asTime(node["from"], `${path}.from`),
|
|
78
|
+
to: asTime(node["to"], `${path}.to`),
|
|
79
|
+
...zonePart(node, path),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
case "all": {
|
|
83
|
+
return { type: "all", rules: asRules(node["rules"], `${path}.rules`) };
|
|
84
|
+
}
|
|
85
|
+
case "any": {
|
|
86
|
+
return { type: "any", rules: asRules(node["rules"], `${path}.rules`) };
|
|
87
|
+
}
|
|
88
|
+
default: {
|
|
89
|
+
return { type: "not", rule: parseRule(node["rule"], `${path}.rule`) };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=parse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.js","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plain forms the domain layer accepts in place of rules.
|
|
3
|
+
*
|
|
4
|
+
* `"09:00-17:00"` instead of `timeOfDay("09:00", "17:00")`, and `"2026-03-11"`
|
|
5
|
+
* instead of `dates("2026-03-11")`. Both are what someone writing a schedule
|
|
6
|
+
* by hand would type, and neither is ambiguous: a rule is an object and these
|
|
7
|
+
* are strings, so nothing has to guess which was meant.
|
|
8
|
+
*
|
|
9
|
+
* These check what they are given, which the rule layer deliberately does not
|
|
10
|
+
* — `timeOfDay` accepts any string and complains when the rule is evaluated,
|
|
11
|
+
* because parsing shape and deciding meaning are kept apart there. This layer
|
|
12
|
+
* exists to be written by hand, and a hand-written mistake is worth catching
|
|
13
|
+
* where it was written rather than at the first query.
|
|
14
|
+
*/
|
|
15
|
+
import type { Rule } from "./rule.js";
|
|
16
|
+
/** A rule, a `"09:00-17:00"` window, or a whole day given as a date. */
|
|
17
|
+
export type PlainRule = Rule | string;
|
|
18
|
+
/** A wall-clock window from `"09:00-17:00"`, or a rule left as it is. */
|
|
19
|
+
export declare function asHours(hours: PlainRule): Rule;
|
|
20
|
+
/** Whole days from `"2026-03-11"`, or a rule left as it is. */
|
|
21
|
+
export declare function asDays(scope: PlainRule): Rule;
|
|
22
|
+
//# sourceMappingURL=plain-forms.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plain-forms.d.ts","sourceRoot":"","sources":["../src/plain-forms.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEtC,wEAAwE;AACxE,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,MAAM,CAAC;AAStC,yEAAyE;AACzE,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAoB9C;AAED,+DAA+D;AAC/D,wBAAgB,MAAM,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAe7C"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plain forms the domain layer accepts in place of rules.
|
|
3
|
+
*
|
|
4
|
+
* `"09:00-17:00"` instead of `timeOfDay("09:00", "17:00")`, and `"2026-03-11"`
|
|
5
|
+
* instead of `dates("2026-03-11")`. Both are what someone writing a schedule
|
|
6
|
+
* by hand would type, and neither is ambiguous: a rule is an object and these
|
|
7
|
+
* are strings, so nothing has to guess which was meant.
|
|
8
|
+
*
|
|
9
|
+
* These check what they are given, which the rule layer deliberately does not
|
|
10
|
+
* — `timeOfDay` accepts any string and complains when the rule is evaluated,
|
|
11
|
+
* because parsing shape and deciding meaning are kept apart there. This layer
|
|
12
|
+
* exists to be written by hand, and a hand-written mistake is worth catching
|
|
13
|
+
* where it was written rather than at the first query.
|
|
14
|
+
*/
|
|
15
|
+
import { dates, timeOfDay } from "./build.js";
|
|
16
|
+
function badRange(range, problem) {
|
|
17
|
+
throw new RangeError(`"${range}" is not a range of times: ${problem}. ` +
|
|
18
|
+
`Expected something like "09:00-17:00".`);
|
|
19
|
+
}
|
|
20
|
+
/** A wall-clock window from `"09:00-17:00"`, or a rule left as it is. */
|
|
21
|
+
export function asHours(hours) {
|
|
22
|
+
if (typeof hours !== "string") {
|
|
23
|
+
return hours;
|
|
24
|
+
}
|
|
25
|
+
const parts = hours.split("-");
|
|
26
|
+
if (parts.length !== 2) {
|
|
27
|
+
badRange(hours, `it has ${parts.length - 1} dashes rather than one`);
|
|
28
|
+
}
|
|
29
|
+
const [from = "", to = ""] = parts.map((part) => part.trim());
|
|
30
|
+
for (const time of [from, to]) {
|
|
31
|
+
try {
|
|
32
|
+
Temporal.PlainTime.from(time);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
badRange(hours, `"${time}" is not a time of day`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return timeOfDay(from, to);
|
|
39
|
+
}
|
|
40
|
+
/** Whole days from `"2026-03-11"`, or a rule left as it is. */
|
|
41
|
+
export function asDays(scope) {
|
|
42
|
+
if (typeof scope !== "string") {
|
|
43
|
+
return scope;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
Temporal.PlainDate.from(scope);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
throw new RangeError(`"${scope}" is not a date. Expected something like "2026-03-11", ` +
|
|
50
|
+
`or a rule such as weekdays().`);
|
|
51
|
+
}
|
|
52
|
+
return dates(scope);
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=plain-forms.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plain-forms.js","sourceRoot":"","sources":["../src/plain-forms.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAM9C,SAAS,QAAQ,CAAC,KAAa,EAAE,OAAe;IAC9C,MAAM,IAAI,UAAU,CAClB,IAAI,KAAK,8BAA8B,OAAO,IAAI;QAChD,wCAAwC,CAC3C,CAAC;AACJ,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,OAAO,CAAC,KAAgB;IACtC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC/B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,QAAQ,CAAC,KAAK,EAAE,UAAU,KAAK,CAAC,MAAM,GAAG,CAAC,yBAAyB,CAAC,CAAC;IACvE,CAAC;IAED,MAAM,CAAC,IAAI,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IAC9D,KAAK,MAAM,IAAI,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC;QAC9B,IAAI,CAAC;YACH,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAChC,CAAC;QAAC,MAAM,CAAC;YACP,QAAQ,CAAC,KAAK,EAAE,IAAI,IAAI,wBAAwB,CAAC,CAAC;QACpD,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AAC7B,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,MAAM,CAAC,KAAgB;IACrC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,CAAC;QACH,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACjC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,UAAU,CAClB,IAAI,KAAK,yDAAyD;YAChE,+BAA+B,CAClC,CAAC;IACJ,CAAC;IAED,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC;AACtB,CAAC"}
|
package/dist/query.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asking a question, rather than reading the times something covers.
|
|
3
|
+
*
|
|
4
|
+
* `intervals` is the plumbing. These are what a caller actually wants: is it
|
|
5
|
+
* open now, how much working time is in this window, when does it next open,
|
|
6
|
+
* and — the one both of the libraries this evolves from were built around —
|
|
7
|
+
* where do you get to after three hours that only count while it is open.
|
|
8
|
+
*
|
|
9
|
+
* Each takes a rule, or a cascade narrowed to one of its values by
|
|
10
|
+
* [`assigned`](./assigned.ts). "Three hours while the warehouse is open" and
|
|
11
|
+
* "three hours while Alice is on call" are the same question.
|
|
12
|
+
*
|
|
13
|
+
* Durations are exact elapsed time throughout. Three operating hours means
|
|
14
|
+
* three real hours of opening, so a window spanning a clock change is measured
|
|
15
|
+
* by how long it lasted rather than by what the clock said.
|
|
16
|
+
*/
|
|
17
|
+
import { type Covers } from "./assigned.js";
|
|
18
|
+
import type { Context } from "./context.js";
|
|
19
|
+
import { type Interval } from "./interval.js";
|
|
20
|
+
/**
|
|
21
|
+
* How far a search runs.
|
|
22
|
+
*
|
|
23
|
+
* With neither the context's `to` nor a `within`, a search is unbounded. That
|
|
24
|
+
* is fine and often what you want — the first interval of a satisfiable rule
|
|
25
|
+
* arrives immediately, however far the rule recurs. It is only a rule that
|
|
26
|
+
* covers *nothing* that has no answer to give and no way to discover it, and
|
|
27
|
+
* that case runs until stopped. Give a bound when the answer might be nothing.
|
|
28
|
+
*/
|
|
29
|
+
export interface Search {
|
|
30
|
+
/**
|
|
31
|
+
* Look no further ahead than this from where the search starts.
|
|
32
|
+
*
|
|
33
|
+
* Narrows only. A context that already ends before the horizon keeps its own
|
|
34
|
+
* end, because a caller who gave a window meant it.
|
|
35
|
+
*/
|
|
36
|
+
readonly within?: Temporal.Duration;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Whether a rule, or a value a cascade assigns, covers an instant.
|
|
40
|
+
*
|
|
41
|
+
* Always terminates, whatever it is reading and whatever the context: it asks
|
|
42
|
+
* about the smallest window there is, so nothing can walk far looking for an
|
|
43
|
+
* answer.
|
|
44
|
+
*/
|
|
45
|
+
export declare function activeAt<V>(covers: Covers<V>, at: Temporal.ZonedDateTime, context?: Omit<Context, "from" | "to">): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* How much time a rule, or a value a cascade assigns, covers within a window.
|
|
48
|
+
*
|
|
49
|
+
* Needs a window with an end, because the alternative is a number that never
|
|
50
|
+
* finishes being counted.
|
|
51
|
+
*/
|
|
52
|
+
export declare function elapsed<V>(covers: Covers<V>, context: Context): Temporal.Duration;
|
|
53
|
+
/**
|
|
54
|
+
* The next stretch of time covered, at or after the context's start.
|
|
55
|
+
*
|
|
56
|
+
* `undefined` when there is none within the search. If time is being covered
|
|
57
|
+
* already at the context's start, that stretch is returned clipped to begin
|
|
58
|
+
* there — "when does it next open" answers "it is open" rather than skipping
|
|
59
|
+
* to tomorrow.
|
|
60
|
+
*/
|
|
61
|
+
export declare function next<V>(covers: Covers<V>, context: Context, search?: Search): Interval | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Where you get to after an amount of time that only counts while something
|
|
64
|
+
* holds.
|
|
65
|
+
*
|
|
66
|
+
* Three operating hours from an order placed at five to five on a Friday is
|
|
67
|
+
* some way into Monday morning, and this is the function that says where.
|
|
68
|
+
* `undefined` when the search runs out before the time does.
|
|
69
|
+
*/
|
|
70
|
+
export declare function advanceBy<V>(from: Temporal.ZonedDateTime, amount: Temporal.Duration, options: {
|
|
71
|
+
readonly during: Covers<V>;
|
|
72
|
+
} & Search & Omit<Context, "from" | "to">): Temporal.ZonedDateTime | undefined;
|
|
73
|
+
//# sourceMappingURL=query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,KAAK,MAAM,EAAW,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAwB,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AAMpE;;;;;;;;GAQG;AACH,MAAM,WAAW,MAAM;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC;CACrC;AA0CD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EACxB,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EACjB,EAAE,EAAE,QAAQ,CAAC,aAAa,EAC1B,OAAO,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC,GACrC,OAAO,CAOT;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,CAAC,EACvB,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EACjB,OAAO,EAAE,OAAO,GACf,QAAQ,CAAC,QAAQ,CAenB;AAED;;;;;;;GAOG;AACH,wBAAgB,IAAI,CAAC,CAAC,EACpB,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EACjB,OAAO,EAAE,OAAO,EAChB,MAAM,CAAC,EAAE,MAAM,GACd,QAAQ,GAAG,SAAS,CAGtB;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,CAAC,EACzB,IAAI,EAAE,QAAQ,CAAC,aAAa,EAC5B,MAAM,EAAE,QAAQ,CAAC,QAAQ,EACzB,OAAO,EAAE;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;CAAE,GAAG,MAAM,GAC9C,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC,GAC7B,QAAQ,CAAC,aAAa,GAAG,SAAS,CA8BpC"}
|
package/dist/query.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asking a question, rather than reading the times something covers.
|
|
3
|
+
*
|
|
4
|
+
* `intervals` is the plumbing. These are what a caller actually wants: is it
|
|
5
|
+
* open now, how much working time is in this window, when does it next open,
|
|
6
|
+
* and — the one both of the libraries this evolves from were built around —
|
|
7
|
+
* where do you get to after three hours that only count while it is open.
|
|
8
|
+
*
|
|
9
|
+
* Each takes a rule, or a cascade narrowed to one of its values by
|
|
10
|
+
* [`assigned`](./assigned.ts). "Three hours while the warehouse is open" and
|
|
11
|
+
* "three hours while Alice is on call" are the same question.
|
|
12
|
+
*
|
|
13
|
+
* Durations are exact elapsed time throughout. Three operating hours means
|
|
14
|
+
* three real hours of opening, so a window spanning a clock change is measured
|
|
15
|
+
* by how long it lasted rather than by what the clock said.
|
|
16
|
+
*/
|
|
17
|
+
import { covered } from "./assigned.js";
|
|
18
|
+
import { duration, earlierEnd } from "./interval.js";
|
|
19
|
+
import { take } from "./stream.js";
|
|
20
|
+
/** Zero, as a duration to accumulate onto. */
|
|
21
|
+
const NOTHING = Temporal.Duration.from({ seconds: 0 });
|
|
22
|
+
function bounded(context, search) {
|
|
23
|
+
const within = search?.within;
|
|
24
|
+
if (within === undefined) {
|
|
25
|
+
return context;
|
|
26
|
+
}
|
|
27
|
+
// Whichever runs out first. `within` narrows a search and must never widen
|
|
28
|
+
// one: a context that already ends on Saturday means the caller is not
|
|
29
|
+
// interested in Monday, whatever horizon the search asks for.
|
|
30
|
+
const horizon = context.from.add(within);
|
|
31
|
+
return { ...context, to: earlierEnd(context.to, horizon) ?? horizon };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Refuses an amount whose units do not mean one fixed length of time.
|
|
35
|
+
*
|
|
36
|
+
* A day is not 24 hours on the two mornings a year a clock changes, and a
|
|
37
|
+
* month is not any number of hours at all. Both halves of this function would
|
|
38
|
+
* otherwise disagree about that: the accounting compares durations without a
|
|
39
|
+
* reference point, where a day *is* 24 hours, while the final step adds to a
|
|
40
|
+
* `ZonedDateTime`, where it is a calendar day. `P1D` and `PT24H` would land an
|
|
41
|
+
* hour apart, and neither answer would be wrong enough to notice.
|
|
42
|
+
*
|
|
43
|
+
* Weeks and months do not even get that far — comparing them without a
|
|
44
|
+
* reference point throws, with an empty message.
|
|
45
|
+
*/
|
|
46
|
+
function checkExact(amount) {
|
|
47
|
+
const calendar = ["years", "months", "weeks", "days"].filter((unit) => amount[unit] !== 0);
|
|
48
|
+
if (calendar.length > 0) {
|
|
49
|
+
throw new RangeError(`advanceBy() measures elapsed time, so ${amount.toString()} is ambiguous: ` +
|
|
50
|
+
`${calendar.join(" and ")} are calendar units, and a day is not 24 hours ` +
|
|
51
|
+
`on the mornings a clock changes. Give hours, minutes or seconds.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether a rule, or a value a cascade assigns, covers an instant.
|
|
56
|
+
*
|
|
57
|
+
* Always terminates, whatever it is reading and whatever the context: it asks
|
|
58
|
+
* about the smallest window there is, so nothing can walk far looking for an
|
|
59
|
+
* answer.
|
|
60
|
+
*/
|
|
61
|
+
export function activeAt(covers, at, context) {
|
|
62
|
+
const moment = {
|
|
63
|
+
...context,
|
|
64
|
+
from: at,
|
|
65
|
+
to: at.add({ nanoseconds: 1 }),
|
|
66
|
+
};
|
|
67
|
+
return take(covered(covers, moment), 1).length > 0;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* How much time a rule, or a value a cascade assigns, covers within a window.
|
|
71
|
+
*
|
|
72
|
+
* Needs a window with an end, because the alternative is a number that never
|
|
73
|
+
* finishes being counted.
|
|
74
|
+
*/
|
|
75
|
+
export function elapsed(covers, context) {
|
|
76
|
+
if (context.to === undefined) {
|
|
77
|
+
throw new RangeError("elapsed() needs a window with an end: give the context a `to`.");
|
|
78
|
+
}
|
|
79
|
+
let total = NOTHING;
|
|
80
|
+
for (const interval of covered(covers, context)) {
|
|
81
|
+
const length = duration(interval);
|
|
82
|
+
if (length !== undefined) {
|
|
83
|
+
total = total.add(length);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return total.round({ largestUnit: "hour" });
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The next stretch of time covered, at or after the context's start.
|
|
90
|
+
*
|
|
91
|
+
* `undefined` when there is none within the search. If time is being covered
|
|
92
|
+
* already at the context's start, that stretch is returned clipped to begin
|
|
93
|
+
* there — "when does it next open" answers "it is open" rather than skipping
|
|
94
|
+
* to tomorrow.
|
|
95
|
+
*/
|
|
96
|
+
export function next(covers, context, search) {
|
|
97
|
+
const [first] = take(covered(covers, bounded(context, search)), 1);
|
|
98
|
+
return first;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Where you get to after an amount of time that only counts while something
|
|
102
|
+
* holds.
|
|
103
|
+
*
|
|
104
|
+
* Three operating hours from an order placed at five to five on a Friday is
|
|
105
|
+
* some way into Monday morning, and this is the function that says where.
|
|
106
|
+
* `undefined` when the search runs out before the time does.
|
|
107
|
+
*/
|
|
108
|
+
export function advanceBy(from, amount, options) {
|
|
109
|
+
checkExact(amount);
|
|
110
|
+
if (Temporal.Duration.compare(amount, NOTHING) < 0) {
|
|
111
|
+
throw new RangeError(`advanceBy() cannot go backwards. Asked for ${amount.toString()}.`);
|
|
112
|
+
}
|
|
113
|
+
const { during, within, ...rest } = options;
|
|
114
|
+
const context = bounded({ ...rest, from }, within === undefined ? undefined : { within });
|
|
115
|
+
let remaining = amount;
|
|
116
|
+
for (const interval of covered(during, context)) {
|
|
117
|
+
const length = duration(interval);
|
|
118
|
+
// An interval with no end has more than enough of whatever is left.
|
|
119
|
+
if (length === undefined ||
|
|
120
|
+
Temporal.Duration.compare(length, remaining) >= 0) {
|
|
121
|
+
return interval.start?.add(remaining);
|
|
122
|
+
}
|
|
123
|
+
remaining = remaining.subtract(length);
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.js","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a cascade as the values it assigns, over time.
|
|
3
|
+
*
|
|
4
|
+
* Resolution is a fold. Each layer is read as the times it covers, and laid
|
|
5
|
+
* over everything below it by the cascade's {@link MergeStrategy}. Precedence
|
|
6
|
+
* falls out of that as the merge that keeps the later value, so a rota and a
|
|
7
|
+
* roster of headcounts take the same path through here and differ only in what
|
|
8
|
+
* they do where two layers meet.
|
|
9
|
+
*
|
|
10
|
+
* The regions themselves stay ordinary rules. A layer covers its own scope,
|
|
11
|
+
* minus anything a *replacing* layer above it has claimed, and that is
|
|
12
|
+
* `all(scope, not(any(…)))` which the rule interpreter already evaluates. So
|
|
13
|
+
* the clipping, the zone normalisation and the laziness all come along
|
|
14
|
+
* unchanged, and the one new sweep is the two-stream {@link overlay}.
|
|
15
|
+
*
|
|
16
|
+
* What comes back is only the time a cascade actually assigns. A moment no
|
|
17
|
+
* layer claims is absent from the stream rather than present with some empty
|
|
18
|
+
* value, for the same reason a rule yields only the time it covers. There is
|
|
19
|
+
* no such thing as the value of an unassigned moment.
|
|
20
|
+
*/
|
|
21
|
+
import type { Cascade } from "./cascade.js";
|
|
22
|
+
import type { Context } from "./context.js";
|
|
23
|
+
import { type ValuedStream } from "./valued-stream.js";
|
|
24
|
+
/**
|
|
25
|
+
* The values a cascade assigns within a context, in order and coalesced.
|
|
26
|
+
*
|
|
27
|
+
* Lazy, and endless when the context has no end and the layers recur, which is
|
|
28
|
+
* the same contract `intervals` keeps because this is built out of it.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolve<V>(cascade: Cascade<V>, context: Context): ValuedStream<V>;
|
|
31
|
+
//# sourceMappingURL=resolve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAS,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAK5C,OAAO,EAAqB,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE1E;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,CAAC,EACvB,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,OAAO,EAAE,OAAO,GACf,YAAY,CAAC,CAAC,CAAC,CAWjB"}
|
package/dist/resolve.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a cascade as the values it assigns, over time.
|
|
3
|
+
*
|
|
4
|
+
* Resolution is a fold. Each layer is read as the times it covers, and laid
|
|
5
|
+
* over everything below it by the cascade's {@link MergeStrategy}. Precedence
|
|
6
|
+
* falls out of that as the merge that keeps the later value, so a rota and a
|
|
7
|
+
* roster of headcounts take the same path through here and differ only in what
|
|
8
|
+
* they do where two layers meet.
|
|
9
|
+
*
|
|
10
|
+
* The regions themselves stay ordinary rules. A layer covers its own scope,
|
|
11
|
+
* minus anything a *replacing* layer above it has claimed, and that is
|
|
12
|
+
* `all(scope, not(any(…)))` which the rule interpreter already evaluates. So
|
|
13
|
+
* the clipping, the zone normalisation and the laziness all come along
|
|
14
|
+
* unchanged, and the one new sweep is the two-stream {@link overlay}.
|
|
15
|
+
*
|
|
16
|
+
* What comes back is only the time a cascade actually assigns. A moment no
|
|
17
|
+
* layer claims is absent from the stream rather than present with some empty
|
|
18
|
+
* value, for the same reason a rule yields only the time it covers. There is
|
|
19
|
+
* no such thing as the value of an unassigned moment.
|
|
20
|
+
*/
|
|
21
|
+
import { intervals } from "./interpret.js";
|
|
22
|
+
import { mergeBy } from "./merge.js";
|
|
23
|
+
import { coalesce, overlay } from "./valued-stream.js";
|
|
24
|
+
/**
|
|
25
|
+
* The values a cascade assigns within a context, in order and coalesced.
|
|
26
|
+
*
|
|
27
|
+
* Lazy, and endless when the context has no end and the layers recur, which is
|
|
28
|
+
* the same contract `intervals` keeps because this is built out of it.
|
|
29
|
+
*/
|
|
30
|
+
export function resolve(cascade, context) {
|
|
31
|
+
const merge = mergeBy(cascade.merge);
|
|
32
|
+
let stack = [];
|
|
33
|
+
for (const [index, layer] of cascade.layers.entries()) {
|
|
34
|
+
const above = cascade.layers.slice(index + 1);
|
|
35
|
+
const claimed = assignments(layer, unreplaced(layer.scope, above), context);
|
|
36
|
+
stack = overlay(stack, claimed, merge);
|
|
37
|
+
}
|
|
38
|
+
return coalesce(stack);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A layer's own scope, minus every scope a replacing layer above it claims.
|
|
42
|
+
*
|
|
43
|
+
* Nothing needs subtracting for an ordinary layer above. The fold already
|
|
44
|
+
* settles that, and under `override` the later value wins the overlap outright.
|
|
45
|
+
* A replacing layer is the case that cannot be left to the fold, because it
|
|
46
|
+
* claims its scope whether or not the cascade inside it assigns anything
|
|
47
|
+
* there. Closing early on one day is exactly that. The base hours must stay
|
|
48
|
+
* out of the afternoon the override dropped, rather than showing through it.
|
|
49
|
+
*/
|
|
50
|
+
function unreplaced(scope, above) {
|
|
51
|
+
const replacing = above.filter((layer) => "replace" in layer);
|
|
52
|
+
if (replacing.length === 0) {
|
|
53
|
+
return scope;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
type: "all",
|
|
57
|
+
rules: [
|
|
58
|
+
scope,
|
|
59
|
+
{
|
|
60
|
+
type: "not",
|
|
61
|
+
rule: { type: "any", rules: replacing.map((layer) => layer.scope) },
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** What a layer assigns, over the region it covers. */
|
|
67
|
+
function* assignments(layer, region, context) {
|
|
68
|
+
for (const interval of intervals(region, context)) {
|
|
69
|
+
if ("value" in layer) {
|
|
70
|
+
yield { ...interval, value: layer.value };
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
// A replacing layer claims the region and hands the question inwards. The
|
|
74
|
+
// inner cascade is resolved against the region rather than against the
|
|
75
|
+
// whole context, which is what stops it reaching outside the scope it
|
|
76
|
+
// replaces. It also carries its own merge, so a replacement says how its
|
|
77
|
+
// own layers combine without the cascade around it having a view.
|
|
78
|
+
yield* resolve(layer.replace, within(context, interval));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* A context narrowed to one interval.
|
|
83
|
+
*
|
|
84
|
+
* The interval came from evaluating a rule against this context, so its start
|
|
85
|
+
* is inside the window and never unbounded. The fallback is for the type
|
|
86
|
+
* rather than for a case that occurs.
|
|
87
|
+
*/
|
|
88
|
+
function within(context, interval) {
|
|
89
|
+
const { from, to: _replaced, ...rest } = context;
|
|
90
|
+
const start = interval.start ?? from;
|
|
91
|
+
return interval.end === undefined
|
|
92
|
+
? { ...rest, from: start }
|
|
93
|
+
: { ...rest, from: start, to: interval.end };
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAKH,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAErC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAqB,MAAM,oBAAoB,CAAC;AAE1E;;;;;GAKG;AACH,MAAM,UAAU,OAAO,CACrB,OAAmB,EACnB,OAAgB;IAEhB,MAAM,KAAK,GAAG,OAAO,CAAI,OAAO,CAAC,KAAK,CAAC,CAAC;IAExC,IAAI,KAAK,GAAoB,EAAE,CAAC;IAChC,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC;QACtD,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;QAC9C,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,EAAE,UAAU,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC;QAC5E,KAAK,GAAG,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IACzC,CAAC;IAED,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC;AACzB,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,UAAU,CAAC,KAAW,EAAE,KAAgC;IAC/D,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,SAAS,IAAI,KAAK,CAAC,CAAC;IAC9D,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO;QACL,IAAI,EAAE,KAAK;QACX,KAAK,EAAE;YACL,KAAK;YACL;gBACE,IAAI,EAAE,KAAK;gBACX,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE;aACpE;SACF;KACF,CAAC;AACJ,CAAC;AAED,uDAAuD;AACvD,QAAQ,CAAC,CAAC,WAAW,CACnB,KAAe,EACf,MAAY,EACZ,OAAgB;IAEhB,KAAK,MAAM,QAAQ,IAAI,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;QAClD,IAAI,OAAO,IAAI,KAAK,EAAE,CAAC;YACrB,MAAM,EAAE,GAAG,QAAQ,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC;YAC1C,SAAS;QACX,CAAC;QAED,0EAA0E;QAC1E,uEAAuE;QACvE,sEAAsE;QACtE,yEAAyE;QACzE,kEAAkE;QAClE,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC3D,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,MAAM,CAAC,OAAgB,EAAE,QAAkB;IAClD,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC;IACjD,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,IAAI,IAAI,CAAC;IAErC,OAAO,QAAQ,CAAC,GAAG,KAAK,SAAS;QAC/B,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE;QAC1B,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,CAAC,GAAG,EAAE,CAAC;AACjD,CAAC"}
|