@bpmnkit/feel 0.0.21 → 0.1.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/dist/parser.js CHANGED
@@ -1,4 +1,4 @@
1
- import { tokenize } from "./lexer.js";
1
+ import { tokenize, unescapeString } from "./lexer.js";
2
2
  // All known multi-word built-in names
3
3
  const BUILTIN_NAMES = new Set([
4
4
  "string length",
@@ -48,12 +48,43 @@ const BUILTIN_PREFIXES = (() => {
48
48
  }
49
49
  return s;
50
50
  })();
51
+ /** Every strict prefix of the given multi-word names. */
52
+ function prefixesOf(names) {
53
+ const prefixes = new Set();
54
+ for (const name of names) {
55
+ if (!name.includes(" "))
56
+ continue;
57
+ const parts = name.split(" ");
58
+ for (let i = 1; i < parts.length; i++)
59
+ prefixes.add(parts.slice(0, i).join(" "));
60
+ }
61
+ return prefixes;
62
+ }
63
+ // The property names FEEL spells with a space. A path key is matched against
64
+ // this fixed set rather than joining words greedily, so `a.b and c` still
65
+ // reads as a conjunction.
66
+ const MULTIWORD_PROPERTIES = ["time offset", "start included", "end included"];
67
+ // The symbols a FEEL name may contain besides letters, digits and spaces.
68
+ const NAME_SYMBOLS = new Set([".", "/", "-", "'", "+", "*"]);
69
+ // Multi-word type names, longest first: a prefix must not win over the whole name.
70
+ const MULTIWORD_TYPES = ["years and months duration", "days and time duration", "date and time"];
51
71
  class Parser {
52
72
  tokens;
53
73
  pos = 0;
74
+ names;
75
+ namePrefixes;
54
76
  errors = [];
55
- constructor(input) {
77
+ constructor(input, options = {}) {
56
78
  this.tokens = tokenize(input).filter((t) => t.kind !== "whitespace" && t.kind !== "comment");
79
+ const scopeNames = options.names ? [...options.names].filter((n) => n.includes(" ")) : [];
80
+ this.names = new Set(scopeNames);
81
+ this.namePrefixes = prefixesOf(scopeNames);
82
+ }
83
+ isKnownName(name) {
84
+ return BUILTIN_NAMES.has(name) || this.names.has(name);
85
+ }
86
+ isKnownPrefix(name) {
87
+ return BUILTIN_PREFIXES.has(name) || this.namePrefixes.has(name);
57
88
  }
58
89
  peek(offset = 0) {
59
90
  return this.tokens[this.pos + offset];
@@ -85,26 +116,32 @@ class Parser {
85
116
  }
86
117
  return tok;
87
118
  }
88
- /** Try to extend a single name token into a multi-word built-in name. */
119
+ /**
120
+ * Extends a single name token into the longest multi-word name in scope,
121
+ * built-in or supplied by the caller. Words consumed while reaching for a
122
+ * longer name that does not exist are given back, so `date and` falls back
123
+ * to `date` rather than becoming a name nothing can resolve.
124
+ */
89
125
  resolveMultiwordName(first) {
90
126
  let name = first;
91
- while (true) {
92
- // Only try to extend if current is a known prefix
93
- if (!BUILTIN_PREFIXES.has(name))
94
- break;
127
+ let longest = first;
128
+ let longestPos = this.pos;
129
+ while (this.isKnownPrefix(name)) {
95
130
  const next = this.peek();
96
131
  if (!next || (next.kind !== "name" && next.kind !== "keyword"))
97
132
  break;
98
133
  const extended = `${name} ${next.value}`;
99
- if (BUILTIN_NAMES.has(extended) || BUILTIN_PREFIXES.has(extended)) {
100
- this.advance();
101
- name = extended;
102
- }
103
- else {
134
+ if (!this.isKnownName(extended) && !this.isKnownPrefix(extended))
104
135
  break;
136
+ this.advance();
137
+ name = extended;
138
+ if (this.isKnownName(name)) {
139
+ longest = name;
140
+ longestPos = this.pos;
105
141
  }
106
142
  }
107
- return name;
143
+ this.pos = longestPos;
144
+ return longest;
108
145
  }
109
146
  // -------------------------------------------------------------------------
110
147
  // Expression parsing (Pratt)
@@ -160,6 +197,8 @@ class Parser {
160
197
  return 80;
161
198
  if (tok.kind === "punct" && tok.value === "[")
162
199
  return 80;
200
+ if (tok.kind === "punct" && tok.value === "(")
201
+ return 80;
163
202
  return 0;
164
203
  }
165
204
  parseInfix(left, prec) {
@@ -199,9 +238,9 @@ class Parser {
199
238
  tok.value === "**")) {
200
239
  this.advance();
201
240
  const op = tok.value;
202
- // ** is right-associative
203
- const rightPrec = op === "**" ? prec - 1 : prec;
204
- const right = this.parseExpression(rightPrec);
241
+ // FEEL makes every infix operator left-associative, "**" included:
242
+ // 2 ** 3 ** 2 is (2 ** 3) ** 2.
243
+ const right = this.parseExpression(prec);
205
244
  if (!right) {
206
245
  const pos = tok.end;
207
246
  this.errors.push({
@@ -258,8 +297,40 @@ class Parser {
258
297
  this.errors.push({ message: "Expected name after '.'", start: tok.start, end: tok.end });
259
298
  return null;
260
299
  }
300
+ let key = nameTok.value;
301
+ let end = nameTok.end;
302
+ const multiword = MULTIWORD_PROPERTIES.find((p) => this.tryConsumeWords(p));
303
+ if (multiword) {
304
+ key = multiword;
305
+ end = this.tokens[this.pos - 1]?.end ?? nameTok.end;
306
+ }
307
+ else {
308
+ this.advance();
309
+ }
310
+ return { kind: "path", base: left, key, start: left.start, end };
311
+ }
312
+ // Invocation of a function-valued expression: expr(args)
313
+ if (tok.kind === "punct" && tok.value === "(") {
261
314
  this.advance();
262
- return { kind: "path", base: left, key: nameTok.value, start: left.start, end: nameTok.end };
315
+ const args = [];
316
+ if (!this.check("punct", ")")) {
317
+ const arg = this.parseExpression(0);
318
+ if (arg)
319
+ args.push(arg);
320
+ while (this.consume("punct", ",")) {
321
+ const a = this.parseExpression(0);
322
+ if (a)
323
+ args.push(a);
324
+ }
325
+ }
326
+ const close = this.expect("punct", ")");
327
+ return {
328
+ kind: "call-expr",
329
+ target: left,
330
+ args,
331
+ start: left.start,
332
+ end: close?.end ?? args[args.length - 1]?.end ?? left.end,
333
+ };
263
334
  }
264
335
  // Filter: expr[condition]
265
336
  if (tok.kind === "punct" && tok.value === "[") {
@@ -298,7 +369,7 @@ class Parser {
298
369
  // String literal
299
370
  if (tok.kind === "string") {
300
371
  this.advance();
301
- const raw = tok.value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
372
+ const raw = unescapeString(tok.value.slice(1, -1));
302
373
  return { kind: "string", value: raw, start: tok.start, end: tok.end };
303
374
  }
304
375
  // Temporal literal
@@ -406,28 +477,45 @@ class Parser {
406
477
  return { kind: "name", name, start, end: this.tokens[this.pos - 1]?.end ?? start };
407
478
  }
408
479
  isNamedArgList() {
409
- // Peek: name colon value pattern?
410
- const t0 = this.peek(0);
411
- const t1 = this.peek(1);
412
- return !!(t0 &&
413
- (t0.kind === "name" || t0.kind === "keyword") &&
414
- t1 &&
415
- t1.kind === "punct" &&
416
- t1.value === ":");
480
+ // Peek: one or more name words followed by a colon? Parameter names may
481
+ // have spaces, as in `substring(start position: 2, string: "hello")`.
482
+ let offset = 0;
483
+ while (true) {
484
+ const tok = this.peek(offset);
485
+ if (!tok || (tok.kind !== "name" && tok.kind !== "keyword"))
486
+ break;
487
+ offset++;
488
+ }
489
+ if (offset === 0)
490
+ return false;
491
+ const after = this.peek(offset);
492
+ return after?.kind === "punct" && after.value === ":";
417
493
  }
418
494
  parseNamedCall(callee, start) {
419
495
  const args = [];
420
496
  if (!this.check("punct", ")")) {
421
497
  const parsePair = () => {
422
- const nameTok = this.advance();
423
- if (!nameTok)
498
+ const words = [];
499
+ while (this.check("name") || this.check("keyword")) {
500
+ const word = this.advance();
501
+ if (word)
502
+ words.push(word.value);
503
+ }
504
+ if (words.length === 0) {
505
+ const t = this.peek();
506
+ this.errors.push({
507
+ message: "Expected parameter name",
508
+ start: t?.start ?? 0,
509
+ end: t?.end ?? 0,
510
+ });
424
511
  return false;
512
+ }
425
513
  if (!this.expect("punct", ":"))
426
514
  return false;
427
515
  const val = this.parseExpression(0);
428
516
  if (!val)
429
517
  return false;
430
- args.push({ name: nameTok.value, value: val });
518
+ args.push({ name: words.join(" "), value: val });
431
519
  return true;
432
520
  };
433
521
  if (!parsePair())
@@ -440,6 +528,56 @@ class Parser {
440
528
  const close = this.expect("punct", ")");
441
529
  return { kind: "call-named", callee, args, start, end: close?.end ?? start };
442
530
  }
531
+ /**
532
+ * Parses the domain of a `for`/`some`/`every` binding. Unlike other
533
+ * positions, a range may appear here undelimited: `for i in 1..3`.
534
+ */
535
+ parseIterationDomain() {
536
+ const domain = this.parseExpression(0);
537
+ if (!domain)
538
+ return null;
539
+ if (!this.check("op", ".."))
540
+ return domain;
541
+ this.advance();
542
+ const high = this.parseExpression(0);
543
+ if (!high)
544
+ return null;
545
+ return {
546
+ kind: "range",
547
+ startIncluded: true,
548
+ low: domain,
549
+ high,
550
+ endIncluded: true,
551
+ start: domain.start,
552
+ end: high.end,
553
+ };
554
+ }
555
+ /**
556
+ * Reads a context key, which runs up to the colon and so can be gathered
557
+ * without ambiguity. A FEEL name may hold spaces and the symbols listed in
558
+ * NAME_SYMBOLS, which is what lets `{_2021-01-11: ...}` and `{foo+bar: ...}`
559
+ * be keys rather than arithmetic.
560
+ */
561
+ parseContextKey() {
562
+ let key = "";
563
+ let previousWasWord = false;
564
+ while (true) {
565
+ const tok = this.peek();
566
+ if (!tok)
567
+ break;
568
+ const isWord = tok.kind === "name" || tok.kind === "keyword" || tok.kind === "number";
569
+ const isSymbol = tok.kind === "op" && NAME_SYMBOLS.has(tok.value);
570
+ if (!isWord && !isSymbol)
571
+ break;
572
+ // Two words in a row are separated by the space that separated them.
573
+ if (isWord && previousWasWord)
574
+ key += " ";
575
+ key += tok.value;
576
+ previousWasWord = isWord;
577
+ this.advance();
578
+ }
579
+ return key;
580
+ }
443
581
  parseIf() {
444
582
  const start = this.peek()?.start ?? 0;
445
583
  this.advance(); // consume "if"
@@ -469,7 +607,7 @@ class Parser {
469
607
  const varName = nameTok.value;
470
608
  if (!this.expect("keyword", "in"))
471
609
  return false;
472
- const domain = this.parseExpression(0);
610
+ const domain = this.parseIterationDomain();
473
611
  if (!domain)
474
612
  return false;
475
613
  bindings.push({ name: varName, domain });
@@ -499,7 +637,7 @@ class Parser {
499
637
  const varName = nameTok.value;
500
638
  if (!this.expect("keyword", "in"))
501
639
  return false;
502
- const domain = this.parseExpression(0);
640
+ const domain = this.parseIterationDomain();
503
641
  if (!domain)
504
642
  return false;
505
643
  bindings.push({ name: varName, domain });
@@ -524,15 +662,19 @@ class Parser {
524
662
  if (!this.expect("punct", "("))
525
663
  return null;
526
664
  const params = [];
665
+ // A parameter may declare a type, which this package does not check:
666
+ // `function(a: number) a + 1`.
667
+ const parseParam = () => {
668
+ const name = this.advance();
669
+ if (name)
670
+ params.push(name.value);
671
+ if (this.consume("punct", ":"))
672
+ this.parseTypeName();
673
+ };
527
674
  if (!this.check("punct", ")")) {
528
- const p = this.advance();
529
- if (p)
530
- params.push(p.value);
531
- while (this.consume("punct", ",")) {
532
- const q = this.advance();
533
- if (q)
534
- params.push(q.value);
535
- }
675
+ parseParam();
676
+ while (this.consume("punct", ","))
677
+ parseParam();
536
678
  }
537
679
  if (!this.expect("punct", ")"))
538
680
  return null;
@@ -651,13 +793,10 @@ class Parser {
651
793
  const t = this.advance();
652
794
  if (!t)
653
795
  return false;
654
- key = t.value.slice(1, -1);
796
+ key = unescapeString(t.value.slice(1, -1));
655
797
  }
656
798
  else if (this.check("name") || this.check("keyword")) {
657
- const t = this.advance();
658
- if (!t)
659
- return false;
660
- key = t.value;
799
+ key = this.parseContextKey();
661
800
  }
662
801
  else {
663
802
  const t = this.peek();
@@ -691,59 +830,119 @@ class Parser {
691
830
  const close = this.expect("punct", "}");
692
831
  return { kind: "context", entries, start, end: close?.end ?? start };
693
832
  }
833
+ /**
834
+ * Parses the right-hand side of `in`, which FEEL defines as a positive
835
+ * unary test rather than an expression: `x in <= 10`, `x in [1..5]`,
836
+ * `x in (1, < 5, >= 10)`, `x in y`.
837
+ */
694
838
  parseInTestExpr() {
695
- // x in (a, b, c) or x in [1..5] or x in expr
696
- if (this.check("punct", "(")) {
697
- // Parenthesized list of tests or a range
698
- return this.parseParenOrRange();
839
+ if (this.check("punct", "("))
840
+ return this.parseInParen();
841
+ return this.parseOneUnaryTest();
842
+ }
843
+ /**
844
+ * A parenthesized `in` operand is a range, a comma-separated list of unary
845
+ * tests, or a plain grouped expression.
846
+ */
847
+ parseInParen() {
848
+ const open = this.peek();
849
+ if (!open)
850
+ return null;
851
+ const start = open.start;
852
+ this.advance(); // consume (
853
+ const first = this.parseOneUnaryTest();
854
+ if (!first)
855
+ return null;
856
+ if (this.check("op", "..")) {
857
+ this.advance();
858
+ const high = this.parseExpression(0);
859
+ if (!high)
860
+ return null;
861
+ const close = this.advance();
862
+ return {
863
+ kind: "range",
864
+ startIncluded: false,
865
+ low: first,
866
+ high,
867
+ endIncluded: close?.value === "]",
868
+ start,
869
+ end: close?.end ?? high.end,
870
+ };
699
871
  }
700
- if (this.check("punct", "[")) {
701
- return this.parseListOrRange();
872
+ if (this.check("punct", ",")) {
873
+ const tests = [first];
874
+ while (this.consume("punct", ",")) {
875
+ const test = this.parseOneUnaryTest();
876
+ if (test)
877
+ tests.push(test);
878
+ }
879
+ const close = this.expect("punct", ")");
880
+ return {
881
+ kind: "unary-test-list",
882
+ tests,
883
+ start,
884
+ end: close?.end ?? tests[tests.length - 1]?.end ?? start,
885
+ };
702
886
  }
703
- return this.parseExpression(30);
887
+ this.expect("punct", ")");
888
+ return first;
704
889
  }
890
+ /**
891
+ * Parses a type name after `instance of`. Multi-word names are tried
892
+ * longest-first, since "date" is also the start of "date and time". Type
893
+ * arguments (`list<number>`, `function<number> -> string`) are consumed and
894
+ * ignored: this package checks the outer type only.
895
+ */
705
896
  parseTypeName() {
706
897
  const tok = this.peek();
707
898
  if (!tok || (tok.kind !== "name" && tok.kind !== "keyword"))
708
899
  return null;
709
- this.advance();
710
- const name = tok.value;
711
- // Handle multi-word type names: "date and time", "years and months duration", etc.
712
- const multiTypes = {
713
- date: "date",
714
- time: "time",
715
- number: "number",
716
- string: "string",
717
- boolean: "boolean",
718
- context: "context",
719
- list: "list",
720
- function: "function",
721
- duration: "duration",
722
- Any: "Any",
723
- };
724
- if (multiTypes[name])
725
- return name;
726
- // Try to extend: "date and time", "years and months duration"
727
- const next1 = this.peek();
728
- if (next1?.kind === "keyword" && next1.value === "and") {
729
- const saved = this.pos;
730
- this.advance(); // consume "and"
731
- const next2 = this.peek();
732
- if (next2?.kind === "name" && next2.value === "time") {
733
- this.advance();
734
- return "date and time";
900
+ let name = null;
901
+ for (const candidate of MULTIWORD_TYPES) {
902
+ if (this.tryConsumeWords(candidate)) {
903
+ name = candidate;
904
+ break;
735
905
  }
736
- if (next2?.kind === "name" && next2.value === "months") {
906
+ }
907
+ if (name === null) {
908
+ this.advance();
909
+ name = tok.value;
910
+ }
911
+ this.skipTypeArguments();
912
+ return name;
913
+ }
914
+ /** Consumes the tokens spelling `phrase`, or nothing if they do not follow. */
915
+ tryConsumeWords(phrase) {
916
+ const words = phrase.split(" ");
917
+ for (let i = 0; i < words.length; i++) {
918
+ const tok = this.peek(i);
919
+ if (!tok || (tok.kind !== "name" && tok.kind !== "keyword") || tok.value !== words[i]) {
920
+ return false;
921
+ }
922
+ }
923
+ this.pos += words.length;
924
+ return true;
925
+ }
926
+ /** Skips `<...>` type arguments and a `-> type` function result. */
927
+ skipTypeArguments() {
928
+ if (this.check("op", "<")) {
929
+ let depth = 0;
930
+ while (this.peek()) {
931
+ if (this.check("op", "<"))
932
+ depth++;
933
+ else if (this.check("op", ">"))
934
+ depth--;
935
+ else if (this.check("op", ">="))
936
+ depth--; // ">>" lexes as one token pair
737
937
  this.advance();
738
- const next3 = this.peek();
739
- if (next3?.kind === "name" && next3.value === "duration") {
740
- this.advance();
741
- return "years and months duration";
742
- }
938
+ if (depth === 0)
939
+ break;
743
940
  }
744
- this.pos = saved;
745
941
  }
746
- return name;
942
+ if (this.check("op", "->")) {
943
+ this.advance();
944
+ this.parseTypeName();
945
+ }
747
946
  }
748
947
  // -------------------------------------------------------------------------
749
948
  // Unary-test mode
@@ -793,9 +992,14 @@ class Parser {
793
992
  const close = this.expect("punct", ")");
794
993
  return { kind: "unary-not", tests: inner, start, end: close?.end ?? start };
795
994
  }
796
- // Comparison operator prefix: < 5, >= 10, etc.
995
+ // Comparison operator prefix: < 5, >= 10, = 3, != 4
797
996
  if (tok.kind === "op" &&
798
- (tok.value === "<" || tok.value === "<=" || tok.value === ">" || tok.value === ">=")) {
997
+ (tok.value === "<" ||
998
+ tok.value === "<=" ||
999
+ tok.value === ">" ||
1000
+ tok.value === ">=" ||
1001
+ tok.value === "=" ||
1002
+ tok.value === "!=")) {
799
1003
  const start = tok.start;
800
1004
  this.advance();
801
1005
  const expr = this.parseExpression(0);
@@ -861,33 +1065,46 @@ class Parser {
861
1065
  const PARSE_CACHE_LIMIT = 2048;
862
1066
  const expressionCache = new Map();
863
1067
  const unaryTestsCache = new Map();
864
- function remember(cache, input, result) {
1068
+ function remember(cache, key, result) {
865
1069
  if (cache.size >= PARSE_CACHE_LIMIT)
866
1070
  cache.clear();
867
- cache.set(input, result);
1071
+ cache.set(key, result);
868
1072
  return result;
869
1073
  }
870
- export function parseExpression(input) {
871
- const cached = expressionCache.get(input);
1074
+ /**
1075
+ * Cache key for a parse. Scope names change how an expression parses, so they
1076
+ * are part of the key; only names with spaces can, so the rest are left out to
1077
+ * keep the key small.
1078
+ */
1079
+ function cacheKey(input, options) {
1080
+ if (!options?.names)
1081
+ return input;
1082
+ const relevant = [...options.names].filter((n) => n.includes(" ")).sort();
1083
+ return relevant.length === 0 ? input : `${input}\u0000${relevant.join("\u0001")}`;
1084
+ }
1085
+ export function parseExpression(input, options) {
1086
+ const key = cacheKey(input, options);
1087
+ const cached = expressionCache.get(key);
872
1088
  if (cached !== undefined)
873
1089
  return cached;
874
- const p = new Parser(input);
1090
+ const p = new Parser(input, options);
875
1091
  const ast = p.parseExpression(0);
876
1092
  p.checkDone();
877
- return remember(expressionCache, input, { ast, errors: p.errors });
1093
+ return remember(expressionCache, key, { ast, errors: p.errors });
878
1094
  }
879
- export function parseUnaryTests(input) {
880
- const cached = unaryTestsCache.get(input);
1095
+ export function parseUnaryTests(input, options) {
1096
+ const key = cacheKey(input, options);
1097
+ const cached = unaryTestsCache.get(key);
881
1098
  if (cached !== undefined)
882
1099
  return cached;
883
1100
  if (input.trim() === "-") {
884
- return remember(unaryTestsCache, input, {
1101
+ return remember(unaryTestsCache, key, {
885
1102
  ast: { kind: "any-input", start: 0, end: input.length },
886
1103
  errors: [],
887
1104
  });
888
1105
  }
889
- const p = new Parser(input);
1106
+ const p = new Parser(input, options);
890
1107
  const ast = p.parseUnaryTests();
891
- return remember(unaryTestsCache, input, { ast, errors: p.errors });
1108
+ return remember(unaryTestsCache, key, { ast, errors: p.errors });
892
1109
  }
893
1110
  //# sourceMappingURL=parser.js.map
package/dist/types.js CHANGED
@@ -67,6 +67,8 @@ export function getProperty(v, key) {
67
67
  return v.month;
68
68
  if (key === "day")
69
69
  return v.day;
70
+ if (key === "weekday")
71
+ return weekdayOf(v);
70
72
  return null;
71
73
  }
72
74
  if (isFeelTime(v)) {
@@ -77,7 +79,7 @@ export function getProperty(v, key) {
77
79
  if (key === "second")
78
80
  return v.second;
79
81
  if (key === "time offset")
80
- return v.offsetSeconds !== undefined ? v.offsetSeconds / 3600 : null;
82
+ return offsetDuration(v.offsetSeconds);
81
83
  if (key === "timezone")
82
84
  return v.timezone ?? null;
83
85
  return null;
@@ -96,11 +98,13 @@ export function getProperty(v, key) {
96
98
  if (key === "second")
97
99
  return v.time.second;
98
100
  if (key === "time offset")
99
- return v.time.offsetSeconds !== undefined ? v.time.offsetSeconds / 3600 : null;
101
+ return offsetDuration(v.time.offsetSeconds);
100
102
  if (key === "timezone")
101
103
  return v.time.timezone ?? null;
102
104
  if (key === "time")
103
105
  return v.time;
106
+ if (key === "weekday")
107
+ return weekdayOf(v.date);
104
108
  return null;
105
109
  }
106
110
  if (isFeelDayTimeDuration(v)) {
@@ -122,8 +126,37 @@ export function getProperty(v, key) {
122
126
  return v.months % 12;
123
127
  return null;
124
128
  }
129
+ if (isFeelRange(v)) {
130
+ if (key === "start")
131
+ return v.start;
132
+ if (key === "end")
133
+ return v.end;
134
+ if (key === "start included")
135
+ return v.startIncluded;
136
+ if (key === "end included")
137
+ return v.endIncluded;
138
+ return null;
139
+ }
125
140
  // FeelContext
126
141
  const val = v[key];
127
142
  return val !== undefined ? val : null;
128
143
  }
144
+ /** A time offset is a duration, not a number of hours. */
145
+ function offsetDuration(offsetSeconds) {
146
+ if (offsetSeconds === undefined)
147
+ return null;
148
+ return { type: "days-time-duration", seconds: offsetSeconds };
149
+ }
150
+ /** Day of the week as FEEL numbers them: Monday is 1, Sunday is 7. */
151
+ function weekdayOf(d) {
152
+ const y = d.year - 1;
153
+ let days = 365 * y + Math.floor(y / 4) - Math.floor(y / 100) + Math.floor(y / 400);
154
+ const monthLengths = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
155
+ const leap = (d.year % 4 === 0 && d.year % 100 !== 0) || d.year % 400 === 0;
156
+ for (let m = 1; m < d.month; m++)
157
+ days += (m === 2 && leap ? 29 : monthLengths[m]) ?? 30;
158
+ days += d.day;
159
+ // 0001-01-01 was a Monday in the proleptic Gregorian calendar.
160
+ return ((days - 1) % 7) + 1;
161
+ }
129
162
  //# sourceMappingURL=types.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bpmnkit/feel",
3
- "version": "0.0.21",
3
+ "version": "0.1.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -44,6 +44,9 @@
44
44
  "build": "tsc",
45
45
  "typecheck": "tsc --noEmit",
46
46
  "check": "biome check .",
47
- "test": "vitest run"
47
+ "test": "vitest run",
48
+ "tck:extract": "node tasks/extract-tck-tests.mjs",
49
+ "tck": "node tasks/extract-tck-tests.mjs && vitest run tests/tck.test.ts",
50
+ "tck:run": "vitest run tests/tck.test.ts"
48
51
  }
49
52
  }