@kensio/quando 0.1.0-beta.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/README.md
CHANGED
|
@@ -2,8 +2,129 @@
|
|
|
2
2
|
|
|
3
3
|
Declarative temporal rules for schedules, deadlines, constraints, and exceptions.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Quando answers one question: **given these rules about time, when does something
|
|
6
|
+
happen?** `Temporal` tells you what a moment _is_; Quando tells you when
|
|
7
|
+
something _occurs_ — "weekdays nine to five, except bank holidays", and then
|
|
8
|
+
what that implies about now, about a window, and about three working hours from
|
|
9
|
+
here.
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
```bash
|
|
12
|
+
npm install @kensio/quando
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires a runtime with `Temporal`: **Node 26 or later**, or a browser that
|
|
16
|
+
implements it. Quando reads the global rather than importing a polyfill, so it
|
|
17
|
+
has no runtime dependencies; anywhere without `Temporal` natively can load
|
|
18
|
+
`temporal-polyfill` first and everything here works untouched.
|
|
19
|
+
|
|
20
|
+
> The rule language, builder, parser and queries below are on `main` and go out
|
|
21
|
+
> in the next release. `0.1.0` on npm carries only the interval core.
|
|
22
|
+
|
|
23
|
+
## A rule
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { timeOfDay, weekdays } from "@kensio/quando";
|
|
27
|
+
|
|
28
|
+
const openingHours = weekdays().and(timeOfDay("09:00", "17:00"));
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
There is no build step to that: it is already the rule, a plain JSON-shaped
|
|
32
|
+
object with methods hanging off it. `JSON.stringify` gives you a document you
|
|
33
|
+
can store, and `parseRule` gives you the rule back.
|
|
34
|
+
|
|
35
|
+
## Four questions
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { activeAt, advanceBy, elapsed, next } from "@kensio/quando";
|
|
39
|
+
|
|
40
|
+
const friday = Temporal.ZonedDateTime.from("2026-03-13T16:55[Europe/London]");
|
|
41
|
+
|
|
42
|
+
// Is it open?
|
|
43
|
+
activeAt(openingHours, friday);
|
|
44
|
+
// → true
|
|
45
|
+
|
|
46
|
+
// When does it next open?
|
|
47
|
+
next(openingHours, { from: friday.add({ hours: 2 }) })?.start?.toString();
|
|
48
|
+
// → 2026-03-16T09:00:00+00:00[Europe/London]
|
|
49
|
+
|
|
50
|
+
// How much opening is there this week?
|
|
51
|
+
elapsed(openingHours, {
|
|
52
|
+
from: Temporal.ZonedDateTime.from("2026-03-09T00:00[Europe/London]"),
|
|
53
|
+
to: Temporal.ZonedDateTime.from("2026-03-16T00:00[Europe/London]"),
|
|
54
|
+
}).toString();
|
|
55
|
+
// → PT40H
|
|
56
|
+
|
|
57
|
+
// Three hours of packing that only count while it is open — when is it done?
|
|
58
|
+
advanceBy(friday, Temporal.Duration.from({ hours: 3 }), {
|
|
59
|
+
during: openingHours,
|
|
60
|
+
})?.toString();
|
|
61
|
+
// → 2026-03-16T11:55:00+00:00[Europe/London]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
That last one is why the library exists, and it is the one that is genuinely
|
|
65
|
+
awkward to do by hand.
|
|
66
|
+
|
|
67
|
+
## Or say it the way you would say it
|
|
68
|
+
|
|
69
|
+
For opening hours and rotas there is a plainer front door, which builds the same
|
|
70
|
+
thing:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { rota, schedule, weekdays, weekends } from "@kensio/quando";
|
|
74
|
+
|
|
75
|
+
const openingHours = schedule()
|
|
76
|
+
.open(weekdays(), "09:00-17:00")
|
|
77
|
+
.closed("2026-12-25")
|
|
78
|
+
.hoursOn("2026-03-11", "09:00-15:00"); // close early, just that day
|
|
79
|
+
|
|
80
|
+
openingHours.isOpen(friday);
|
|
81
|
+
// → true
|
|
82
|
+
|
|
83
|
+
const onCall = rota().assign(weekdays(), "alice").assign(weekends(), "bob");
|
|
84
|
+
|
|
85
|
+
onCall.whoIsOn(friday);
|
|
86
|
+
// → "alice"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Each line outranks the ones above it, so exceptions read in the order you would
|
|
90
|
+
say them. See [schedules and rotas](docs/schedules/).
|
|
91
|
+
|
|
92
|
+
## Why intervals
|
|
93
|
+
|
|
94
|
+
A rule does not answer about an instant — it produces the intervals over which
|
|
95
|
+
it holds. Anything that samples has to choose a step size, trading correctness
|
|
96
|
+
against speed, and every derived question becomes the same loop. Producing
|
|
97
|
+
intervals turns those loops into arithmetic: working time in a window is a sum,
|
|
98
|
+
three working days from now is a walk, and a rule that can never be satisfied
|
|
99
|
+
yields nothing instead of never finishing.
|
|
100
|
+
|
|
101
|
+
[Concepts](docs/concepts/) has the long version.
|
|
102
|
+
|
|
103
|
+
## Documentation
|
|
104
|
+
|
|
105
|
+
[quandojs.dev](https://quandojs.dev), which is built from [`docs/`](docs/) in
|
|
106
|
+
this repository:
|
|
107
|
+
|
|
108
|
+
- [Getting started](docs/getting-started/) — requirements, install, first query
|
|
109
|
+
- [Concepts](docs/concepts/) — the model, and why it is that shape
|
|
110
|
+
- [Rules](docs/rules/) — every rule type, and what each produces
|
|
111
|
+
- [Queries](docs/queries/) — the four questions, and termination
|
|
112
|
+
- [Time zones](docs/time-zones/) — wall clock against elapsed time
|
|
113
|
+
- [Serialisation](docs/serialisation/) — the JSON form and its boundary
|
|
114
|
+
- [Schedules and rotas](docs/schedules/) — opening hours and who is on, plainly
|
|
115
|
+
- [Cascades](docs/cascades/) — layers carrying values, resolved by precedence
|
|
116
|
+
- [API](docs/api/) — everything the package exports
|
|
117
|
+
|
|
118
|
+
## What it is not
|
|
119
|
+
|
|
120
|
+
Not a date library — `Temporal` is that, and Quando is built on it. Not a
|
|
121
|
+
scheduler: Quando calculates _when_ and never fires anything, so its answer is
|
|
122
|
+
your scheduler's input. Not storage, and not a holiday data provider; calendar
|
|
123
|
+
data belongs in satellite packages so that the core carries none.
|
|
124
|
+
|
|
125
|
+
Cascades carry values by precedence; merging values that should add rather than
|
|
126
|
+
displace is designed and not yet built, along with estimates and a command line.
|
|
127
|
+
|
|
128
|
+
## Licence
|
|
129
|
+
|
|
130
|
+
[Apache-2.0](LICENSE).
|
package/dist/build.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A readable way to write a rule, which is also the rule.
|
|
3
|
+
*
|
|
4
|
+
* `weekdays().and(timeOfDay("09:00", "17:00"))` reads better than the nested
|
|
5
|
+
* object literal it stands for — and it *is* that object literal, with methods
|
|
6
|
+
* hanging off it. `JSON.stringify` omits function-valued properties, so a built
|
|
7
|
+
* rule serialises to exactly the document a hand-written one would, with no
|
|
8
|
+
* `.build()` step and nothing to unwrap.
|
|
9
|
+
*
|
|
10
|
+
* That is the whole trick: there is no builder type to convert out of, because
|
|
11
|
+
* a built rule already satisfies `Rule`.
|
|
12
|
+
*/
|
|
13
|
+
import type { AllRule, AlwaysRule, AnyRule, DatesRule, DaysOfWeekRule, NeverRule, NotRule, Rule, TimeOfDayRule, Weekday } from "./rule.js";
|
|
14
|
+
/** A rule, plus the methods for combining it with others. */
|
|
15
|
+
export type Built<R extends Rule> = R & {
|
|
16
|
+
/** Both this and the others must hold. */
|
|
17
|
+
readonly and: (...others: readonly Rule[]) => Built<AllRule>;
|
|
18
|
+
/** This or any of the others. */
|
|
19
|
+
readonly or: (...others: readonly Rule[]) => Built<AnyRule>;
|
|
20
|
+
/**
|
|
21
|
+
* This, minus the times the others cover.
|
|
22
|
+
*
|
|
23
|
+
* The common shape by far — opening hours except holidays, a schedule except
|
|
24
|
+
* a shutdown — and worth its own method because writing it out is
|
|
25
|
+
* `all(this, not(any(others)))`, which reads like nothing at all.
|
|
26
|
+
*/
|
|
27
|
+
readonly except: (...others: readonly Rule[]) => Built<AllRule>;
|
|
28
|
+
};
|
|
29
|
+
/** All of time. */
|
|
30
|
+
export declare function always(): Built<AlwaysRule>;
|
|
31
|
+
/** No time at all. */
|
|
32
|
+
export declare function never(): Built<NeverRule>;
|
|
33
|
+
/** Whole days, by day of the week. */
|
|
34
|
+
export declare function daysOfWeek(...days: readonly Weekday[]): Built<DaysOfWeekRule>;
|
|
35
|
+
/** Monday to Friday. */
|
|
36
|
+
export declare function weekdays(): Built<DaysOfWeekRule>;
|
|
37
|
+
/** Saturday and Sunday. */
|
|
38
|
+
export declare function weekends(): Built<DaysOfWeekRule>;
|
|
39
|
+
/**
|
|
40
|
+
* A wall-clock window within each day. A `to` earlier than `from` wraps past
|
|
41
|
+
* midnight, so `timeOfDay("22:00", "06:00")` is a night shift.
|
|
42
|
+
*/
|
|
43
|
+
export declare function timeOfDay(from: string, to: string, zone?: string): Built<TimeOfDayRule>;
|
|
44
|
+
/** Whole days, by date. */
|
|
45
|
+
export declare function dates(...days: readonly string[]): Built<DatesRule>;
|
|
46
|
+
/** Every one of these must hold. With none, all of time. */
|
|
47
|
+
export declare function all(...rules: readonly Rule[]): Built<AllRule>;
|
|
48
|
+
/** Any one of these. With none, no time at all. */
|
|
49
|
+
export declare function any(...rules: readonly Rule[]): Built<AnyRule>;
|
|
50
|
+
/** The times a rule does not cover. */
|
|
51
|
+
export declare function not(rule: Rule): Built<NotRule>;
|
|
52
|
+
/** Whole days, by the zone a day is measured in. */
|
|
53
|
+
export declare function inZone<R extends DaysOfWeekRule | DatesRule | TimeOfDayRule>(rule: R, zone: string): Built<R>;
|
|
54
|
+
//# sourceMappingURL=build.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EACV,OAAO,EACP,UAAU,EACV,OAAO,EACP,SAAS,EACT,cAAc,EACd,SAAS,EACT,OAAO,EACP,IAAI,EACJ,aAAa,EACb,OAAO,EACR,MAAM,WAAW,CAAC;AAEnB,6DAA6D;AAC7D,MAAM,MAAM,KAAK,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG;IACtC,0CAA0C;IAC1C,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,MAAM,EAAE,SAAS,IAAI,EAAE,KAAK,KAAK,CAAC,OAAO,CAAC,CAAC;IAC7D,iCAAiC;IACjC,QAAQ,CAAC,EAAE,EAAE,CAAC,GAAG,MAAM,EAAE,SAAS,IAAI,EAAE,KAAK,KAAK,CAAC,OAAO,CAAC,CAAC;IAC5D;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,MAAM,EAAE,SAAS,IAAI,EAAE,KAAK,KAAK,CAAC,OAAO,CAAC,CAAC;CACjE,CAAC;AAmBF,mBAAmB;AACnB,wBAAgB,MAAM,IAAI,KAAK,CAAC,UAAU,CAAC,CAE1C;AAED,sBAAsB;AACtB,wBAAgB,KAAK,IAAI,KAAK,CAAC,SAAS,CAAC,CAExC;AAED,sCAAsC;AACtC,wBAAgB,UAAU,CAAC,GAAG,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG,KAAK,CAAC,cAAc,CAAC,CAE7E;AAED,wBAAwB;AACxB,wBAAgB,QAAQ,IAAI,KAAK,CAAC,cAAc,CAAC,CAEhD;AAED,2BAA2B;AAC3B,wBAAgB,QAAQ,IAAI,KAAK,CAAC,cAAc,CAAC,CAEhD;AAED;;;GAGG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,IAAI,CAAC,EAAE,MAAM,GACZ,KAAK,CAAC,aAAa,CAAC,CAMtB;AAED,2BAA2B;AAC3B,wBAAgB,KAAK,CAAC,GAAG,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,CAAC,CAElE;AAED,4DAA4D;AAC5D,wBAAgB,GAAG,CAAC,GAAG,KAAK,EAAE,SAAS,IAAI,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,CAE7D;AAED,mDAAmD;AACnD,wBAAgB,GAAG,CAAC,GAAG,KAAK,EAAE,SAAS,IAAI,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,CAE7D;AAED,uCAAuC;AACvC,wBAAgB,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,CAE9C;AAED,oDAAoD;AACpD,wBAAgB,MAAM,CAAC,CAAC,SAAS,cAAc,GAAG,SAAS,GAAG,aAAa,EACzE,IAAI,EAAE,CAAC,EACP,IAAI,EAAE,MAAM,GACX,KAAK,CAAC,CAAC,CAAC,CAEV"}
|
package/dist/build.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A readable way to write a rule, which is also the rule.
|
|
3
|
+
*
|
|
4
|
+
* `weekdays().and(timeOfDay("09:00", "17:00"))` reads better than the nested
|
|
5
|
+
* object literal it stands for — and it *is* that object literal, with methods
|
|
6
|
+
* hanging off it. `JSON.stringify` omits function-valued properties, so a built
|
|
7
|
+
* rule serialises to exactly the document a hand-written one would, with no
|
|
8
|
+
* `.build()` step and nothing to unwrap.
|
|
9
|
+
*
|
|
10
|
+
* That is the whole trick: there is no builder type to convert out of, because
|
|
11
|
+
* a built rule already satisfies `Rule`.
|
|
12
|
+
*/
|
|
13
|
+
function build(node) {
|
|
14
|
+
const self = {
|
|
15
|
+
...node,
|
|
16
|
+
and: (...others) => build({ type: "all", rules: [self, ...others] }),
|
|
17
|
+
or: (...others) => build({ type: "any", rules: [self, ...others] }),
|
|
18
|
+
except: (...others) => build({
|
|
19
|
+
type: "all",
|
|
20
|
+
rules: [
|
|
21
|
+
self,
|
|
22
|
+
{ type: "not", rule: { type: "any", rules: [...others] } },
|
|
23
|
+
],
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
return self;
|
|
27
|
+
}
|
|
28
|
+
/** All of time. */
|
|
29
|
+
export function always() {
|
|
30
|
+
return build({ type: "always" });
|
|
31
|
+
}
|
|
32
|
+
/** No time at all. */
|
|
33
|
+
export function never() {
|
|
34
|
+
return build({ type: "never" });
|
|
35
|
+
}
|
|
36
|
+
/** Whole days, by day of the week. */
|
|
37
|
+
export function daysOfWeek(...days) {
|
|
38
|
+
return build({ type: "daysOfWeek", days });
|
|
39
|
+
}
|
|
40
|
+
/** Monday to Friday. */
|
|
41
|
+
export function weekdays() {
|
|
42
|
+
return daysOfWeek("monday", "tuesday", "wednesday", "thursday", "friday");
|
|
43
|
+
}
|
|
44
|
+
/** Saturday and Sunday. */
|
|
45
|
+
export function weekends() {
|
|
46
|
+
return daysOfWeek("saturday", "sunday");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A wall-clock window within each day. A `to` earlier than `from` wraps past
|
|
50
|
+
* midnight, so `timeOfDay("22:00", "06:00")` is a night shift.
|
|
51
|
+
*/
|
|
52
|
+
export function timeOfDay(from, to, zone) {
|
|
53
|
+
return build(zone === undefined
|
|
54
|
+
? { type: "timeOfDay", from, to }
|
|
55
|
+
: { type: "timeOfDay", from, to, zone });
|
|
56
|
+
}
|
|
57
|
+
/** Whole days, by date. */
|
|
58
|
+
export function dates(...days) {
|
|
59
|
+
return build({ type: "dates", dates: days });
|
|
60
|
+
}
|
|
61
|
+
/** Every one of these must hold. With none, all of time. */
|
|
62
|
+
export function all(...rules) {
|
|
63
|
+
return build({ type: "all", rules });
|
|
64
|
+
}
|
|
65
|
+
/** Any one of these. With none, no time at all. */
|
|
66
|
+
export function any(...rules) {
|
|
67
|
+
return build({ type: "any", rules });
|
|
68
|
+
}
|
|
69
|
+
/** The times a rule does not cover. */
|
|
70
|
+
export function not(rule) {
|
|
71
|
+
return build({ type: "not", rule });
|
|
72
|
+
}
|
|
73
|
+
/** Whole days, by the zone a day is measured in. */
|
|
74
|
+
export function inZone(rule, zone) {
|
|
75
|
+
return build({ ...rule, zone });
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=build.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.js","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layers, and what holds inside them.
|
|
3
|
+
*
|
|
4
|
+
* A `Rule` says *when*. A `Cascade<V>` says *what holds when*: an ordered list
|
|
5
|
+
* of layers, each pairing a scope with what applies inside it, resolved like
|
|
6
|
+
* the rules in a stylesheet — the last layer to claim a moment wins.
|
|
7
|
+
*
|
|
8
|
+
* Keeping values here rather than on `Rule` is what lets the set algebra stay
|
|
9
|
+
* simple. If a rule carried a value then every combinator would be generic in
|
|
10
|
+
* it, and `not` would have to answer what the complement of a rota is. Values
|
|
11
|
+
* only appear where they are actually needed, which is assignment.
|
|
12
|
+
*/
|
|
13
|
+
import type { Interval } from "./interval.js";
|
|
14
|
+
import type { Rule } from "./rule.js";
|
|
15
|
+
/**
|
|
16
|
+
* An interval with a value assigned to it.
|
|
17
|
+
*
|
|
18
|
+
* Extends `Interval`, so everything that reads an interval — `duration`,
|
|
19
|
+
* `contains`, `isEmpty` — reads one of these unchanged.
|
|
20
|
+
*/
|
|
21
|
+
export interface Valued<V> extends Interval {
|
|
22
|
+
readonly value: V;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* One layer: where it applies, and what applies there.
|
|
26
|
+
*
|
|
27
|
+
* The two forms are separate fields rather than one field holding either,
|
|
28
|
+
* because with one field the resolver would have to decide which meaning a
|
|
29
|
+
* value carries by inspecting the shape of the caller's own domain type — and
|
|
30
|
+
* for a `Cascade<Rule>` the two are indistinguishable.
|
|
31
|
+
*/
|
|
32
|
+
export type Layer<V> = ConstantLayer<V> | ReplacingLayer<V>;
|
|
33
|
+
/** A layer assigning one value across the whole of its scope. */
|
|
34
|
+
export interface ConstantLayer<V> {
|
|
35
|
+
readonly scope: Rule;
|
|
36
|
+
readonly value: V;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A layer whose scope is claimed, but whose value inside it comes from another
|
|
40
|
+
* cascade.
|
|
41
|
+
*
|
|
42
|
+
* This is the case a plain value cannot express. "On the eleventh we close at
|
|
43
|
+
* three" is not `(scope: the eleventh, value: closed)`, which would shut the
|
|
44
|
+
* whole day, and writing it as a value over 15:00–17:00 forces the author to
|
|
45
|
+
* know the hours it is overriding — which is the thing a cascade exists to
|
|
46
|
+
* avoid. What it means is: *within this scope, ignore the layers below and use
|
|
47
|
+
* this instead.*
|
|
48
|
+
*/
|
|
49
|
+
export interface ReplacingLayer<V> {
|
|
50
|
+
readonly scope: Rule;
|
|
51
|
+
readonly replace: Cascade<V>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* An ordered list of layers. Later layers win.
|
|
55
|
+
*
|
|
56
|
+
* Order is part of the meaning, so the JSON is an array and reordering it
|
|
57
|
+
* changes the answer.
|
|
58
|
+
*/
|
|
59
|
+
export interface Cascade<V> {
|
|
60
|
+
readonly type: "cascade";
|
|
61
|
+
readonly layers: readonly Layer<V>[];
|
|
62
|
+
}
|
|
63
|
+
/** Whether a value is a cascade rather than a rule. */
|
|
64
|
+
export declare function isCascade<V>(value: Rule | Cascade<V>): value is Cascade<V>;
|
|
65
|
+
/** An ordered list of layers, lowest priority first. */
|
|
66
|
+
export declare function cascade<V>(...layers: readonly Layer<V>[]): Cascade<V>;
|
|
67
|
+
/** One value, across the whole of a scope. */
|
|
68
|
+
export declare function layer<V>(scope: Rule, value: V): ConstantLayer<V>;
|
|
69
|
+
/**
|
|
70
|
+
* True while a rule holds, and unassigned everywhere else.
|
|
71
|
+
*
|
|
72
|
+
* The bridge from *when* to *what*: it is how a plain schedule becomes a
|
|
73
|
+
* cascade, and what {@link replace} lifts a bare rule with.
|
|
74
|
+
*/
|
|
75
|
+
export declare function whenever(rule: Rule): Cascade<boolean>;
|
|
76
|
+
/**
|
|
77
|
+
* A scope claimed outright, with what holds inside it given by another
|
|
78
|
+
* cascade — or, for a schedule, by a rule.
|
|
79
|
+
*
|
|
80
|
+
* The rule form is sugar: it stores the lifted cascade, so the document is the
|
|
81
|
+
* same either way and a stored layer never needs a reader to know which form
|
|
82
|
+
* was written.
|
|
83
|
+
*/
|
|
84
|
+
export declare function replace<V>(scope: Rule, replacement: Cascade<V>): ReplacingLayer<V>;
|
|
85
|
+
export declare function replace(scope: Rule, replacement: Rule): ReplacingLayer<boolean>;
|
|
86
|
+
//# sourceMappingURL=cascade.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cascade.d.ts","sourceRoot":"","sources":["../src/cascade.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEtC;;;;;GAKG;AACH,MAAM,WAAW,MAAM,CAAC,CAAC,CAAE,SAAQ,QAAQ;IACzC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,KAAK,CAAC,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC;AAE5D,iEAAiE;AACjE,MAAM,WAAW,aAAa,CAAC,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;CACnB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;CAC9B;AAED;;;;;GAKG;AACH,MAAM,WAAW,OAAO,CAAC,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,SAAS,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;CACtC;AAED,uDAAuD;AACvD,wBAAgB,SAAS,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,KAAK,IAAI,OAAO,CAAC,CAAC,CAAC,CAE1E;AAED,wDAAwD;AACxD,wBAAgB,OAAO,CAAC,CAAC,EAAE,GAAG,MAAM,EAAE,SAAS,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAErE;AAED,8CAA8C;AAC9C,wBAAgB,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAEhE;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAErD;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,CAAC,EACvB,KAAK,EAAE,IAAI,EACX,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC,GACtB,cAAc,CAAC,CAAC,CAAC,CAAC;AACrB,wBAAgB,OAAO,CACrB,KAAK,EAAE,IAAI,EACX,WAAW,EAAE,IAAI,GAChB,cAAc,CAAC,OAAO,CAAC,CAAC"}
|
package/dist/cascade.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layers, and what holds inside them.
|
|
3
|
+
*
|
|
4
|
+
* A `Rule` says *when*. A `Cascade<V>` says *what holds when*: an ordered list
|
|
5
|
+
* of layers, each pairing a scope with what applies inside it, resolved like
|
|
6
|
+
* the rules in a stylesheet — the last layer to claim a moment wins.
|
|
7
|
+
*
|
|
8
|
+
* Keeping values here rather than on `Rule` is what lets the set algebra stay
|
|
9
|
+
* simple. If a rule carried a value then every combinator would be generic in
|
|
10
|
+
* it, and `not` would have to answer what the complement of a rota is. Values
|
|
11
|
+
* only appear where they are actually needed, which is assignment.
|
|
12
|
+
*/
|
|
13
|
+
/** Whether a value is a cascade rather than a rule. */
|
|
14
|
+
export function isCascade(value) {
|
|
15
|
+
return value.type === "cascade";
|
|
16
|
+
}
|
|
17
|
+
/** An ordered list of layers, lowest priority first. */
|
|
18
|
+
export function cascade(...layers) {
|
|
19
|
+
return { type: "cascade", layers };
|
|
20
|
+
}
|
|
21
|
+
/** One value, across the whole of a scope. */
|
|
22
|
+
export function layer(scope, value) {
|
|
23
|
+
return { scope, value };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* True while a rule holds, and unassigned everywhere else.
|
|
27
|
+
*
|
|
28
|
+
* The bridge from *when* to *what*: it is how a plain schedule becomes a
|
|
29
|
+
* cascade, and what {@link replace} lifts a bare rule with.
|
|
30
|
+
*/
|
|
31
|
+
export function whenever(rule) {
|
|
32
|
+
return cascade(layer(rule, true));
|
|
33
|
+
}
|
|
34
|
+
export function replace(scope, replacement) {
|
|
35
|
+
return {
|
|
36
|
+
scope,
|
|
37
|
+
replace: isCascade(replacement) ? replacement : whenever(replacement),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=cascade.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cascade.js","sourceRoot":"","sources":["../src/cascade.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AA0DH,uDAAuD;AACvD,MAAM,UAAU,SAAS,CAAI,KAAwB;IACnD,OAAO,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC;AAClC,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,OAAO,CAAI,GAAG,MAA2B;IACvD,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;AACrC,CAAC;AAED,8CAA8C;AAC9C,MAAM,UAAU,KAAK,CAAI,KAAW,EAAE,KAAQ;IAC5C,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAC1B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAU;IACjC,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;AACpC,CAAC;AAkBD,MAAM,UAAU,OAAO,CACrB,KAAW,EACX,WAAoC;IAEpC,OAAO;QACL,KAAK;QACL,OAAO,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC;KACtE,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a rule is evaluated against.
|
|
3
|
+
*
|
|
4
|
+
* An object rather than a bare window, because rules need more than a window
|
|
5
|
+
* and always will: a sunrise rule needs coordinates, a rule written in the
|
|
6
|
+
* Hebrew calendar needs the calendar, a rendered description needs the locale.
|
|
7
|
+
* Adding a field here is harmless; changing a bare parameter into an object
|
|
8
|
+
* later would break every rule implementation, including anyone else's.
|
|
9
|
+
*/
|
|
10
|
+
export interface Context {
|
|
11
|
+
/**
|
|
12
|
+
* Where evaluation begins, and — since a `ZonedDateTime` carries one — the
|
|
13
|
+
* time zone any rule that does not name its own is read in. One source of
|
|
14
|
+
* truth: there is no way for a separate `zone` field to disagree with this.
|
|
15
|
+
*/
|
|
16
|
+
readonly from: Temporal.ZonedDateTime;
|
|
17
|
+
/**
|
|
18
|
+
* Where evaluation stops. Optional, because a recurrence genuinely has no
|
|
19
|
+
* end and pretending otherwise would have callers guessing a window big
|
|
20
|
+
* enough to hold an answer they cannot predict.
|
|
21
|
+
*
|
|
22
|
+
* Leaving it out means the streams a rule produces may be endless. That is
|
|
23
|
+
* supported and sometimes what you want — `take(…, 3)` over an endless
|
|
24
|
+
* stream is exact and cheap — but a composition whose answer is empty then
|
|
25
|
+
* has nothing to discover that from, and will not finish. Bound the window
|
|
26
|
+
* when the answer might be empty.
|
|
27
|
+
*/
|
|
28
|
+
readonly to?: Temporal.ZonedDateTime;
|
|
29
|
+
/** Where on Earth, for rules about the sun, the moon or the tide. */
|
|
30
|
+
readonly location?: {
|
|
31
|
+
readonly latitude: number;
|
|
32
|
+
readonly longitude: number;
|
|
33
|
+
};
|
|
34
|
+
/** For rendering a rule as text, and for locale-specific conventions. */
|
|
35
|
+
readonly locale?: string;
|
|
36
|
+
}
|
|
37
|
+
/** The context's window, in the form the interval algebra takes. */
|
|
38
|
+
export declare function windowOf(context: Context): {
|
|
39
|
+
readonly start: Temporal.ZonedDateTime;
|
|
40
|
+
readonly end: Temporal.ZonedDateTime | undefined;
|
|
41
|
+
};
|
|
42
|
+
/** The zone a rule is read in when it does not name one of its own. */
|
|
43
|
+
export declare function zoneOf(context: Context, override?: string): string;
|
|
44
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,WAAW,OAAO;IACtB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,aAAa,CAAC;IAEtC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,aAAa,CAAC;IAErC,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,CAAC,EAAE;QAClB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;KAC5B,CAAC;IAEF,yEAAyE;IACzE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,oEAAoE;AACpE,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG;IAC1C,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,aAAa,CAAC;IACvC,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,aAAa,GAAG,SAAS,CAAC;CAClD,CAEA;AAED,uEAAuE;AACvE,wBAAgB,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAElE"}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** The context's window, in the form the interval algebra takes. */
|
|
2
|
+
export function windowOf(context) {
|
|
3
|
+
return { start: context.from, end: context.to };
|
|
4
|
+
}
|
|
5
|
+
/** The zone a rule is read in when it does not name one of its own. */
|
|
6
|
+
export function zoneOf(context, override) {
|
|
7
|
+
return override ?? context.from.timeZoneId;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAwCA,oEAAoE;AACpE,MAAM,UAAU,QAAQ,CAAC,OAAgB;IAIvC,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,IAAI,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC;AAClD,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,MAAM,CAAC,OAAgB,EAAE,QAAiB;IACxD,OAAO,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC;AAC7C,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rules that select whole days: by weekday, and by date.
|
|
3
|
+
*
|
|
4
|
+
* Both uphold the stream contract — sorted, not overlapping, coalesced — and
|
|
5
|
+
* the coalescing is the fiddly part. Two selected days that happen to be
|
|
6
|
+
* consecutive are one interval, not two that touch at midnight, and a stream of
|
|
7
|
+
* touching intervals is one the sweeps in `interval-stream.ts` read wrongly.
|
|
8
|
+
*/
|
|
9
|
+
import { type Context } from "./context.js";
|
|
10
|
+
import type { IntervalStream } from "./interval-stream.js";
|
|
11
|
+
import { type Weekday } from "./rule.js";
|
|
12
|
+
/** Whole days selected by day of the week. */
|
|
13
|
+
export declare function weekdayIntervals(context: Context, days: readonly Weekday[], zone?: string): IntervalStream;
|
|
14
|
+
/**
|
|
15
|
+
* Whole days named by date.
|
|
16
|
+
*
|
|
17
|
+
* Walks the given dates rather than the calendar, so a handful of dates costs a
|
|
18
|
+
* handful of steps however far apart they are, and the stream ends when they
|
|
19
|
+
* do. Sorted and de-duplicated first, because the contract is about the output
|
|
20
|
+
* and callers write dates in whatever order they think of them.
|
|
21
|
+
*/
|
|
22
|
+
export declare function dateIntervals(context: Context, dates: readonly string[], zone?: string): IntervalStream;
|
|
23
|
+
//# sourceMappingURL=day-rules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"day-rules.d.ts","sourceRoot":"","sources":["../src/day-rules.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,KAAK,OAAO,EAAU,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAY,KAAK,OAAO,EAAE,MAAM,WAAW,CAAC;AAsDnD,8CAA8C;AAC9C,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,SAAS,OAAO,EAAE,EACxB,IAAI,CAAC,EAAE,MAAM,GACZ,cAAc,CAehB;AAED;;;;;;;GAOG;AACH,wBAAiB,aAAa,CAC5B,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,IAAI,CAAC,EAAE,MAAM,GACZ,cAAc,CAiChB"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rules that select whole days: by weekday, and by date.
|
|
3
|
+
*
|
|
4
|
+
* Both uphold the stream contract — sorted, not overlapping, coalesced — and
|
|
5
|
+
* the coalescing is the fiddly part. Two selected days that happen to be
|
|
6
|
+
* consecutive are one interval, not two that touch at midnight, and a stream of
|
|
7
|
+
* touching intervals is one the sweeps in `interval-stream.ts` read wrongly.
|
|
8
|
+
*/
|
|
9
|
+
import { zoneOf } from "./context.js";
|
|
10
|
+
import { WEEKDAYS } from "./rule.js";
|
|
11
|
+
function startOfDay(date, zone) {
|
|
12
|
+
return date.toZonedDateTime({ timeZone: zone, plainTime: "00:00" });
|
|
13
|
+
}
|
|
14
|
+
function weekdayOf(date) {
|
|
15
|
+
return WEEKDAYS[date.dayOfWeek - 1];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Whole days matching a predicate, walked forward from the context, with runs
|
|
19
|
+
* of consecutive matches merged into one interval.
|
|
20
|
+
*
|
|
21
|
+
* Endless unless the context bounds it. The run being built is flushed when the
|
|
22
|
+
* window ends, so a rule that matches every day still terminates — without
|
|
23
|
+
* that, a run that never closes would never yield anything at all.
|
|
24
|
+
*/
|
|
25
|
+
function* matchingDays(context, zone, matches) {
|
|
26
|
+
const stop = context.to;
|
|
27
|
+
let date = context.from.withTimeZone(zone).toPlainDate();
|
|
28
|
+
let runStart;
|
|
29
|
+
for (;;) {
|
|
30
|
+
const dayStart = startOfDay(date, zone);
|
|
31
|
+
if (stop !== undefined &&
|
|
32
|
+
Temporal.ZonedDateTime.compare(dayStart, stop) >= 0) {
|
|
33
|
+
if (runStart !== undefined) {
|
|
34
|
+
yield { start: startOfDay(runStart, zone), end: dayStart };
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (matches(date)) {
|
|
39
|
+
runStart ??= date;
|
|
40
|
+
}
|
|
41
|
+
else if (runStart !== undefined) {
|
|
42
|
+
yield { start: startOfDay(runStart, zone), end: dayStart };
|
|
43
|
+
runStart = undefined;
|
|
44
|
+
}
|
|
45
|
+
date = date.add({ days: 1 });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Whole days selected by day of the week. */
|
|
49
|
+
export function weekdayIntervals(context, days, zone) {
|
|
50
|
+
const wanted = new Set(days);
|
|
51
|
+
// Nothing can match, so there is nothing to walk the calendar for. Without
|
|
52
|
+
// this, an unbounded context sends `matchingDays` forward a day at a time
|
|
53
|
+
// until Temporal's year limit, thousands of centuries later, and reports it
|
|
54
|
+
// as a date range error rather than as the empty rule it is.
|
|
55
|
+
if (wanted.size === 0) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
return matchingDays(context, zoneOf(context, zone), (date) => {
|
|
59
|
+
const weekday = weekdayOf(date);
|
|
60
|
+
return weekday !== undefined && wanted.has(weekday);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Whole days named by date.
|
|
65
|
+
*
|
|
66
|
+
* Walks the given dates rather than the calendar, so a handful of dates costs a
|
|
67
|
+
* handful of steps however far apart they are, and the stream ends when they
|
|
68
|
+
* do. Sorted and de-duplicated first, because the contract is about the output
|
|
69
|
+
* and callers write dates in whatever order they think of them.
|
|
70
|
+
*/
|
|
71
|
+
export function* dateIntervals(context, dates, zone) {
|
|
72
|
+
const inZone = zoneOf(context, zone);
|
|
73
|
+
const days = [
|
|
74
|
+
...new Set(dates.map((date) => Temporal.PlainDate.from(date).toString())),
|
|
75
|
+
]
|
|
76
|
+
.toSorted()
|
|
77
|
+
.map((date) => Temporal.PlainDate.from(date));
|
|
78
|
+
let runStart;
|
|
79
|
+
let runEnd;
|
|
80
|
+
for (const day of days) {
|
|
81
|
+
if (runEnd !== undefined && Temporal.PlainDate.compare(day, runEnd) === 0) {
|
|
82
|
+
// Consecutive with the run so far, so it extends rather than starts one.
|
|
83
|
+
runEnd = day.add({ days: 1 });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (runStart !== undefined && runEnd !== undefined) {
|
|
87
|
+
yield {
|
|
88
|
+
start: startOfDay(runStart, inZone),
|
|
89
|
+
end: startOfDay(runEnd, inZone),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
runStart = day;
|
|
93
|
+
runEnd = day.add({ days: 1 });
|
|
94
|
+
}
|
|
95
|
+
if (runStart !== undefined && runEnd !== undefined) {
|
|
96
|
+
yield {
|
|
97
|
+
start: startOfDay(runStart, inZone),
|
|
98
|
+
end: startOfDay(runEnd, inZone),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=day-rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"day-rules.js","sourceRoot":"","sources":["../src/day-rules.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,8 +2,14 @@
|
|
|
2
2
|
* Quando: declarative temporal rules for schedules, deadlines, constraints and
|
|
3
3
|
* exceptions.
|
|
4
4
|
*
|
|
5
|
-
* What exists so far is the interval core
|
|
6
|
-
*
|
|
5
|
+
* What exists so far is the interval core, the rule language on top of it, and
|
|
6
|
+
* the two ends of "rules are data": a builder that writes one, and a parser
|
|
7
|
+
* that reads one back from whatever a database or a form actually held.
|
|
8
|
+
* Queries sit on top: is it open now, how much working time is in this window,
|
|
9
|
+
* when does it next open, and where do you get to after three hours that only
|
|
10
|
+
* count while it is open. Cascades sit beside them: ordered layers that carry
|
|
11
|
+
* values, resolved by precedence, for the questions a boolean schedule cannot
|
|
12
|
+
* answer.
|
|
7
13
|
*
|
|
8
14
|
* Requires a runtime with `Temporal`: Node 26 or later, or a browser that
|
|
9
15
|
* implements it.
|
|
@@ -16,4 +22,22 @@ export { compareEnds, compareStarts, contains, duration, isEmpty, startsAtOrBefo
|
|
|
16
22
|
export type { IntervalStream } from "./interval-stream.js";
|
|
17
23
|
export { clip, complement, intersect, union } from "./interval-stream.js";
|
|
18
24
|
export { take } from "./stream.js";
|
|
25
|
+
export type { Context } from "./context.js";
|
|
26
|
+
export type { AllRule, AnyRule, AlwaysRule, DatesRule, DaysOfWeekRule, NeverRule, NotRule, Rule, TimeOfDayRule, Weekday, } from "./rule.js";
|
|
27
|
+
export { WEEKDAYS } from "./rule.js";
|
|
28
|
+
export { intervals } from "./interpret.js";
|
|
29
|
+
export type { Built } from "./build.js";
|
|
30
|
+
export { all, always, any, dates, daysOfWeek, inZone, never, not, timeOfDay, weekdays, weekends, } from "./build.js";
|
|
31
|
+
export { parseRule } from "./parse.js";
|
|
32
|
+
export type { Cascade, ConstantLayer, Layer, ReplacingLayer, Valued, } from "./cascade.js";
|
|
33
|
+
export { cascade, isCascade, layer, replace, whenever } from "./cascade.js";
|
|
34
|
+
export type { ValuedStream } from "./valued-stream.js";
|
|
35
|
+
export { resolve } from "./resolve.js";
|
|
36
|
+
export type { PlainRule } from "./plain-forms.js";
|
|
37
|
+
export type { Schedule } from "./schedule.js";
|
|
38
|
+
export { schedule } from "./schedule.js";
|
|
39
|
+
export type { Rota } from "./rota.js";
|
|
40
|
+
export { rota } from "./rota.js";
|
|
41
|
+
export type { Search } from "./query.js";
|
|
42
|
+
export { activeAt, advanceBy, elapsed, next } from "./query.js";
|
|
19
43
|
//# sourceMappingURL=index.d.ts.map
|