@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.
Files changed (97) hide show
  1. package/README.md +135 -4
  2. package/dist/assigned.d.ts +68 -0
  3. package/dist/assigned.d.ts.map +1 -0
  4. package/dist/assigned.js +84 -0
  5. package/dist/assigned.js.map +1 -0
  6. package/dist/build.d.ts +54 -0
  7. package/dist/build.d.ts.map +1 -0
  8. package/dist/build.js +77 -0
  9. package/dist/build.js.map +1 -0
  10. package/dist/canonical-rule.d.ts +23 -0
  11. package/dist/canonical-rule.d.ts.map +1 -0
  12. package/dist/canonical-rule.js +139 -0
  13. package/dist/canonical-rule.js.map +1 -0
  14. package/dist/canonical.d.ts +44 -0
  15. package/dist/canonical.d.ts.map +1 -0
  16. package/dist/canonical.js +53 -0
  17. package/dist/canonical.js.map +1 -0
  18. package/dist/cascade.d.ts +107 -0
  19. package/dist/cascade.d.ts.map +1 -0
  20. package/dist/cascade.js +53 -0
  21. package/dist/cascade.js.map +1 -0
  22. package/dist/context.d.ts +44 -0
  23. package/dist/context.d.ts.map +1 -0
  24. package/dist/context.js +9 -0
  25. package/dist/context.js.map +1 -0
  26. package/dist/day-rules.d.ts +23 -0
  27. package/dist/day-rules.d.ts.map +1 -0
  28. package/dist/day-rules.js +102 -0
  29. package/dist/day-rules.js.map +1 -0
  30. package/dist/index.d.ts +37 -2
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +24 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/interpret.d.ts +31 -0
  35. package/dist/interpret.d.ts.map +1 -0
  36. package/dist/interpret.js +99 -0
  37. package/dist/interpret.js.map +1 -0
  38. package/dist/interval-stream.d.ts +11 -4
  39. package/dist/interval-stream.d.ts.map +1 -1
  40. package/dist/interval-stream.js.map +1 -1
  41. package/dist/merge.d.ts +43 -0
  42. package/dist/merge.d.ts.map +1 -0
  43. package/dist/merge.js +79 -0
  44. package/dist/merge.js.map +1 -0
  45. package/dist/parse-cascade.d.ts +37 -0
  46. package/dist/parse-cascade.d.ts.map +1 -0
  47. package/dist/parse-cascade.js +105 -0
  48. package/dist/parse-cascade.js.map +1 -0
  49. package/dist/parse-fields.d.ts +27 -0
  50. package/dist/parse-fields.d.ts.map +1 -0
  51. package/dist/parse-fields.js +65 -0
  52. package/dist/parse-fields.js.map +1 -0
  53. package/dist/parse-shape.d.ts +39 -0
  54. package/dist/parse-shape.d.ts.map +1 -0
  55. package/dist/parse-shape.js +77 -0
  56. package/dist/parse-shape.js.map +1 -0
  57. package/dist/parse.d.ts +22 -0
  58. package/dist/parse.d.ts.map +1 -0
  59. package/dist/parse.js +93 -0
  60. package/dist/parse.js.map +1 -0
  61. package/dist/plain-forms.d.ts +22 -0
  62. package/dist/plain-forms.d.ts.map +1 -0
  63. package/dist/plain-forms.js +54 -0
  64. package/dist/plain-forms.js.map +1 -0
  65. package/dist/query.d.ts +73 -0
  66. package/dist/query.d.ts.map +1 -0
  67. package/dist/query.js +127 -0
  68. package/dist/query.js.map +1 -0
  69. package/dist/resolve.d.ts +31 -0
  70. package/dist/resolve.d.ts.map +1 -0
  71. package/dist/resolve.js +95 -0
  72. package/dist/resolve.js.map +1 -0
  73. package/dist/rota.d.ts +49 -0
  74. package/dist/rota.d.ts.map +1 -0
  75. package/dist/rota.js +41 -0
  76. package/dist/rota.js.map +1 -0
  77. package/dist/rule.d.ts +73 -0
  78. package/dist/rule.d.ts.map +1 -0
  79. package/dist/rule.js +20 -0
  80. package/dist/rule.js.map +1 -0
  81. package/dist/schedule.d.ts +69 -0
  82. package/dist/schedule.d.ts.map +1 -0
  83. package/dist/schedule.js +80 -0
  84. package/dist/schedule.js.map +1 -0
  85. package/dist/tally.d.ts +76 -0
  86. package/dist/tally.d.ts.map +1 -0
  87. package/dist/tally.js +80 -0
  88. package/dist/tally.js.map +1 -0
  89. package/dist/time-rules.d.ts +18 -0
  90. package/dist/time-rules.d.ts.map +1 -0
  91. package/dist/time-rules.js +55 -0
  92. package/dist/time-rules.js.map +1 -0
  93. package/dist/valued-stream.d.ts +51 -0
  94. package/dist/valued-stream.d.ts.map +1 -0
  95. package/dist/valued-stream.js +148 -0
  96. package/dist/valued-stream.js.map +1 -0
  97. 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"}
@@ -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"}
@@ -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"}
@@ -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"}