@kensio/quando 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +135 -4
- package/dist/assigned.d.ts +68 -0
- package/dist/assigned.d.ts.map +1 -0
- package/dist/assigned.js +84 -0
- package/dist/assigned.js.map +1 -0
- package/dist/build.d.ts +54 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +77 -0
- package/dist/build.js.map +1 -0
- package/dist/canonical-rule.d.ts +23 -0
- package/dist/canonical-rule.d.ts.map +1 -0
- package/dist/canonical-rule.js +139 -0
- package/dist/canonical-rule.js.map +1 -0
- package/dist/canonical.d.ts +44 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +53 -0
- package/dist/canonical.js.map +1 -0
- package/dist/cascade.d.ts +107 -0
- package/dist/cascade.d.ts.map +1 -0
- package/dist/cascade.js +53 -0
- package/dist/cascade.js.map +1 -0
- package/dist/context.d.ts +44 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +9 -0
- package/dist/context.js.map +1 -0
- package/dist/day-rules.d.ts +23 -0
- package/dist/day-rules.d.ts.map +1 -0
- package/dist/day-rules.js +102 -0
- package/dist/day-rules.js.map +1 -0
- package/dist/index.d.ts +37 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -2
- package/dist/index.js.map +1 -1
- package/dist/interpret.d.ts +31 -0
- package/dist/interpret.d.ts.map +1 -0
- package/dist/interpret.js +99 -0
- package/dist/interpret.js.map +1 -0
- package/dist/interval-stream.d.ts +11 -4
- package/dist/interval-stream.d.ts.map +1 -1
- package/dist/interval-stream.js.map +1 -1
- package/dist/merge.d.ts +43 -0
- package/dist/merge.d.ts.map +1 -0
- package/dist/merge.js +79 -0
- package/dist/merge.js.map +1 -0
- package/dist/parse-cascade.d.ts +37 -0
- package/dist/parse-cascade.d.ts.map +1 -0
- package/dist/parse-cascade.js +105 -0
- package/dist/parse-cascade.js.map +1 -0
- package/dist/parse-fields.d.ts +27 -0
- package/dist/parse-fields.d.ts.map +1 -0
- package/dist/parse-fields.js +65 -0
- package/dist/parse-fields.js.map +1 -0
- package/dist/parse-shape.d.ts +39 -0
- package/dist/parse-shape.d.ts.map +1 -0
- package/dist/parse-shape.js +77 -0
- package/dist/parse-shape.js.map +1 -0
- package/dist/parse.d.ts +22 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +93 -0
- package/dist/parse.js.map +1 -0
- package/dist/plain-forms.d.ts +22 -0
- package/dist/plain-forms.d.ts.map +1 -0
- package/dist/plain-forms.js +54 -0
- package/dist/plain-forms.js.map +1 -0
- package/dist/query.d.ts +73 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +127 -0
- package/dist/query.js.map +1 -0
- package/dist/resolve.d.ts +31 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +95 -0
- package/dist/resolve.js.map +1 -0
- package/dist/rota.d.ts +49 -0
- package/dist/rota.d.ts.map +1 -0
- package/dist/rota.js +41 -0
- package/dist/rota.js.map +1 -0
- package/dist/rule.d.ts +73 -0
- package/dist/rule.d.ts.map +1 -0
- package/dist/rule.js +20 -0
- package/dist/rule.js.map +1 -0
- package/dist/schedule.d.ts +69 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +80 -0
- package/dist/schedule.js.map +1 -0
- package/dist/tally.d.ts +76 -0
- package/dist/tally.d.ts.map +1 -0
- package/dist/tally.js +80 -0
- package/dist/tally.js.map +1 -0
- package/dist/time-rules.d.ts +18 -0
- package/dist/time-rules.d.ts.map +1 -0
- package/dist/time-rules.js +55 -0
- package/dist/time-rules.js.map +1 -0
- package/dist/valued-stream.d.ts +51 -0
- package/dist/valued-stream.d.ts.map +1 -0
- package/dist/valued-stream.js +148 -0
- package/dist/valued-stream.js.map +1 -0
- package/package.json +22 -20
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;AAGH,OAAO,EAAE,KAAK,OAAO,EAAqB,MAAM,cAAc,CAAC;AAC/D,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE1D,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;AAsBD;;;;;;;;;GASG;AACH,wBAAgB,IAAI,CAAC,CAAC,GAAG,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,CAEzC"}
|
package/dist/rota.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
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 { valueAt } from "./assigned.js";
|
|
14
|
+
import { layer } from "./cascade.js";
|
|
15
|
+
import { asDays } from "./plain-forms.js";
|
|
16
|
+
import { resolve } from "./resolve.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) => valueAt(self, at),
|
|
24
|
+
shifts: (from, to) => resolve(self, to === undefined ? { from } : { from, to }),
|
|
25
|
+
};
|
|
26
|
+
return self;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* An empty rota: nobody is on until something says they are.
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* const onCall = rota()
|
|
33
|
+
* .assign(weekdays(), "alice")
|
|
34
|
+
* .assign(weekends(), "bob")
|
|
35
|
+
* .swap("2026-03-11", "carol");
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export function rota() {
|
|
39
|
+
return build([]);
|
|
40
|
+
}
|
|
41
|
+
//# 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,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAA4B,KAAK,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,EAAE,MAAM,EAAkB,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAgCvC,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,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;QAElC,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;AAIH,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;AAyDD;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,IAAI,QAAQ,CAEnC"}
|
package/dist/schedule.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
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 { valueAt } from "./assigned.js";
|
|
15
|
+
import { all } from "./build.js";
|
|
16
|
+
import { layer, replace } from "./cascade.js";
|
|
17
|
+
import { duration } from "./interval.js";
|
|
18
|
+
import { asDays, asHours } from "./plain-forms.js";
|
|
19
|
+
import { resolve } from "./resolve.js";
|
|
20
|
+
import { take } from "./stream.js";
|
|
21
|
+
/** Zero, as a duration to accumulate onto. */
|
|
22
|
+
const NOTHING = Temporal.Duration.from({ seconds: 0 });
|
|
23
|
+
function open(scope, hours) {
|
|
24
|
+
const when = asDays(scope);
|
|
25
|
+
return layer(hours === undefined ? when : all(when, asHours(hours)), true);
|
|
26
|
+
}
|
|
27
|
+
function build(layers) {
|
|
28
|
+
const self = {
|
|
29
|
+
type: "cascade",
|
|
30
|
+
layers,
|
|
31
|
+
open: (scope, hours) => build([...layers, open(scope, hours)]),
|
|
32
|
+
closed: (scope) => build([...layers, layer(asDays(scope), false)]),
|
|
33
|
+
hoursOn: (day, hours) => build([...layers, replace(asDays(day), asHours(hours))]),
|
|
34
|
+
isOpen: (at) => valueAt(self, at) ?? false,
|
|
35
|
+
opensNext: (at, within) => {
|
|
36
|
+
const search = within === undefined ? { from: at } : { from: at, to: at.add(within) };
|
|
37
|
+
for (const period of resolve(self, search)) {
|
|
38
|
+
if (!period.value) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (within === undefined || period.start === undefined) {
|
|
42
|
+
return period;
|
|
43
|
+
}
|
|
44
|
+
// The horizon bounds how far to look, not what is found. Everything
|
|
45
|
+
// resolved against a context is clipped to it, so a stretch that runs
|
|
46
|
+
// past the horizon comes back ending at the horizon — which reads as a
|
|
47
|
+
// closing time and is not one. Read it again from its own start, with
|
|
48
|
+
// nothing to clip it, so the end is when it really closes.
|
|
49
|
+
const [whole] = take(resolve(self, { from: period.start }), 1);
|
|
50
|
+
return whole ?? period;
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
},
|
|
54
|
+
openBetween: (from, to) => {
|
|
55
|
+
let total = NOTHING;
|
|
56
|
+
for (const period of resolve(self, { from, to })) {
|
|
57
|
+
const length = period.value ? duration(period) : undefined;
|
|
58
|
+
if (length !== undefined) {
|
|
59
|
+
total = total.add(length);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return total.round({ largestUnit: "hour" });
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
return self;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* An empty schedule: open for nothing until something says otherwise.
|
|
69
|
+
*
|
|
70
|
+
* ```ts
|
|
71
|
+
* const openingHours = schedule()
|
|
72
|
+
* .open(weekdays(), "09:00-17:00")
|
|
73
|
+
* .closed("2026-12-25")
|
|
74
|
+
* .hoursOn("2026-03-11", "09:00-15:00");
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function schedule() {
|
|
78
|
+
return build([]);
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=schedule.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schedule.js","sourceRoot":"","sources":["../src/schedule.ts"],"names":[],"mappings":"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"}
|
package/dist/tally.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many, in the words people use for counting.
|
|
3
|
+
*
|
|
4
|
+
* The same relationship to [merging](./merge.ts) that a
|
|
5
|
+
* [schedule](./schedule.ts) has to a cascade. `merged("sum", layer(…),
|
|
6
|
+
* layer(…))` says what it does, and it asks the reader to know three things
|
|
7
|
+
* first: that layers overlap, that an overlap is what combines, and what a
|
|
8
|
+
* bare `"sum"` in the first argument governs. Nobody staffing a warehouse
|
|
9
|
+
* says any of that. They say three on weekdays, two more on the eleventh.
|
|
10
|
+
*
|
|
11
|
+
* A `Tally` *is* a `Cascade<number>` with `merge: "sum"`, so everything that
|
|
12
|
+
* takes a cascade takes one of these, and it serialises to the document a
|
|
13
|
+
* hand-written one would.
|
|
14
|
+
*/
|
|
15
|
+
import { type Cascade } from "./cascade.js";
|
|
16
|
+
import { type PlainRule } from "./plain-forms.js";
|
|
17
|
+
import type { ValuedStream } from "./valued-stream.js";
|
|
18
|
+
/** How many there are over time, and the questions worth asking about that. */
|
|
19
|
+
export interface Tally extends Cascade<number> {
|
|
20
|
+
/**
|
|
21
|
+
* That much more, on top of whatever else covers the same time.
|
|
22
|
+
*
|
|
23
|
+
* The verb a tally is mostly written in. Two teams each putting three
|
|
24
|
+
* people on a Monday have six people on that Monday.
|
|
25
|
+
*/
|
|
26
|
+
readonly plus: (scope: PlainRule, amount: number) => Tally;
|
|
27
|
+
/**
|
|
28
|
+
* That many for these times, in place of whatever was said before.
|
|
29
|
+
*
|
|
30
|
+
* Instead of, not as well as. A skeleton crew on Christmas Eve is a figure
|
|
31
|
+
* that replaces the usual one, and writing it as a `plus` would need the
|
|
32
|
+
* author to know what they were adding to.
|
|
33
|
+
*
|
|
34
|
+
* "Exactly" is about the figure rather than the last word on it. This
|
|
35
|
+
* outranks every line above it, and a `plus` written afterwards still adds,
|
|
36
|
+
* the same way `hoursOn` works on a [schedule](./schedule.ts).
|
|
37
|
+
*/
|
|
38
|
+
readonly exactly: (scope: PlainRule, amount: number) => Tally;
|
|
39
|
+
/**
|
|
40
|
+
* How many at that moment.
|
|
41
|
+
*
|
|
42
|
+
* Zero where no layer claims the moment. A cascade leaves an unclaimed
|
|
43
|
+
* moment out of its stream, and nobody rostered is nobody there, so this
|
|
44
|
+
* reads it as the figure it is. The same call
|
|
45
|
+
* [`isOpen`](./schedule.ts) makes for a schedule.
|
|
46
|
+
*/
|
|
47
|
+
readonly at: (at: Temporal.ZonedDateTime) => number;
|
|
48
|
+
/**
|
|
49
|
+
* The lowest figure anywhere between two moments.
|
|
50
|
+
*
|
|
51
|
+
* The question capacity is really asking. A stretch that no layer claims
|
|
52
|
+
* counts as zero, so a window with a gap in it answers zero however well
|
|
53
|
+
* covered the rest of it is.
|
|
54
|
+
*/
|
|
55
|
+
readonly least: (from: Temporal.ZonedDateTime, to: Temporal.ZonedDateTime) => number;
|
|
56
|
+
/**
|
|
57
|
+
* Each stretch between two moments, and how many are on for it.
|
|
58
|
+
*
|
|
59
|
+
* Leave `to` out for an endless run of them, which is lazy and safe to stop
|
|
60
|
+
* pulling from whenever you have enough.
|
|
61
|
+
*/
|
|
62
|
+
readonly counts: (from: Temporal.ZonedDateTime, to?: Temporal.ZonedDateTime) => ValuedStream<number>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* An empty tally: nobody, until something says otherwise.
|
|
66
|
+
*
|
|
67
|
+
* ```ts
|
|
68
|
+
* const staff = tally()
|
|
69
|
+
* .plus(weekdays(), 3)
|
|
70
|
+
* .plus(weekends(), 1)
|
|
71
|
+
* .plus("2026-03-11", 2)
|
|
72
|
+
* .exactly("2026-12-24", 1);
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export declare function tally(): Tally;
|
|
76
|
+
//# sourceMappingURL=tally.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tally.d.ts","sourceRoot":"","sources":["../src/tally.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,EACL,KAAK,OAAO,EAKb,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAKvD,+EAA+E;AAC/E,MAAM,WAAW,KAAM,SAAQ,OAAO,CAAC,MAAM,CAAC;IAC5C;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,KAAK,KAAK,CAAC;IAE3D;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,KAAK,KAAK,CAAC;IAE9D;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,QAAQ,CAAC,aAAa,KAAK,MAAM,CAAC;IAEpD;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,EAAE,CACd,IAAI,EAAE,QAAQ,CAAC,aAAa,EAC5B,EAAE,EAAE,QAAQ,CAAC,aAAa,KACvB,MAAM,CAAC;IAEZ;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,CACf,IAAI,EAAE,QAAQ,CAAC,aAAa,EAC5B,EAAE,CAAC,EAAE,QAAQ,CAAC,aAAa,KACxB,YAAY,CAAC,MAAM,CAAC,CAAC;CAC3B;AAwDD;;;;;;;;;;GAUG;AACH,wBAAgB,KAAK,IAAI,KAAK,CAE7B"}
|
package/dist/tally.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many, in the words people use for counting.
|
|
3
|
+
*
|
|
4
|
+
* The same relationship to [merging](./merge.ts) that a
|
|
5
|
+
* [schedule](./schedule.ts) has to a cascade. `merged("sum", layer(…),
|
|
6
|
+
* layer(…))` says what it does, and it asks the reader to know three things
|
|
7
|
+
* first: that layers overlap, that an overlap is what combines, and what a
|
|
8
|
+
* bare `"sum"` in the first argument governs. Nobody staffing a warehouse
|
|
9
|
+
* says any of that. They say three on weekdays, two more on the eleventh.
|
|
10
|
+
*
|
|
11
|
+
* A `Tally` *is* a `Cascade<number>` with `merge: "sum"`, so everything that
|
|
12
|
+
* takes a cascade takes one of these, and it serialises to the document a
|
|
13
|
+
* hand-written one would.
|
|
14
|
+
*/
|
|
15
|
+
import { valueAt } from "./assigned.js";
|
|
16
|
+
import { always } from "./build.js";
|
|
17
|
+
import { cascade, layer, replace, } from "./cascade.js";
|
|
18
|
+
import { duration } from "./interval.js";
|
|
19
|
+
import { asDays } from "./plain-forms.js";
|
|
20
|
+
import { resolve } from "./resolve.js";
|
|
21
|
+
/** Zero, as a duration to accumulate onto. */
|
|
22
|
+
const NOTHING = Temporal.Duration.from({ seconds: 0 });
|
|
23
|
+
/**
|
|
24
|
+
* A figure that claims its scope outright.
|
|
25
|
+
*
|
|
26
|
+
* The inner cascade covers the whole of the region this layer wins, which is
|
|
27
|
+
* what `always` means once it is resolved against that region rather than
|
|
28
|
+
* against the context.
|
|
29
|
+
*/
|
|
30
|
+
function fixed(scope, amount) {
|
|
31
|
+
const figure = cascade(layer(always(), amount));
|
|
32
|
+
return replace(asDays(scope), figure);
|
|
33
|
+
}
|
|
34
|
+
function build(layers) {
|
|
35
|
+
const self = {
|
|
36
|
+
type: "cascade",
|
|
37
|
+
merge: "sum",
|
|
38
|
+
layers,
|
|
39
|
+
plus: (scope, amount) => build([...layers, layer(asDays(scope), amount)]),
|
|
40
|
+
exactly: (scope, amount) => build([...layers, fixed(scope, amount)]),
|
|
41
|
+
at: (at) => valueAt(self, at) ?? 0,
|
|
42
|
+
least: (from, to) => {
|
|
43
|
+
let lowest;
|
|
44
|
+
let covered = NOTHING;
|
|
45
|
+
for (const span of resolve(self, { from, to })) {
|
|
46
|
+
lowest =
|
|
47
|
+
lowest === undefined ? span.value : Math.min(lowest, span.value);
|
|
48
|
+
const length = duration(span);
|
|
49
|
+
if (length !== undefined) {
|
|
50
|
+
covered = covered.add(length);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Anything the layers left uncovered is nobody there, which is lower
|
|
54
|
+
// than any figure they assigned. Comparing what was covered against the
|
|
55
|
+
// window finds that without a second sweep for the gaps.
|
|
56
|
+
const window = from.until(to, { largestUnit: "hour" });
|
|
57
|
+
if (Temporal.Duration.compare(covered, window) < 0) {
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
return lowest ?? 0;
|
|
61
|
+
},
|
|
62
|
+
counts: (from, to) => resolve(self, to === undefined ? { from } : { from, to }),
|
|
63
|
+
};
|
|
64
|
+
return self;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* An empty tally: nobody, until something says otherwise.
|
|
68
|
+
*
|
|
69
|
+
* ```ts
|
|
70
|
+
* const staff = tally()
|
|
71
|
+
* .plus(weekdays(), 3)
|
|
72
|
+
* .plus(weekends(), 1)
|
|
73
|
+
* .plus("2026-03-11", 2)
|
|
74
|
+
* .exactly("2026-12-24", 1);
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function tally() {
|
|
78
|
+
return build([]);
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=tally.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tally.js","sourceRoot":"","sources":["../src/tally.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rule that selects a wall-clock window within each day.
|
|
3
|
+
*
|
|
4
|
+
* Wall clock rather than elapsed time is the whole point: across a daylight
|
|
5
|
+
* saving transition the clock times stay put and the real length of the window
|
|
6
|
+
* changes, which is what a schedule means by "nine to five".
|
|
7
|
+
*/
|
|
8
|
+
import { type Context } from "./context.js";
|
|
9
|
+
import type { IntervalStream } from "./interval-stream.js";
|
|
10
|
+
/**
|
|
11
|
+
* A wall-clock window within each day, endless unless the context bounds it.
|
|
12
|
+
*
|
|
13
|
+
* Starts a day earlier than the context does, because a window that wraps past
|
|
14
|
+
* midnight may have opened yesterday and still be running. Clipping to the
|
|
15
|
+
* window drops whatever that turns up too early.
|
|
16
|
+
*/
|
|
17
|
+
export declare function timeOfDayIntervals(context: Context, from: string, to: string, zone?: string): IntervalStream;
|
|
18
|
+
//# sourceMappingURL=time-rules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-rules.d.ts","sourceRoot":"","sources":["../src/time-rules.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,OAAO,EAAU,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D;;;;;;GAMG;AACH,wBAAiB,kBAAkB,CACjC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,IAAI,CAAC,EAAE,MAAM,GACZ,cAAc,CAgDhB"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rule that selects a wall-clock window within each day.
|
|
3
|
+
*
|
|
4
|
+
* Wall clock rather than elapsed time is the whole point: across a daylight
|
|
5
|
+
* saving transition the clock times stay put and the real length of the window
|
|
6
|
+
* changes, which is what a schedule means by "nine to five".
|
|
7
|
+
*/
|
|
8
|
+
import { zoneOf } from "./context.js";
|
|
9
|
+
/**
|
|
10
|
+
* A wall-clock window within each day, endless unless the context bounds it.
|
|
11
|
+
*
|
|
12
|
+
* Starts a day earlier than the context does, because a window that wraps past
|
|
13
|
+
* midnight may have opened yesterday and still be running. Clipping to the
|
|
14
|
+
* window drops whatever that turns up too early.
|
|
15
|
+
*/
|
|
16
|
+
export function* timeOfDayIntervals(context, from, to, zone) {
|
|
17
|
+
const inZone = zoneOf(context, zone);
|
|
18
|
+
const opens = Temporal.PlainTime.from(from);
|
|
19
|
+
const closes = Temporal.PlainTime.from(to);
|
|
20
|
+
if (Temporal.PlainTime.compare(opens, closes) === 0) {
|
|
21
|
+
throw new RangeError(`A time-of-day window from ${from} to ${to} has the same start and end. ` +
|
|
22
|
+
`Use { type: "always" } for a whole day.`);
|
|
23
|
+
}
|
|
24
|
+
// Earlier `to` than `from` means the window runs past midnight into the day
|
|
25
|
+
// after — a night shift, not an empty window.
|
|
26
|
+
const wraps = Temporal.PlainTime.compare(closes, opens) < 0;
|
|
27
|
+
const stop = context.to;
|
|
28
|
+
let date = context.from
|
|
29
|
+
.withTimeZone(inZone)
|
|
30
|
+
.toPlainDate()
|
|
31
|
+
.subtract({ days: 1 });
|
|
32
|
+
for (;;) {
|
|
33
|
+
const start = date.toZonedDateTime({ timeZone: inZone, plainTime: opens });
|
|
34
|
+
if (stop !== undefined &&
|
|
35
|
+
Temporal.ZonedDateTime.compare(start, stop) >= 0) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const closing = wraps ? date.add({ days: 1 }) : date;
|
|
39
|
+
const end = closing.toZonedDateTime({
|
|
40
|
+
timeZone: inZone,
|
|
41
|
+
plainTime: closes,
|
|
42
|
+
});
|
|
43
|
+
// A window can collapse to nothing on the morning clocks go forward. Both
|
|
44
|
+
// ends of 01:00-02:00 in London on 2026-03-29 resolve to the same instant,
|
|
45
|
+
// because the hour between them does not exist and Temporal's default
|
|
46
|
+
// disambiguation moves a nonexistent time forward to the far side of the
|
|
47
|
+
// gap. Yielding that would put a zero-length interval into a stream whose
|
|
48
|
+
// contract says there are none.
|
|
49
|
+
if (Temporal.ZonedDateTime.compare(start, end) < 0) {
|
|
50
|
+
yield { start, end };
|
|
51
|
+
}
|
|
52
|
+
date = date.add({ days: 1 });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=time-rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-rules.js","sourceRoot":"","sources":["../src/time-rules.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAgB,MAAM,EAAE,MAAM,cAAc,CAAC;AAGpD;;;;;;GAMG;AACH,MAAM,SAAS,CAAC,CAAC,kBAAkB,CACjC,OAAgB,EAChB,IAAY,EACZ,EAAU,EACV,IAAa;IAEb,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IACrC,MAAM,KAAK,GAAG,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAG,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAE3C,IAAI,QAAQ,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,UAAU,CAClB,6BAA6B,IAAI,OAAO,EAAE,+BAA+B;YACvE,yCAAyC,CAC5C,CAAC;IACJ,CAAC;IAED,4EAA4E;IAC5E,8CAA8C;IAC9C,MAAM,KAAK,GAAG,QAAQ,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAC5D,MAAM,IAAI,GAAG,OAAO,CAAC,EAAE,CAAC;IACxB,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI;SACpB,YAAY,CAAC,MAAM,CAAC;SACpB,WAAW,EAAE;SACb,QAAQ,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;IAEzB,SAAS,CAAC;QACR,MAAM,KAAK,GAAG,IAAI,CAAC,eAAe,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;QAC3E,IACE,IAAI,KAAK,SAAS;YAClB,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,EAChD,CAAC;YACD,OAAO;QACT,CAAC;QAED,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACrD,MAAM,GAAG,GAAG,OAAO,CAAC,eAAe,CAAC;YAClC,QAAQ,EAAE,MAAM;YAChB,SAAS,EAAE,MAAM;SAClB,CAAC,CAAC;QAEH,0EAA0E;QAC1E,2EAA2E;QAC3E,sEAAsE;QACtE,yEAAyE;QACzE,0EAA0E;QAC1E,gCAAgC;QAChC,IAAI,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACnD,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;QACvB,CAAC;QAED,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;IAC/B,CAAC;AACH,CAAC"}
|