@kensio/quando 0.1.0 → 0.2.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 +125 -4
- 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/cascade.d.ts +86 -0
- package/dist/cascade.d.ts.map +1 -0
- package/dist/cascade.js +40 -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 +26 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -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/parse-fields.d.ts +30 -0
- package/dist/parse-fields.d.ts.map +1 -0
- package/dist/parse-fields.js +93 -0
- package/dist/parse-fields.js.map +1 -0
- package/dist/parse.d.ts +22 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +115 -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 +67 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +121 -0
- package/dist/query.js.map +1 -0
- package/dist/resolve.d.ts +25 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +80 -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 +47 -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 +85 -0
- package/dist/schedule.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 +42 -0
- package/dist/valued-stream.d.ts.map +1 -0
- package/dist/valued-stream.js +77 -0
- package/dist/valued-stream.js.map +1 -0
- package/package.json +1 -1
package/dist/query.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asking a rule a question, rather than reading the times it 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
|
+
* Durations are exact elapsed time throughout. Three operating hours means
|
|
10
|
+
* three real hours of opening, so a window spanning a clock change is measured
|
|
11
|
+
* by how long it lasted rather than by what the clock said.
|
|
12
|
+
*/
|
|
13
|
+
import type { Context } from "./context.js";
|
|
14
|
+
import { type Interval } from "./interval.js";
|
|
15
|
+
import type { Rule } from "./rule.js";
|
|
16
|
+
/**
|
|
17
|
+
* How far a search runs.
|
|
18
|
+
*
|
|
19
|
+
* With neither the context's `to` nor a `within`, a search is unbounded. That
|
|
20
|
+
* is fine and often what you want — the first interval of a satisfiable rule
|
|
21
|
+
* arrives immediately, however far the rule recurs. It is only a rule that
|
|
22
|
+
* covers *nothing* that has no answer to give and no way to discover it, and
|
|
23
|
+
* that case runs until stopped. Give a bound when the answer might be nothing.
|
|
24
|
+
*/
|
|
25
|
+
export interface Search {
|
|
26
|
+
/**
|
|
27
|
+
* Look no further ahead than this from where the search starts.
|
|
28
|
+
*
|
|
29
|
+
* Narrows only. A context that already ends before the horizon keeps its own
|
|
30
|
+
* end, because a caller who gave a window meant it.
|
|
31
|
+
*/
|
|
32
|
+
readonly within?: Temporal.Duration;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Whether a rule covers an instant.
|
|
36
|
+
*
|
|
37
|
+
* Always terminates, whatever the rule and whatever the context: it asks about
|
|
38
|
+
* the smallest window there is, so nothing can walk far looking for an answer.
|
|
39
|
+
*/
|
|
40
|
+
export declare function activeAt(rule: Rule, at: Temporal.ZonedDateTime, context?: Omit<Context, "from" | "to">): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* How much time a rule covers within a window.
|
|
43
|
+
*
|
|
44
|
+
* Needs a window with an end, because the alternative is a number that never
|
|
45
|
+
* finishes being counted.
|
|
46
|
+
*/
|
|
47
|
+
export declare function elapsed(rule: Rule, context: Context): Temporal.Duration;
|
|
48
|
+
/**
|
|
49
|
+
* The next stretch of time a rule covers, at or after the context's start.
|
|
50
|
+
*
|
|
51
|
+
* `undefined` when there is none within the search. If the rule is covering
|
|
52
|
+
* time already at the context's start, that stretch is returned clipped to
|
|
53
|
+
* begin there — "when does it next open" answers "it is open" rather than
|
|
54
|
+
* skipping to tomorrow.
|
|
55
|
+
*/
|
|
56
|
+
export declare function next(rule: Rule, context: Context, search?: Search): Interval | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Where you get to after an amount of time that only counts while a rule holds.
|
|
59
|
+
*
|
|
60
|
+
* Three operating hours from an order placed at five to five on a Friday is
|
|
61
|
+
* some way into Monday morning, and this is the function that says where.
|
|
62
|
+
* `undefined` when the search runs out before the time does.
|
|
63
|
+
*/
|
|
64
|
+
export declare function advanceBy(from: Temporal.ZonedDateTime, amount: Temporal.Duration, options: {
|
|
65
|
+
readonly during: Rule;
|
|
66
|
+
} & Search & Omit<Context, "from" | "to">): Temporal.ZonedDateTime | undefined;
|
|
67
|
+
//# sourceMappingURL=query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAwB,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEpE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAMtC;;;;;;;;GAQG;AACH,MAAM,WAAW,MAAM;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC;CACrC;AA0CD;;;;;GAKG;AACH,wBAAgB,QAAQ,CACtB,IAAI,EAAE,IAAI,EACV,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,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,GAAG,QAAQ,CAAC,QAAQ,CAevE;AAED;;;;;;;GAOG;AACH,wBAAgB,IAAI,CAClB,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,OAAO,EAChB,MAAM,CAAC,EAAE,MAAM,GACd,QAAQ,GAAG,SAAS,CAGtB;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,QAAQ,CAAC,aAAa,EAC5B,MAAM,EAAE,QAAQ,CAAC,QAAQ,EACzB,OAAO,EAAE;IAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAA;CAAE,GAAG,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC,GACzE,QAAQ,CAAC,aAAa,GAAG,SAAS,CA8BpC"}
|
package/dist/query.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asking a rule a question, rather than reading the times it 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
|
+
* Durations are exact elapsed time throughout. Three operating hours means
|
|
10
|
+
* three real hours of opening, so a window spanning a clock change is measured
|
|
11
|
+
* by how long it lasted rather than by what the clock said.
|
|
12
|
+
*/
|
|
13
|
+
import { duration, earlierEnd } from "./interval.js";
|
|
14
|
+
import { intervals } from "./interpret.js";
|
|
15
|
+
import { take } from "./stream.js";
|
|
16
|
+
/** Zero, as a duration to accumulate onto. */
|
|
17
|
+
const NOTHING = Temporal.Duration.from({ seconds: 0 });
|
|
18
|
+
function bounded(context, search) {
|
|
19
|
+
const within = search?.within;
|
|
20
|
+
if (within === undefined) {
|
|
21
|
+
return context;
|
|
22
|
+
}
|
|
23
|
+
// Whichever runs out first. `within` narrows a search and must never widen
|
|
24
|
+
// one: a context that already ends on Saturday means the caller is not
|
|
25
|
+
// interested in Monday, whatever horizon the search asks for.
|
|
26
|
+
const horizon = context.from.add(within);
|
|
27
|
+
return { ...context, to: earlierEnd(context.to, horizon) ?? horizon };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Refuses an amount whose units do not mean one fixed length of time.
|
|
31
|
+
*
|
|
32
|
+
* A day is not 24 hours on the two mornings a year a clock changes, and a
|
|
33
|
+
* month is not any number of hours at all. Both halves of this function would
|
|
34
|
+
* otherwise disagree about that: the accounting compares durations without a
|
|
35
|
+
* reference point, where a day *is* 24 hours, while the final step adds to a
|
|
36
|
+
* `ZonedDateTime`, where it is a calendar day. `P1D` and `PT24H` would land an
|
|
37
|
+
* hour apart, and neither answer would be wrong enough to notice.
|
|
38
|
+
*
|
|
39
|
+
* Weeks and months do not even get that far — comparing them without a
|
|
40
|
+
* reference point throws, with an empty message.
|
|
41
|
+
*/
|
|
42
|
+
function checkExact(amount) {
|
|
43
|
+
const calendar = ["years", "months", "weeks", "days"].filter((unit) => amount[unit] !== 0);
|
|
44
|
+
if (calendar.length > 0) {
|
|
45
|
+
throw new RangeError(`advanceBy() measures elapsed time, so ${amount.toString()} is ambiguous: ` +
|
|
46
|
+
`${calendar.join(" and ")} are calendar units, and a day is not 24 hours ` +
|
|
47
|
+
`on the mornings a clock changes. Give hours, minutes or seconds.`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Whether a rule covers an instant.
|
|
52
|
+
*
|
|
53
|
+
* Always terminates, whatever the rule and whatever the context: it asks about
|
|
54
|
+
* the smallest window there is, so nothing can walk far looking for an answer.
|
|
55
|
+
*/
|
|
56
|
+
export function activeAt(rule, at, context) {
|
|
57
|
+
const moment = {
|
|
58
|
+
...context,
|
|
59
|
+
from: at,
|
|
60
|
+
to: at.add({ nanoseconds: 1 }),
|
|
61
|
+
};
|
|
62
|
+
return take(intervals(rule, moment), 1).length > 0;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* How much time a rule covers within a window.
|
|
66
|
+
*
|
|
67
|
+
* Needs a window with an end, because the alternative is a number that never
|
|
68
|
+
* finishes being counted.
|
|
69
|
+
*/
|
|
70
|
+
export function elapsed(rule, context) {
|
|
71
|
+
if (context.to === undefined) {
|
|
72
|
+
throw new RangeError("elapsed() needs a window with an end: give the context a `to`.");
|
|
73
|
+
}
|
|
74
|
+
let total = NOTHING;
|
|
75
|
+
for (const interval of intervals(rule, context)) {
|
|
76
|
+
const length = duration(interval);
|
|
77
|
+
if (length !== undefined) {
|
|
78
|
+
total = total.add(length);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return total.round({ largestUnit: "hour" });
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The next stretch of time a rule covers, at or after the context's start.
|
|
85
|
+
*
|
|
86
|
+
* `undefined` when there is none within the search. If the rule is covering
|
|
87
|
+
* time already at the context's start, that stretch is returned clipped to
|
|
88
|
+
* begin there — "when does it next open" answers "it is open" rather than
|
|
89
|
+
* skipping to tomorrow.
|
|
90
|
+
*/
|
|
91
|
+
export function next(rule, context, search) {
|
|
92
|
+
const [first] = take(intervals(rule, bounded(context, search)), 1);
|
|
93
|
+
return first;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Where you get to after an amount of time that only counts while a rule holds.
|
|
97
|
+
*
|
|
98
|
+
* Three operating hours from an order placed at five to five on a Friday is
|
|
99
|
+
* some way into Monday morning, and this is the function that says where.
|
|
100
|
+
* `undefined` when the search runs out before the time does.
|
|
101
|
+
*/
|
|
102
|
+
export function advanceBy(from, amount, options) {
|
|
103
|
+
checkExact(amount);
|
|
104
|
+
if (Temporal.Duration.compare(amount, NOTHING) < 0) {
|
|
105
|
+
throw new RangeError(`advanceBy() cannot go backwards. Asked for ${amount.toString()}.`);
|
|
106
|
+
}
|
|
107
|
+
const { during, within, ...rest } = options;
|
|
108
|
+
const context = bounded({ ...rest, from }, within === undefined ? undefined : { within });
|
|
109
|
+
let remaining = amount;
|
|
110
|
+
for (const interval of intervals(during, context)) {
|
|
111
|
+
const length = duration(interval);
|
|
112
|
+
// An interval with no end has more than enough of whatever is left.
|
|
113
|
+
if (length === undefined ||
|
|
114
|
+
Temporal.Duration.compare(length, remaining) >= 0) {
|
|
115
|
+
return interval.start?.add(remaining);
|
|
116
|
+
}
|
|
117
|
+
remaining = remaining.subtract(length);
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
//# 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,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a cascade as the values it assigns, over time.
|
|
3
|
+
*
|
|
4
|
+
* The whole of resolution is one idea: **a layer holds where its own scope
|
|
5
|
+
* holds and no layer above it claims the moment.** Said as a rule that is
|
|
6
|
+
* `all(scope, not(any(…the scopes above)))`, which the rule interpreter
|
|
7
|
+
* already knows how to evaluate — so precedence costs no new algebra, and the
|
|
8
|
+
* clipping, the zone normalisation and the laziness all come along unchanged.
|
|
9
|
+
*
|
|
10
|
+
* What comes back is only the time a cascade actually assigns. A moment no
|
|
11
|
+
* layer claims is absent from the stream rather than present with some empty
|
|
12
|
+
* value, for the same reason a rule yields only the time it covers: there is
|
|
13
|
+
* no such thing as the value of an unassigned moment.
|
|
14
|
+
*/
|
|
15
|
+
import type { Cascade } from "./cascade.js";
|
|
16
|
+
import type { Context } from "./context.js";
|
|
17
|
+
import { type ValuedStream } from "./valued-stream.js";
|
|
18
|
+
/**
|
|
19
|
+
* The values a cascade assigns within a context, in order and coalesced.
|
|
20
|
+
*
|
|
21
|
+
* Lazy, and endless when the context has no end and the layers recur — the
|
|
22
|
+
* same contract `intervals` keeps, because this is built out of it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolve<V>(cascade: Cascade<V>, context: Context): ValuedStream<V>;
|
|
25
|
+
//# sourceMappingURL=resolve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAS,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAI5C,OAAO,EAAwB,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE7E;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,CAAC,EACvB,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,OAAO,EAAE,OAAO,GACf,YAAY,CAAC,CAAC,CAAC,CASjB"}
|
package/dist/resolve.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a cascade as the values it assigns, over time.
|
|
3
|
+
*
|
|
4
|
+
* The whole of resolution is one idea: **a layer holds where its own scope
|
|
5
|
+
* holds and no layer above it claims the moment.** Said as a rule that is
|
|
6
|
+
* `all(scope, not(any(…the scopes above)))`, which the rule interpreter
|
|
7
|
+
* already knows how to evaluate — so precedence costs no new algebra, and the
|
|
8
|
+
* clipping, the zone normalisation and the laziness all come along unchanged.
|
|
9
|
+
*
|
|
10
|
+
* What comes back is only the time a cascade actually assigns. A moment no
|
|
11
|
+
* layer claims is absent from the stream rather than present with some empty
|
|
12
|
+
* value, for the same reason a rule yields only the time it covers: there is
|
|
13
|
+
* no such thing as the value of an unassigned moment.
|
|
14
|
+
*/
|
|
15
|
+
import { intervals } from "./interpret.js";
|
|
16
|
+
import { coalesce, interleave } from "./valued-stream.js";
|
|
17
|
+
/**
|
|
18
|
+
* The values a cascade assigns within a context, in order and coalesced.
|
|
19
|
+
*
|
|
20
|
+
* Lazy, and endless when the context has no end and the layers recur — the
|
|
21
|
+
* same contract `intervals` keeps, because this is built out of it.
|
|
22
|
+
*/
|
|
23
|
+
export function resolve(cascade, context) {
|
|
24
|
+
const claimed = cascade.layers.map((layer, index) => assignments(layer, wins(layer.scope, cascade.layers.slice(index + 1)), context));
|
|
25
|
+
return coalesce(interleave(claimed));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Where a layer wins: inside its own scope, and outside every scope above it.
|
|
29
|
+
*
|
|
30
|
+
* The layers above are re-evaluated once per layer below them, which is
|
|
31
|
+
* quadratic in the number of layers. Layers are few and the streams are lazy,
|
|
32
|
+
* so this buys a precedence that needs no sweep of its own for the price of
|
|
33
|
+
* some repeated work over a handful of rules.
|
|
34
|
+
*/
|
|
35
|
+
function wins(scope, above) {
|
|
36
|
+
// For the top layer this is `all(scope, not(any()))` — `any()` covers no
|
|
37
|
+
// time, its complement covers all of it, and the intersection is the scope
|
|
38
|
+
// itself. Left general rather than special-cased: one path is easier to
|
|
39
|
+
// trust than two, and the identities make the general one right.
|
|
40
|
+
return {
|
|
41
|
+
type: "all",
|
|
42
|
+
rules: [
|
|
43
|
+
scope,
|
|
44
|
+
{
|
|
45
|
+
type: "not",
|
|
46
|
+
rule: { type: "any", rules: above.map((layer) => layer.scope) },
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** What a layer assigns, over the region it wins. */
|
|
52
|
+
function* assignments(layer, region, context) {
|
|
53
|
+
for (const interval of intervals(region, context)) {
|
|
54
|
+
if ("value" in layer) {
|
|
55
|
+
yield { ...interval, value: layer.value };
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
// A replacing layer claims the region and hands the question inwards. The
|
|
59
|
+
// inner cascade is resolved against the region rather than against the
|
|
60
|
+
// whole context, which is what stops it reaching outside the scope it
|
|
61
|
+
// replaces — and what makes anything it leaves unassigned stay unassigned
|
|
62
|
+
// rather than falling through to the layers this one outranks.
|
|
63
|
+
yield* resolve(layer.replace, within(context, interval));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A context narrowed to one interval.
|
|
68
|
+
*
|
|
69
|
+
* The interval came from evaluating a rule against this context, so its start
|
|
70
|
+
* is inside the window and never unbounded; the fallback is for the type
|
|
71
|
+
* rather than for a case that occurs.
|
|
72
|
+
*/
|
|
73
|
+
function within(context, interval) {
|
|
74
|
+
const { from, to: _replaced, ...rest } = context;
|
|
75
|
+
const start = interval.start ?? from;
|
|
76
|
+
return interval.end === undefined
|
|
77
|
+
? { ...rest, from: start }
|
|
78
|
+
: { ...rest, from: start, to: interval.end };
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAqB,MAAM,oBAAoB,CAAC;AAE7E;;;;;GAKG;AACH,MAAM,UAAU,OAAO,CACrB,OAAmB,EACnB,OAAgB;IAEhB,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAClD,WAAW,CACT,KAAK,EACL,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,EAClD,OAAO,CACR,CACF,CAAC;IACF,OAAO,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC;AACvC,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,IAAI,CAAC,KAAW,EAAE,KAAgC;IACzD,yEAAyE;IACzE,2EAA2E;IAC3E,wEAAwE;IACxE,iEAAiE;IACjE,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,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE;aAChE;SACF;KACF,CAAC;AACJ,CAAC;AAED,qDAAqD;AACrD,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,0EAA0E;QAC1E,+DAA+D;QAC/D,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"}
|
package/dist/rota.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who is on, in the words people use for that.
|
|
3
|
+
*
|
|
4
|
+
* The same machinery as a [schedule](./schedule.ts) with the value left open:
|
|
5
|
+
* a rota assigns a person, a tariff assigns a rate, a roster assigns how many
|
|
6
|
+
* are working. A `Rota<V>` is a `Cascade<V>`, so the core reads it unchanged.
|
|
7
|
+
*
|
|
8
|
+
* The value type accumulates as layers are added, so a rota of two names
|
|
9
|
+
* answers `"alice" | "bob" | undefined` rather than `string` — which is what
|
|
10
|
+
* makes an exhaustive switch over who is on call actually exhaustive. Ask for
|
|
11
|
+
* `rota<string>()` when the names are not known up front.
|
|
12
|
+
*/
|
|
13
|
+
import { type Cascade } from "./cascade.js";
|
|
14
|
+
import { type PlainRule } from "./plain-forms.js";
|
|
15
|
+
import type { ValuedStream } from "./valued-stream.js";
|
|
16
|
+
/** Who or what holds when, and the questions worth asking about that. */
|
|
17
|
+
export interface Rota<V> extends Cascade<V> {
|
|
18
|
+
/** These times belong to this one, unless something later says otherwise. */
|
|
19
|
+
readonly assign: <const W>(scope: PlainRule, value: W) => Rota<V | W>;
|
|
20
|
+
/**
|
|
21
|
+
* A swap: this day goes to this one instead.
|
|
22
|
+
*
|
|
23
|
+
* The same thing as an `assign` naming a single day — it exists because
|
|
24
|
+
* "Carol is swapping the eleventh" is what happened, and a rota reads better
|
|
25
|
+
* when the exceptions say they are exceptions.
|
|
26
|
+
*/
|
|
27
|
+
readonly swap: <const W>(day: PlainRule, value: W) => Rota<V | W>;
|
|
28
|
+
/** Who is on at that moment, or `undefined` if nobody is. */
|
|
29
|
+
readonly whoIsOn: (at: Temporal.ZonedDateTime) => V | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Each stretch between two moments, and who has it.
|
|
32
|
+
*
|
|
33
|
+
* Leave `to` out for an endless run of them, which is lazy and safe to stop
|
|
34
|
+
* pulling from whenever you have enough.
|
|
35
|
+
*/
|
|
36
|
+
readonly shifts: (from: Temporal.ZonedDateTime, to?: Temporal.ZonedDateTime) => ValuedStream<V>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* An empty rota: nobody is on until something says they are.
|
|
40
|
+
*
|
|
41
|
+
* ```ts
|
|
42
|
+
* const onCall = rota()
|
|
43
|
+
* .assign(weekdays(), "alice")
|
|
44
|
+
* .assign(weekends(), "bob")
|
|
45
|
+
* .swap("2026-03-11", "carol");
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function rota<V = never>(): Rota<V>;
|
|
49
|
+
//# sourceMappingURL=rota.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rota.d.ts","sourceRoot":"","sources":["../src/rota.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,KAAK,OAAO,EAAqB,MAAM,cAAc,CAAC;AAE/D,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAG1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,yEAAyE;AACzE,MAAM,WAAW,IAAI,CAAC,CAAC,CAAE,SAAQ,OAAO,CAAC,CAAC,CAAC;IACzC,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAEtE;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAElE,6DAA6D;IAC7D,QAAQ,CAAC,OAAO,EAAE,CAAC,EAAE,EAAE,QAAQ,CAAC,aAAa,KAAK,CAAC,GAAG,SAAS,CAAC;IAEhE;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,CACf,IAAI,EAAE,QAAQ,CAAC,aAAa,EAC5B,EAAE,CAAC,EAAE,QAAQ,CAAC,aAAa,KACxB,YAAY,CAAC,CAAC,CAAC,CAAC;CACtB;AA4BD;;;;;;;;;GASG;AACH,wBAAgB,IAAI,CAAC,CAAC,GAAG,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,CAEzC"}
|
package/dist/rota.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who is on, in the words people use for that.
|
|
3
|
+
*
|
|
4
|
+
* The same machinery as a [schedule](./schedule.ts) with the value left open:
|
|
5
|
+
* a rota assigns a person, a tariff assigns a rate, a roster assigns how many
|
|
6
|
+
* are working. A `Rota<V>` is a `Cascade<V>`, so the core reads it unchanged.
|
|
7
|
+
*
|
|
8
|
+
* The value type accumulates as layers are added, so a rota of two names
|
|
9
|
+
* answers `"alice" | "bob" | undefined` rather than `string` — which is what
|
|
10
|
+
* makes an exhaustive switch over who is on call actually exhaustive. Ask for
|
|
11
|
+
* `rota<string>()` when the names are not known up front.
|
|
12
|
+
*/
|
|
13
|
+
import { layer } from "./cascade.js";
|
|
14
|
+
import { asDays } from "./plain-forms.js";
|
|
15
|
+
import { resolve } from "./resolve.js";
|
|
16
|
+
import { take } from "./stream.js";
|
|
17
|
+
function build(layers) {
|
|
18
|
+
const self = {
|
|
19
|
+
type: "cascade",
|
|
20
|
+
layers,
|
|
21
|
+
assign: (scope, value) => build([...layers, layer(asDays(scope), value)]),
|
|
22
|
+
swap: (day, value) => build([...layers, layer(asDays(day), value)]),
|
|
23
|
+
whoIsOn: (at) => {
|
|
24
|
+
// The smallest window there is, so this terminates whatever the layers
|
|
25
|
+
// say — the same trick `activeAt` uses on a rule.
|
|
26
|
+
const moment = { from: at, to: at.add({ nanoseconds: 1 }) };
|
|
27
|
+
const [now] = take(resolve(self, moment), 1);
|
|
28
|
+
return now?.value;
|
|
29
|
+
},
|
|
30
|
+
shifts: (from, to) => resolve(self, to === undefined ? { from } : { from, to }),
|
|
31
|
+
};
|
|
32
|
+
return self;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* An empty rota: nobody is on until something says they are.
|
|
36
|
+
*
|
|
37
|
+
* ```ts
|
|
38
|
+
* const onCall = rota()
|
|
39
|
+
* .assign(weekdays(), "alice")
|
|
40
|
+
* .assign(weekends(), "bob")
|
|
41
|
+
* .swap("2026-03-11", "carol");
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function rota() {
|
|
45
|
+
return build([]);
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=rota.js.map
|
package/dist/rota.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rota.js","sourceRoot":"","sources":["../src/rota.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAA4B,KAAK,EAAE,MAAM,cAAc,CAAC;AAE/D,OAAO,EAAE,MAAM,EAAkB,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAgCnC,SAAS,KAAK,CAAI,MAA2B;IAC3C,MAAM,IAAI,GAAY;QACpB,IAAI,EAAE,SAAS;QACf,MAAM;QAEN,MAAM,EAAE,CAAI,KAAgB,EAAE,KAAQ,EAAE,EAAE,CACxC,KAAK,CAAQ,CAAC,GAAG,MAAM,EAAE,KAAK,CAAQ,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;QAE/D,IAAI,EAAE,CAAI,GAAc,EAAE,KAAQ,EAAE,EAAE,CACpC,KAAK,CAAQ,CAAC,GAAG,MAAM,EAAE,KAAK,CAAQ,MAAM,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;QAE7D,OAAO,EAAE,CAAC,EAAE,EAAE,EAAE;YACd,uEAAuE;YACvE,kDAAkD;YAClD,MAAM,MAAM,GAAY,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;YACrE,MAAM,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;YAC7C,OAAO,GAAG,EAAE,KAAK,CAAC;QACpB,CAAC;QAED,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,CACnB,OAAO,CAAC,IAAI,EAAE,EAAE,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;KAC5D,CAAC;IAEF,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,IAAI;IAClB,OAAO,KAAK,CAAI,EAAE,CAAC,CAAC;AACtB,CAAC"}
|
package/dist/rule.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rule language, as data.
|
|
3
|
+
*
|
|
4
|
+
* A rule is a plain JSON value with a `type` tag — not an object with methods.
|
|
5
|
+
* That is what makes storing one, sending one over a wire, and validating one
|
|
6
|
+
* cost nothing: the document *is* the rule. It also means a new operation over
|
|
7
|
+
* rules is a new function rather than a new method on every rule type, which
|
|
8
|
+
* matters because there are many operations coming — evaluating, describing,
|
|
9
|
+
* validating, rendering, diffing — and comparatively few rule types.
|
|
10
|
+
*/
|
|
11
|
+
export declare const WEEKDAYS: readonly ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"];
|
|
12
|
+
export type Weekday = (typeof WEEKDAYS)[number];
|
|
13
|
+
/**
|
|
14
|
+
* A rule says *when*, and nothing else. It is boolean: the times it covers and
|
|
15
|
+
* the times it does not.
|
|
16
|
+
*
|
|
17
|
+
* Values — who is on call, what the tariff is — attach to layers rather than to
|
|
18
|
+
* rules, which is what keeps `not` meaningful and the set algebra simple.
|
|
19
|
+
*/
|
|
20
|
+
export type Rule = AlwaysRule | NeverRule | DaysOfWeekRule | TimeOfDayRule | DatesRule | AllRule | AnyRule | NotRule;
|
|
21
|
+
/** All time. The identity for intersection. */
|
|
22
|
+
export interface AlwaysRule {
|
|
23
|
+
readonly type: "always";
|
|
24
|
+
}
|
|
25
|
+
/** No time at all. The identity for union. */
|
|
26
|
+
export interface NeverRule {
|
|
27
|
+
readonly type: "never";
|
|
28
|
+
}
|
|
29
|
+
/** Whole days, by their day of the week. */
|
|
30
|
+
export interface DaysOfWeekRule {
|
|
31
|
+
readonly type: "daysOfWeek";
|
|
32
|
+
readonly days: readonly Weekday[];
|
|
33
|
+
/** Overrides the context's zone, for a rule about a particular place. */
|
|
34
|
+
readonly zone?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A window within each day, as wall-clock times: `"09:00"` to `"17:00"`.
|
|
38
|
+
*
|
|
39
|
+
* Wall clock is what people write and what schedules mean. Across a daylight
|
|
40
|
+
* saving transition the elapsed length of the window changes and the clock
|
|
41
|
+
* times do not, which is the right way round.
|
|
42
|
+
*
|
|
43
|
+
* A `to` earlier than `from` wraps past midnight, so `"22:00"` to `"06:00"` is
|
|
44
|
+
* a night shift rather than nothing.
|
|
45
|
+
*/
|
|
46
|
+
export interface TimeOfDayRule {
|
|
47
|
+
readonly type: "timeOfDay";
|
|
48
|
+
readonly from: string;
|
|
49
|
+
readonly to: string;
|
|
50
|
+
readonly zone?: string;
|
|
51
|
+
}
|
|
52
|
+
/** Whole days, by date: `"2026-03-14"`. */
|
|
53
|
+
export interface DatesRule {
|
|
54
|
+
readonly type: "dates";
|
|
55
|
+
readonly dates: readonly string[];
|
|
56
|
+
readonly zone?: string;
|
|
57
|
+
}
|
|
58
|
+
/** Every rule must hold: intersection. */
|
|
59
|
+
export interface AllRule {
|
|
60
|
+
readonly type: "all";
|
|
61
|
+
readonly rules: readonly Rule[];
|
|
62
|
+
}
|
|
63
|
+
/** At least one rule must hold: union. */
|
|
64
|
+
export interface AnyRule {
|
|
65
|
+
readonly type: "any";
|
|
66
|
+
readonly rules: readonly Rule[];
|
|
67
|
+
}
|
|
68
|
+
/** The times a rule does not hold: complement. */
|
|
69
|
+
export interface NotRule {
|
|
70
|
+
readonly type: "not";
|
|
71
|
+
readonly rule: Rule;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=rule.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rule.d.ts","sourceRoot":"","sources":["../src/rule.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,eAAO,MAAM,QAAQ,YACnB,QAAQ,EACR,SAAS,EACT,WAAW,EACX,UAAU,EACV,QAAQ,EACR,UAAU,EACV,QAAQ,CACA,CAAC;AAEX,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC;AAEhD;;;;;;GAMG;AACH,MAAM,MAAM,IAAI,GACZ,UAAU,GACV,SAAS,GACT,cAAc,GACd,aAAa,GACb,SAAS,GACT,OAAO,GACP,OAAO,GACP,OAAO,CAAC;AAEZ,+CAA+C;AAC/C,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;CACzB;AAED,8CAA8C;AAC9C,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,4CAA4C;AAC5C,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,CAAC;IAClC,yEAAyE;IACzE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,2CAA2C;AAC3C,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,0CAA0C;AAC1C,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,SAAS,IAAI,EAAE,CAAC;CACjC;AAED,0CAA0C;AAC1C,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,SAAS,IAAI,EAAE,CAAC;CACjC;AAED,kDAAkD;AAClD,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;CACrB"}
|
package/dist/rule.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rule language, as data.
|
|
3
|
+
*
|
|
4
|
+
* A rule is a plain JSON value with a `type` tag — not an object with methods.
|
|
5
|
+
* That is what makes storing one, sending one over a wire, and validating one
|
|
6
|
+
* cost nothing: the document *is* the rule. It also means a new operation over
|
|
7
|
+
* rules is a new function rather than a new method on every rule type, which
|
|
8
|
+
* matters because there are many operations coming — evaluating, describing,
|
|
9
|
+
* validating, rendering, diffing — and comparatively few rule types.
|
|
10
|
+
*/
|
|
11
|
+
export const WEEKDAYS = [
|
|
12
|
+
"monday",
|
|
13
|
+
"tuesday",
|
|
14
|
+
"wednesday",
|
|
15
|
+
"thursday",
|
|
16
|
+
"friday",
|
|
17
|
+
"saturday",
|
|
18
|
+
"sunday",
|
|
19
|
+
];
|
|
20
|
+
//# sourceMappingURL=rule.js.map
|
package/dist/rule.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rule.js","sourceRoot":"","sources":["../src/rule.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,QAAQ;IACR,SAAS;IACT,WAAW;IACX,UAAU;IACV,QAAQ;IACR,UAAU;IACV,QAAQ;CACA,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opening hours, in the words people use for them.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is a cascade underneath, and a `Schedule` *is* one — the
|
|
5
|
+
* same trick the rule builders use, so it serialises to the same document and
|
|
6
|
+
* `resolve` reads it unchanged. What this adds is vocabulary: nobody running a
|
|
7
|
+
* warehouse says "add a layer to the cascade", they say we are open weekdays,
|
|
8
|
+
* closed on bank holidays, and on the eleventh we close at three.
|
|
9
|
+
*
|
|
10
|
+
* Those three read in the order they are said, and each one outranks what came
|
|
11
|
+
* before it — which is the same precedence a cascade has, arrived at by
|
|
12
|
+
* writing the sentence in the obvious order rather than by knowing the rule.
|
|
13
|
+
*/
|
|
14
|
+
import { type Cascade } from "./cascade.js";
|
|
15
|
+
import { type Interval } from "./interval.js";
|
|
16
|
+
import { type PlainRule } from "./plain-forms.js";
|
|
17
|
+
/**
|
|
18
|
+
* When something is open, and the questions worth asking about that.
|
|
19
|
+
*
|
|
20
|
+
* A `Schedule` is a `Cascade<boolean>`, so anything that takes a cascade takes
|
|
21
|
+
* one of these. The methods below are the common half said plainly; the
|
|
22
|
+
* cascade underneath is the whole of it.
|
|
23
|
+
*/
|
|
24
|
+
export interface Schedule extends Cascade<boolean> {
|
|
25
|
+
/**
|
|
26
|
+
* Open during these times. With hours, inside them on those days; without,
|
|
27
|
+
* for the whole of them.
|
|
28
|
+
*/
|
|
29
|
+
readonly open: (scope: PlainRule, hours?: PlainRule) => Schedule;
|
|
30
|
+
/** Closed for the whole of these times, whatever was said before. */
|
|
31
|
+
readonly closed: (scope: PlainRule) => Schedule;
|
|
32
|
+
/**
|
|
33
|
+
* The hours on this day, in place of whatever was said before.
|
|
34
|
+
*
|
|
35
|
+
* Instead of, not as well as: the usual hours do not show through the part
|
|
36
|
+
* this leaves out, which is what "we close early on the eleventh" means and
|
|
37
|
+
* what makes it different from being shut between three and five.
|
|
38
|
+
*/
|
|
39
|
+
readonly hoursOn: (day: PlainRule, hours: PlainRule) => Schedule;
|
|
40
|
+
/** Whether it is open at that moment. */
|
|
41
|
+
readonly isOpen: (at: Temporal.ZonedDateTime) => boolean;
|
|
42
|
+
/**
|
|
43
|
+
* The next stretch it is open, at or after a moment, or `undefined` if there
|
|
44
|
+
* is none within `within` of it.
|
|
45
|
+
*
|
|
46
|
+
* `within` bounds how far to look and not what is found: a stretch that
|
|
47
|
+
* starts inside the horizon is returned whole, ending when it really closes
|
|
48
|
+
* rather than where the search stopped. That differs from `next` on a rule,
|
|
49
|
+
* which clips its answer to the window it was given.
|
|
50
|
+
*
|
|
51
|
+
* A schedule that is never open has no answer to give and no way to discover
|
|
52
|
+
* that, so pass `within` when that is a possibility.
|
|
53
|
+
*/
|
|
54
|
+
readonly opensNext: (at: Temporal.ZonedDateTime, within?: Temporal.Duration) => Interval | undefined;
|
|
55
|
+
/** How long it is open between two moments. */
|
|
56
|
+
readonly openBetween: (from: Temporal.ZonedDateTime, to: Temporal.ZonedDateTime) => Temporal.Duration;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* An empty schedule: open for nothing until something says otherwise.
|
|
60
|
+
*
|
|
61
|
+
* ```ts
|
|
62
|
+
* const openingHours = schedule()
|
|
63
|
+
* .open(weekdays(), "09:00-17:00")
|
|
64
|
+
* .closed("2026-12-25")
|
|
65
|
+
* .hoursOn("2026-03-11", "09:00-15:00");
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare function schedule(): Schedule;
|
|
69
|
+
//# sourceMappingURL=schedule.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schedule.d.ts","sourceRoot":"","sources":["../src/schedule.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,KAAK,OAAO,EAA8B,MAAM,cAAc,CAAC;AAExE,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAOnE;;;;;;GAMG;AACH,MAAM,WAAW,QAAS,SAAQ,OAAO,CAAC,OAAO,CAAC;IAChD;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,EAAE,SAAS,KAAK,QAAQ,CAAC;IAEjE,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,QAAQ,CAAC;IAEhD;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,KAAK,QAAQ,CAAC;IAEjE,yCAAyC;IACzC,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,QAAQ,CAAC,aAAa,KAAK,OAAO,CAAC;IAEzD;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,SAAS,EAAE,CAClB,EAAE,EAAE,QAAQ,CAAC,aAAa,EAC1B,MAAM,CAAC,EAAE,QAAQ,CAAC,QAAQ,KACvB,QAAQ,GAAG,SAAS,CAAC;IAE1B,+CAA+C;IAC/C,QAAQ,CAAC,WAAW,EAAE,CACpB,IAAI,EAAE,QAAQ,CAAC,aAAa,EAC5B,EAAE,EAAE,QAAQ,CAAC,aAAa,KACvB,QAAQ,CAAC,QAAQ,CAAC;CACxB;AA+DD;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,IAAI,QAAQ,CAEnC"}
|