@kensio/quando 0.1.0 → 1.0.0

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