@qrvey/formula-lang 3.2.0-rc.1109 → 3.2.0-rc.1159
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/QRVEY-DATE-PRESETS.md +630 -69
- package/date-preset-qdp-examples.csv +118 -0
- package/dist/cjs/constants/index.d.ts +29 -1
- package/dist/cjs/constants/index.js +33 -1
- package/dist/cjs/constants/index.js.map +1 -1
- package/dist/cjs/constants/interfaces.d.ts +73 -4
- package/dist/cjs/date-presets/date-preset-classification.d.ts +41 -0
- package/dist/cjs/date-presets/date-preset-classification.js +183 -0
- package/dist/cjs/date-presets/date-preset-classification.js.map +1 -0
- package/dist/cjs/date-presets/date-preset-matches.d.ts +30 -0
- package/dist/cjs/date-presets/date-preset-matches.js +86 -0
- package/dist/cjs/date-presets/date-preset-matches.js.map +1 -0
- package/dist/cjs/date-presets/date-preset-tokens.d.ts +20 -8
- package/dist/cjs/date-presets/date-preset-tokens.js +36 -10
- package/dist/cjs/date-presets/date-preset-tokens.js.map +1 -1
- package/dist/cjs/date-presets/date-presets.d.ts +18 -1
- package/dist/cjs/date-presets/date-presets.js +99 -178
- package/dist/cjs/date-presets/date-presets.js.map +1 -1
- package/dist/cjs/date-presets/index.d.ts +3 -1
- package/dist/cjs/date-presets/index.js +9 -1
- package/dist/cjs/date-presets/index.js.map +1 -1
- package/dist/cjs/errors/dictionary.d.ts +6 -1
- package/dist/cjs/errors/dictionary.js +25 -0
- package/dist/cjs/errors/dictionary.js.map +1 -1
- package/dist/cjs/functions/calendarPeriod.js +6 -10
- package/dist/cjs/functions/calendarPeriod.js.map +1 -1
- package/dist/cjs/functions/dateRange.js +5 -2
- package/dist/cjs/functions/dateRange.js.map +1 -1
- package/dist/cjs/functions/endOf.js +16 -2
- package/dist/cjs/functions/endOf.js.map +1 -1
- package/dist/cjs/functions/field.d.ts +5 -0
- package/dist/cjs/functions/field.js +52 -0
- package/dist/cjs/functions/field.js.map +1 -0
- package/dist/cjs/functions/format.d.ts +2 -0
- package/dist/cjs/functions/format.js +281 -0
- package/dist/cjs/functions/format.js.map +1 -0
- package/dist/cjs/functions/index.d.ts +5 -0
- package/dist/cjs/functions/index.js +36 -1
- package/dist/cjs/functions/index.js.map +1 -1
- package/dist/cjs/functions/offset.d.ts +2 -0
- package/dist/cjs/functions/offset.js +57 -0
- package/dist/cjs/functions/offset.js.map +1 -0
- package/dist/cjs/functions/part.js +19 -3
- package/dist/cjs/functions/part.js.map +1 -1
- package/dist/cjs/functions/partialDate.js +53 -4
- package/dist/cjs/functions/partialDate.js.map +1 -1
- package/dist/cjs/functions/periodAt.js +5 -9
- package/dist/cjs/functions/periodAt.js.map +1 -1
- package/dist/cjs/functions/priorPeriod.d.ts +2 -0
- package/dist/cjs/functions/priorPeriod.js +46 -0
- package/dist/cjs/functions/priorPeriod.js.map +1 -0
- package/dist/cjs/functions/relativePeriod.js +6 -10
- package/dist/cjs/functions/relativePeriod.js.map +1 -1
- package/dist/cjs/functions/samePeriod.d.ts +2 -0
- package/dist/cjs/functions/samePeriod.js +57 -0
- package/dist/cjs/functions/samePeriod.js.map +1 -0
- package/dist/cjs/functions/startOf.js +16 -2
- package/dist/cjs/functions/startOf.js.map +1 -1
- package/dist/cjs/functions/toDate.d.ts +2 -0
- package/dist/cjs/functions/toDate.js +46 -0
- package/dist/cjs/functions/toDate.js.map +1 -0
- package/dist/cjs/functions/today.js +4 -2
- package/dist/cjs/functions/today.js.map +1 -1
- package/dist/cjs/grammar/generated/qformula.lang.js +8 -8
- package/dist/cjs/grammar/generated/qformula.lang.js.map +1 -1
- package/dist/cjs/grammar/generated/qformula.lang.terms.d.ts +1 -0
- package/dist/cjs/grammar/generated/qformula.lang.terms.js +2 -2
- package/dist/cjs/grammar/generated/qformula.lang.terms.js.map +1 -1
- package/dist/cjs/index.d.ts +2 -2
- package/dist/cjs/index.js +7 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/parser/formula-parser.js +18 -1
- package/dist/cjs/parser/formula-parser.js.map +1 -1
- package/dist/cjs/parser/json-parser.js +22 -23
- package/dist/cjs/parser/json-parser.js.map +1 -1
- package/dist/cjs/transpiler/columnTranspilation.js +15 -0
- package/dist/cjs/transpiler/columnTranspilation.js.map +1 -1
- package/dist/cjs/transpiler/index.js +5 -3
- package/dist/cjs/transpiler/index.js.map +1 -1
- package/dist/cjs/transpiler/qdpArithmetic.d.ts +15 -0
- package/dist/cjs/transpiler/qdpArithmetic.js +69 -0
- package/dist/cjs/transpiler/qdpArithmetic.js.map +1 -0
- package/dist/cjs/utils/alignedUnit.d.ts +14 -0
- package/dist/cjs/utils/alignedUnit.js +43 -0
- package/dist/cjs/utils/alignedUnit.js.map +1 -0
- package/dist/cjs/utils/datePresetFormat.d.ts +44 -0
- package/dist/cjs/utils/datePresetFormat.js +222 -0
- package/dist/cjs/utils/datePresetFormat.js.map +1 -0
- package/dist/cjs/utils/datePresetUtils.d.ts +63 -4
- package/dist/cjs/utils/datePresetUtils.js +192 -35
- package/dist/cjs/utils/datePresetUtils.js.map +1 -1
- package/dist/cjs/utils/escapeCharacters.js +2 -2
- package/dist/cjs/utils/escapeCharacters.js.map +1 -1
- package/dist/cjs/utils/extractContextInfo.d.ts +41 -0
- package/dist/cjs/utils/extractContextInfo.js +52 -0
- package/dist/cjs/utils/extractContextInfo.js.map +1 -0
- package/dist/cjs/utils/index.d.ts +1 -1
- package/dist/cjs/utils/index.js +2 -3
- package/dist/cjs/utils/index.js.map +1 -1
- package/dist/cjs/utils/isInteger.js +17 -4
- package/dist/cjs/utils/isInteger.js.map +1 -1
- package/dist/cjs/utils/removeQuotes.d.ts +8 -0
- package/dist/cjs/utils/removeQuotes.js +13 -0
- package/dist/cjs/utils/removeQuotes.js.map +1 -0
- package/dist/module/constants/index.d.ts +29 -1
- package/dist/module/constants/index.js +32 -0
- package/dist/module/constants/index.js.map +1 -1
- package/dist/module/constants/interfaces.d.ts +73 -4
- package/dist/module/date-presets/date-preset-classification.d.ts +41 -0
- package/dist/module/date-presets/date-preset-classification.js +179 -0
- package/dist/module/date-presets/date-preset-classification.js.map +1 -0
- package/dist/module/date-presets/date-preset-matches.d.ts +30 -0
- package/dist/module/date-presets/date-preset-matches.js +81 -0
- package/dist/module/date-presets/date-preset-matches.js.map +1 -0
- package/dist/module/date-presets/date-preset-tokens.d.ts +20 -8
- package/dist/module/date-presets/date-preset-tokens.js +36 -10
- package/dist/module/date-presets/date-preset-tokens.js.map +1 -1
- package/dist/module/date-presets/date-presets.d.ts +18 -1
- package/dist/module/date-presets/date-presets.js +95 -176
- package/dist/module/date-presets/date-presets.js.map +1 -1
- package/dist/module/date-presets/index.d.ts +3 -1
- package/dist/module/date-presets/index.js +3 -1
- package/dist/module/date-presets/index.js.map +1 -1
- package/dist/module/errors/dictionary.d.ts +6 -1
- package/dist/module/errors/dictionary.js +25 -0
- package/dist/module/errors/dictionary.js.map +1 -1
- package/dist/module/functions/calendarPeriod.js +7 -11
- package/dist/module/functions/calendarPeriod.js.map +1 -1
- package/dist/module/functions/dateRange.js +5 -2
- package/dist/module/functions/dateRange.js.map +1 -1
- package/dist/module/functions/endOf.js +17 -3
- package/dist/module/functions/endOf.js.map +1 -1
- package/dist/module/functions/field.d.ts +5 -0
- package/dist/module/functions/field.js +49 -0
- package/dist/module/functions/field.js.map +1 -0
- package/dist/module/functions/format.d.ts +2 -0
- package/dist/module/functions/format.js +278 -0
- package/dist/module/functions/format.js.map +1 -0
- package/dist/module/functions/index.d.ts +5 -0
- package/dist/module/functions/index.js +36 -1
- package/dist/module/functions/index.js.map +1 -1
- package/dist/module/functions/offset.d.ts +2 -0
- package/dist/module/functions/offset.js +54 -0
- package/dist/module/functions/offset.js.map +1 -0
- package/dist/module/functions/part.js +19 -3
- package/dist/module/functions/part.js.map +1 -1
- package/dist/module/functions/partialDate.js +53 -4
- package/dist/module/functions/partialDate.js.map +1 -1
- package/dist/module/functions/periodAt.js +5 -9
- package/dist/module/functions/periodAt.js.map +1 -1
- package/dist/module/functions/priorPeriod.d.ts +2 -0
- package/dist/module/functions/priorPeriod.js +43 -0
- package/dist/module/functions/priorPeriod.js.map +1 -0
- package/dist/module/functions/relativePeriod.js +7 -11
- package/dist/module/functions/relativePeriod.js.map +1 -1
- package/dist/module/functions/samePeriod.d.ts +2 -0
- package/dist/module/functions/samePeriod.js +54 -0
- package/dist/module/functions/samePeriod.js.map +1 -0
- package/dist/module/functions/startOf.js +17 -3
- package/dist/module/functions/startOf.js.map +1 -1
- package/dist/module/functions/toDate.d.ts +2 -0
- package/dist/module/functions/toDate.js +43 -0
- package/dist/module/functions/toDate.js.map +1 -0
- package/dist/module/functions/today.js +4 -2
- package/dist/module/functions/today.js.map +1 -1
- package/dist/module/grammar/generated/qformula.lang.js +8 -8
- package/dist/module/grammar/generated/qformula.lang.js.map +1 -1
- package/dist/module/grammar/generated/qformula.lang.terms.d.ts +1 -0
- package/dist/module/grammar/generated/qformula.lang.terms.js +1 -1
- package/dist/module/grammar/generated/qformula.lang.terms.js.map +1 -1
- package/dist/module/index.d.ts +2 -2
- package/dist/module/index.js +1 -1
- package/dist/module/index.js.map +1 -1
- package/dist/module/parser/formula-parser.js +18 -1
- package/dist/module/parser/formula-parser.js.map +1 -1
- package/dist/module/parser/json-parser.js +22 -23
- package/dist/module/parser/json-parser.js.map +1 -1
- package/dist/module/transpiler/columnTranspilation.js +15 -0
- package/dist/module/transpiler/columnTranspilation.js.map +1 -1
- package/dist/module/transpiler/index.js +5 -3
- package/dist/module/transpiler/index.js.map +1 -1
- package/dist/module/transpiler/qdpArithmetic.d.ts +15 -0
- package/dist/module/transpiler/qdpArithmetic.js +65 -0
- package/dist/module/transpiler/qdpArithmetic.js.map +1 -0
- package/dist/module/utils/alignedUnit.d.ts +14 -0
- package/dist/module/utils/alignedUnit.js +39 -0
- package/dist/module/utils/alignedUnit.js.map +1 -0
- package/dist/module/utils/datePresetFormat.d.ts +44 -0
- package/dist/module/utils/datePresetFormat.js +214 -0
- package/dist/module/utils/datePresetFormat.js.map +1 -0
- package/dist/module/utils/datePresetUtils.d.ts +63 -4
- package/dist/module/utils/datePresetUtils.js +188 -35
- package/dist/module/utils/datePresetUtils.js.map +1 -1
- package/dist/module/utils/escapeCharacters.js +2 -2
- package/dist/module/utils/escapeCharacters.js.map +1 -1
- package/dist/module/utils/extractContextInfo.d.ts +41 -0
- package/dist/module/utils/extractContextInfo.js +46 -0
- package/dist/module/utils/extractContextInfo.js.map +1 -0
- package/dist/module/utils/index.d.ts +1 -1
- package/dist/module/utils/index.js +1 -1
- package/dist/module/utils/index.js.map +1 -1
- package/dist/module/utils/isInteger.js +17 -4
- package/dist/module/utils/isInteger.js.map +1 -1
- package/dist/module/utils/removeQuotes.d.ts +8 -0
- package/dist/module/utils/removeQuotes.js +9 -0
- package/dist/module/utils/removeQuotes.js.map +1 -0
- package/docs/QRV-1069-analysis.md +458 -0
- package/docs/QRV-1069-jira-comment.md +35 -0
- package/docs/TECH-DEBT.md +351 -0
- package/package.json +5 -2
- package/scripts/date-preset-examples.data.js +269 -0
- package/scripts/generate-date-preset-examples.js +219 -0
- package/specs/QRV-1069--date-expression-framework/QRV-1069--additive-functions/input.md +51 -0
- package/specs/QRV-1069--date-expression-framework/QRV-1069--classification/input.md +51 -0
- package/specs/QRV-1069--date-expression-framework/QRV-1069--column-bound/input.md +89 -0
- package/specs/QRV-1069--date-expression-framework/QRV-1069--format/input.md +83 -0
- package/specs/QRV-1069--date-expression-framework/QRV-1069--scalar-results/input.md +55 -0
- package/specs/QRV-1069--date-expression-framework/README.md +53 -0
- package/specs/QRV-1069--date-expression-framework/constitution.md +118 -0
package/QRVEY-DATE-PRESETS.md
CHANGED
|
@@ -35,7 +35,11 @@ Formats dates, ranges, and partial dates for UI.
|
|
|
35
35
|
## Output Types
|
|
36
36
|
|
|
37
37
|
```ts
|
|
38
|
-
type DatePresetValue =
|
|
38
|
+
type DatePresetValue =
|
|
39
|
+
| string
|
|
40
|
+
| DateRangeValue
|
|
41
|
+
| PartialDateValue
|
|
42
|
+
| ScalarValue;
|
|
39
43
|
```
|
|
40
44
|
|
|
41
45
|
By default, `date` is represented as a UTC ISO string:
|
|
@@ -74,16 +78,51 @@ Supported fields for `partialDate`:
|
|
|
74
78
|
|
|
75
79
|
## Value Types
|
|
76
80
|
|
|
77
|
-
`TranspileDatePreset`
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
|
83
|
-
|
|
|
84
|
-
| `
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
`TranspileDatePreset` classifies every expression on two independent axes:
|
|
82
|
+
`valueType` answers *how the value was built*, `temporality` answers *whether it
|
|
83
|
+
moves with the clock*. They are independent — `PERIOD_AT("MONTH", 4, 2026)` is
|
|
84
|
+
`aligned` and `static`, while `PERIOD_AT("MONTH", 4)` is `aligned` and `dynamic`.
|
|
85
|
+
|
|
86
|
+
| Value type | Meaning | Examples |
|
|
87
|
+
| ----------- | ---------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
88
|
+
| `explicit` | A named instant or a range between two of them | `DATE("2026-07-20")`, `TODAY()`, `DATE_RANGE(...)` |
|
|
89
|
+
| `aligned` | Snapped to a calendar boundary, or positioned in a period | `CALENDAR_PERIOD("MONTH", 0)`, `PERIOD_AT("MONTH", 4, 2026)` |
|
|
90
|
+
| `rolling` | Moving window or shift measured from an anchor | `RELATIVE_PERIOD(-29, "DAY")`, `OFFSET(TODAY(), -7, "DAY")` |
|
|
91
|
+
| `recurring` | Partial pattern that repeats | `PARTIAL_DATE("ANY", 4)`, `PARTIAL_DATE("ANY", 4, "ANY", "ANY", 15)` |
|
|
92
|
+
| `scalar` | A number extracted from a date | `PART(TODAY(), "MONTH")`, `PART(TODAY(), "YEAR") - 1` |
|
|
93
|
+
| `dataBound` | Resolved from a column value | reserved; see QRV-1069 column-bound functions |
|
|
94
|
+
|
|
95
|
+
| Temporality | Meaning | Examples |
|
|
96
|
+
| ----------- | ---------------------------------------------------------------- | ---------------------------------------------- |
|
|
97
|
+
| `static` | Resolves to the same value on every run | `DATE("2026-07-20")`, `PERIOD_AT("MONTH", 4, 2026)` |
|
|
98
|
+
| `dynamic` | Depends on the current instant, directly or through a default | `TODAY()`, `CALENDAR_PERIOD("MONTH", 0)` |
|
|
99
|
+
| `timeless` | Has no instant to move — a pattern rather than a point | `PARTIAL_DATE("ANY", 4)` |
|
|
100
|
+
|
|
101
|
+
`temporality` is optional in the response type; an absent value means `timeless`.
|
|
102
|
+
|
|
103
|
+
Both axes are structural: they are derived from the expression tree alone. The
|
|
104
|
+
resolution context — timezone, calendar, `weekStartsOn`, `fiscalYearStartMonth`,
|
|
105
|
+
`applyTimezoneToExpression` — changes resolved values, never classification.
|
|
106
|
+
|
|
107
|
+
`START_OF` and `END_OF` are boundary selectors: they take the `valueType` of
|
|
108
|
+
their operand. `START_OF(CALENDAR_PERIOD("MONTH", 0))` is `aligned`,
|
|
109
|
+
`START_OF(RELATIVE_PERIOD(-6, "DAY"))` is `rolling`, and
|
|
110
|
+
`START_OF(DATE("2026-07-08"))` is `explicit`.
|
|
111
|
+
|
|
112
|
+
Their optional `UNIT` does not change that. Snapping to a boundary is not what
|
|
113
|
+
makes a value `aligned` — `START_OF(DATE("2026-07-08"))` already snaps to a day
|
|
114
|
+
boundary and stays `explicit`. The unit chooses *which* boundary; it does not
|
|
115
|
+
change how the value was built. So `START_OF(TODAY(), "MONTH")` is `explicit`
|
|
116
|
+
even though it resolves to the same instant as the `aligned`
|
|
117
|
+
`START_OF(CALENDAR_PERIOD("MONTH", 0))`.
|
|
118
|
+
|
|
119
|
+
A now-bearing argument that is omitted defaults to now, which makes the call
|
|
120
|
+
`dynamic`: the year of `PERIOD_AT` and the anchor of `RELATIVE_PERIOD` both
|
|
121
|
+
behave that way.
|
|
122
|
+
|
|
123
|
+
The table that drives all of this is exported as
|
|
124
|
+
`DATE_PRESET_CLASSIFICATION_SLOTS`, one row per function, in
|
|
125
|
+
`src/date-presets/date-preset-classification.ts`.
|
|
87
126
|
|
|
88
127
|
## Resolution Context
|
|
89
128
|
|
|
@@ -99,6 +138,7 @@ QDP functions can receive `FormulaContext` with date preset configuration:
|
|
|
99
138
|
fiscalYearStartDay: 1,
|
|
100
139
|
weekStartsOn: 0,
|
|
101
140
|
applyTimezoneToExpression: "default",
|
|
141
|
+
columnValues: { shipping_date: "2026-03-14T09:20:00.000Z" },
|
|
102
142
|
},
|
|
103
143
|
}
|
|
104
144
|
```
|
|
@@ -115,6 +155,7 @@ Current defaults:
|
|
|
115
155
|
| `fiscalYearStartDay` | `1` | Clamped to 1-31 and adjusted to the last valid day of the month |
|
|
116
156
|
| `weekStartsOn` | `0` | Sunday. Accepts 0-6 or English weekday names |
|
|
117
157
|
| `applyTimezoneToExpression` | `default` | Controls the timezone format used in the returned public `expression` value |
|
|
158
|
+
| `columnValues` | `{}` | The value of each column for the row being resolved, keyed by column id. Read by `FIELD` |
|
|
118
159
|
|
|
119
160
|
Use `timezone.timeZone` for IANA identifiers. Use `timezone.offset` for admin/default/browser/custom offset values such as `{ offset: "default" }`, `{ offset: "browser" }`, or `{ offset: "-05:00" }`.
|
|
120
161
|
|
|
@@ -176,7 +217,7 @@ NOW();
|
|
|
176
217
|
|
|
177
218
|
Output: `date`.
|
|
178
219
|
|
|
179
|
-
Value type when used as the final result: `
|
|
220
|
+
Value type when used as the final result: `explicit`, `dynamic`.
|
|
180
221
|
|
|
181
222
|
### `TODAY()`
|
|
182
223
|
|
|
@@ -189,7 +230,7 @@ TODAY();
|
|
|
189
230
|
|
|
190
231
|
Output: `date`.
|
|
191
232
|
|
|
192
|
-
Value type when used as the final result: `
|
|
233
|
+
Value type when used as the final result: `explicit`, `dynamic`.
|
|
193
234
|
|
|
194
235
|
### `DATE(value)`
|
|
195
236
|
|
|
@@ -223,7 +264,7 @@ Rules:
|
|
|
223
264
|
|
|
224
265
|
Output: `date`.
|
|
225
266
|
|
|
226
|
-
Value type: `
|
|
267
|
+
Value type: `explicit`, `static`.
|
|
227
268
|
|
|
228
269
|
### `DATE_RANGE(start, end)`
|
|
229
270
|
|
|
@@ -248,7 +289,7 @@ Rules:
|
|
|
248
289
|
|
|
249
290
|
Output: `dateRange`.
|
|
250
291
|
|
|
251
|
-
Value type: `
|
|
292
|
+
Value type: `explicit`. Temporality is derived from `START` and `END`.
|
|
252
293
|
|
|
253
294
|
### `RELATIVE_PERIOD(offset, unit, anchor?)`
|
|
254
295
|
|
|
@@ -282,7 +323,61 @@ RELATIVE_PERIOD(-2, "WEEK", DATE("2026-07-08T15:30"));
|
|
|
282
323
|
|
|
283
324
|
Output: `dateRange`.
|
|
284
325
|
|
|
285
|
-
Value type: `rolling`.
|
|
326
|
+
Value type: `rolling`. Temporality is derived from the anchor, and is `dynamic`
|
|
327
|
+
when the anchor is omitted.
|
|
328
|
+
|
|
329
|
+
### `OFFSET(point, offset, unit, mode?)`
|
|
330
|
+
|
|
331
|
+
Shifts a single instant, keeping its time of day.
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
// Reference date 2026-07-22.
|
|
335
|
+
OFFSET(TODAY(), -7, "DAY");
|
|
336
|
+
// 2026-07-15T00:00:00.000Z
|
|
337
|
+
|
|
338
|
+
OFFSET(DATE("2026-07-15T14:35:27"), 1, "MONTH");
|
|
339
|
+
// 2026-08-15T14:35:27.000Z
|
|
340
|
+
|
|
341
|
+
OFFSET(DATE("2025-01-31"), 1, "MONTH");
|
|
342
|
+
// 2025-02-28T00:00:00.000Z -- clamped, February has no 31st
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Parameters:
|
|
346
|
+
|
|
347
|
+
| Parameter | Type | Required | Notes |
|
|
348
|
+
| --------- | ---------------- | -------- | ----------------------------------------- |
|
|
349
|
+
| `POINT` | `date` | Yes | A single instant, not a range |
|
|
350
|
+
| `OFFSET` | Integer `number` | Yes | Negative, zero, or positive |
|
|
351
|
+
| `UNIT` | `string` | Yes | `DAY`, `WEEK`, `MONTH`, `QUARTER`, `YEAR` |
|
|
352
|
+
| `OVERFLOW_MODE` | `string` | No | `CLAMP` (default) or `OVERFLOW` |
|
|
353
|
+
|
|
354
|
+
Rules:
|
|
355
|
+
|
|
356
|
+
- `POINT` must be a `date`. A `dateRange` is rejected rather than silently
|
|
357
|
+
reduced to one of its edges — `START_OF` and `END_OF` exist so the author
|
|
358
|
+
says which edge is meant.
|
|
359
|
+
- The shift is on the local wall clock of the resolution timezone, so a day
|
|
360
|
+
is a calendar day rather than 24 hours.
|
|
361
|
+
- **`OVERFLOW_MODE` decides what happens when a shift by whole months lands on a day
|
|
362
|
+
the target month does not have.** `CLAMP`, the default, pins it to the last
|
|
363
|
+
day that exists: `OFFSET(DATE("2025-01-31"), 1, "MONTH")` is `2025-02-28`.
|
|
364
|
+
`OVERFLOW` lets it spill: the same call with `"OVERFLOW"` is `2025-03-03`.
|
|
365
|
+
`OVERFLOW_MODE` has no effect on `DAY` or `WEEK`, which cannot overflow a month.
|
|
366
|
+
- `OVERFLOW` is the spelling that reproduces `RELATIVE_PERIOD`, which has no
|
|
367
|
+
mode of its own and always spills. That divergence is deliberate: the
|
|
368
|
+
default should be the answer a reader expects, and the other one stays
|
|
369
|
+
reachable by name.
|
|
370
|
+
- Clamping is not reversible. `OFFSET(OFFSET(DATE("2025-01-31"), 1, "MONTH"), -1, "MONTH")`
|
|
371
|
+
is `2025-01-28`, not `2025-01-31`; once a day is clamped away it is gone.
|
|
372
|
+
- Under a timezone the instant is rebuilt from its wall-clock parts, which
|
|
373
|
+
does not carry milliseconds. In UTC they survive.
|
|
374
|
+
|
|
375
|
+
Output: `date`.
|
|
376
|
+
|
|
377
|
+
Value type: `rolling`, always. A shift from an anchor is rolling whether or not
|
|
378
|
+
the anchor itself moves, so it is never delegated to `POINT`. Temporality is
|
|
379
|
+
derived from `POINT`: `OFFSET(TODAY(), -7, "DAY")` is `dynamic`,
|
|
380
|
+
`OFFSET(DATE("2026-01-01"), -7, "DAY")` is `static`.
|
|
286
381
|
|
|
287
382
|
### `CALENDAR_PERIOD(period, offset?)`
|
|
288
383
|
|
|
@@ -319,7 +414,47 @@ CALENDAR_PERIOD("QUARTER", -1);
|
|
|
319
414
|
|
|
320
415
|
Output: `dateRange`.
|
|
321
416
|
|
|
322
|
-
Value type: `
|
|
417
|
+
Value type: `aligned`, `dynamic`.
|
|
418
|
+
|
|
419
|
+
### `TO_DATE(unit)`
|
|
420
|
+
|
|
421
|
+
The elapsed part of the current period: from the start of the `UNIT` containing
|
|
422
|
+
today, to the end of today.
|
|
423
|
+
|
|
424
|
+
```ts
|
|
425
|
+
// Reference date 2026-07-22.
|
|
426
|
+
TO_DATE("MONTH");
|
|
427
|
+
// 2026-07-01T00:00:00.000Z -> 2026-07-22T23:59:59.999Z
|
|
428
|
+
|
|
429
|
+
TO_DATE("YEAR");
|
|
430
|
+
// 2026-01-01T00:00:00.000Z -> 2026-07-22T23:59:59.999Z
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Parameter:
|
|
434
|
+
|
|
435
|
+
| Parameter | Type | Required | Notes |
|
|
436
|
+
| ------------------ | --------- | -------- | ---------------------------------- |
|
|
437
|
+
| `UNIT` | `string` | Yes | `WEEK`, `MONTH`, `QUARTER`, `YEAR` |
|
|
438
|
+
| `ANCHOR_INCLUSIVE` | `boolean` | No | `true` by default |
|
|
439
|
+
|
|
440
|
+
Rules:
|
|
441
|
+
|
|
442
|
+
- The range **covers today in full** by default. Ending it at `TODAY()` would
|
|
443
|
+
admit a single instant, `00:00:00.000`, and drop every row stamped later.
|
|
444
|
+
- `ANCHOR_INCLUSIVE: false` stops the range at the end of yesterday, so today
|
|
445
|
+
is left out entirely: `TO_DATE("MONTH", false)` at reference date
|
|
446
|
+
2026-07-22 is `2026-07-01T00:00:00.000Z -> 2026-07-21T23:59:59.999Z`.
|
|
447
|
+
- The start is the one `CALENDAR_PERIOD` produces for the same unit, so the
|
|
448
|
+
calendar, timezone, `weekStartsOn` and `fiscalYearStartMonth` of the
|
|
449
|
+
resolution context all apply. Under `corporate-fiscal` with
|
|
450
|
+
`fiscalYearStartMonth: 4`, `TO_DATE("YEAR")` starts on 1 April.
|
|
451
|
+
- `DAY` is not accepted. `TO_DATE("DAY")` would be the whole of today, which
|
|
452
|
+
`CALENDAR_PERIOD("DAY", 0)` already spells.
|
|
453
|
+
|
|
454
|
+
Output: `dateRange`.
|
|
455
|
+
|
|
456
|
+
Value type: `aligned`, always `dynamic`. Both endpoints are read from the clock,
|
|
457
|
+
so there is no argument for the temporality to be derived from.
|
|
323
458
|
|
|
324
459
|
### `PERIOD_AT(period, position, year?)`
|
|
325
460
|
|
|
@@ -360,7 +495,8 @@ Week rules:
|
|
|
360
495
|
|
|
361
496
|
Output: `dateRange`.
|
|
362
497
|
|
|
363
|
-
|
|
498
|
+
Value type: `aligned`. Temporality is derived from `YEAR`, and is `dynamic` when
|
|
499
|
+
`YEAR` is omitted, because it then defaults to the current year.
|
|
364
500
|
|
|
365
501
|
### `PARTIAL_DATE(year?, month?, quarter?, week?, day?)`
|
|
366
502
|
|
|
@@ -409,15 +545,27 @@ PARTIAL_DATE("ANY", "ANY", "ANY", "ANY", 15);
|
|
|
409
545
|
// Short format: Day 15
|
|
410
546
|
```
|
|
411
547
|
|
|
412
|
-
|
|
548
|
+
A pattern that no date could satisfy is rejected where it is written, rather
|
|
549
|
+
than resolving and then matching nothing:
|
|
550
|
+
|
|
551
|
+
- The month must fall inside the quarter. `PARTIAL_DATE("ANY", 5, 3)` — May in
|
|
552
|
+
Q3 — is an error.
|
|
553
|
+
- The day must exist in the month. `PARTIAL_DATE("ANY", 2, "ANY", "ANY", 30)`
|
|
554
|
+
is an error, and so is `PARTIAL_DATE(2026, 2, "ANY", "ANY", 29)`, since 2026
|
|
555
|
+
is not a leap year. Without a year, 29 February is accepted: the pattern
|
|
556
|
+
matches any year, and leap years exist.
|
|
557
|
+
|
|
558
|
+
A quarter and a day never contradict, because every quarter contains a 31-day
|
|
559
|
+
month, and a week can fall in any month. Neither pair is checked.
|
|
413
560
|
|
|
414
|
-
|
|
415
|
-
- `relative` if it includes `week`.
|
|
416
|
-
- `recurring` for all other partial dates.
|
|
561
|
+
Value type, always with temporality `timeless`:
|
|
417
562
|
|
|
418
|
-
|
|
563
|
+
- `explicit` if it includes `year`, `month`, and `day`, without `week`.
|
|
564
|
+
- `recurring` for every other partial date, `week` included.
|
|
419
565
|
|
|
420
|
-
|
|
566
|
+
### `START_OF(value, unit?)`
|
|
567
|
+
|
|
568
|
+
Returns the start of `VALUE`, or the start of the `UNIT` period containing it.
|
|
421
569
|
|
|
422
570
|
```ts
|
|
423
571
|
START_OF(DATE("2026-07-08T15:30"));
|
|
@@ -425,21 +573,45 @@ START_OF(DATE("2026-07-08T15:30"));
|
|
|
425
573
|
|
|
426
574
|
START_OF(CALENDAR_PERIOD("MONTH", -1));
|
|
427
575
|
// 2026-06-01T00:00:00.000Z
|
|
576
|
+
|
|
577
|
+
START_OF(DATE("2026-07-15T14:35:27"), "QUARTER");
|
|
578
|
+
// 2026-07-01T00:00:00.000Z
|
|
428
579
|
```
|
|
429
580
|
|
|
430
|
-
|
|
581
|
+
Parameters:
|
|
582
|
+
|
|
583
|
+
| Parameter | Type | Required |
|
|
584
|
+
| --------- | --------------------- | -------- |
|
|
585
|
+
| `VALUE` | `date` or `dateRange` | Yes |
|
|
586
|
+
| `UNIT` | `string` | No |
|
|
431
587
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
588
|
+
`UNIT` accepts `DAY`, `WEEK`, `MONTH`, `QUARTER` and `YEAR`.
|
|
589
|
+
|
|
590
|
+
Rules:
|
|
591
|
+
|
|
592
|
+
- Without `UNIT`, a `dateRange` yields its own `start` verbatim and a `date`
|
|
593
|
+
yields the start of its day.
|
|
594
|
+
- **Omitting `UNIT` is not the same as passing `"DAY"`.** On a `dateRange` the
|
|
595
|
+
bare call preserves the endpoint, while `"DAY"` imposes a day boundary:
|
|
596
|
+
`START_OF(DATE_RANGE(DATE("2026-07-08T15:30"), DATE("2026-07-25T09:00")))` is
|
|
597
|
+
`2026-07-08T15:30:00.000Z`, and the same call with `"DAY"` is
|
|
598
|
+
`2026-07-08T00:00:00.000Z`.
|
|
599
|
+
- With `UNIT`, a `dateRange` contributes its `start` and the boundary is
|
|
600
|
+
computed from there.
|
|
601
|
+
- The boundary is the one `CALENDAR_PERIOD` would produce for the same unit,
|
|
602
|
+
so it follows the timezone, calendar, `weekStartsOn` and
|
|
603
|
+
`fiscalYearStartMonth` of the resolution context. Under a timezone the snap
|
|
604
|
+
is to the local boundary: `START_OF(DATE("2026-07-01T02:00"), "MONTH")` at
|
|
605
|
+
`-05:00` lands in **June**, because `02:00Z` is 21:00 on 30 June locally.
|
|
435
606
|
|
|
436
607
|
Output: `date`.
|
|
437
608
|
|
|
438
|
-
Value type
|
|
609
|
+
Value type: delegated to `VALUE`, with or without `UNIT`. Temporality is derived
|
|
610
|
+
from `VALUE`.
|
|
439
611
|
|
|
440
|
-
### `END_OF(value)`
|
|
612
|
+
### `END_OF(value, unit?)`
|
|
441
613
|
|
|
442
|
-
Returns the end of
|
|
614
|
+
Returns the end of `VALUE`, or the end of the `UNIT` period containing it.
|
|
443
615
|
|
|
444
616
|
```ts
|
|
445
617
|
END_OF(DATE("2026-07-08T15:30"));
|
|
@@ -447,17 +619,159 @@ END_OF(DATE("2026-07-08T15:30"));
|
|
|
447
619
|
|
|
448
620
|
END_OF(CALENDAR_PERIOD("YEAR", -1));
|
|
449
621
|
// 2025-12-31T23:59:59.999Z
|
|
622
|
+
|
|
623
|
+
END_OF(DATE("2026-07-15T14:35:27"), "QUARTER");
|
|
624
|
+
// 2026-09-30T23:59:59.999Z
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
Parameters:
|
|
628
|
+
|
|
629
|
+
| Parameter | Type | Required |
|
|
630
|
+
| --------- | --------------------- | -------- |
|
|
631
|
+
| `VALUE` | `date` or `dateRange` | Yes |
|
|
632
|
+
| `UNIT` | `string` | No |
|
|
633
|
+
|
|
634
|
+
`UNIT` accepts `DAY`, `WEEK`, `MONTH`, `QUARTER` and `YEAR`.
|
|
635
|
+
|
|
636
|
+
The rules mirror `START_OF`, measured from the other edge: without `UNIT` a
|
|
637
|
+
`dateRange` yields its own `end` verbatim, and with `UNIT` the boundary is
|
|
638
|
+
computed from that `end`.
|
|
639
|
+
|
|
640
|
+
Output: `date`.
|
|
641
|
+
|
|
642
|
+
Value type: delegated to `VALUE`, with or without `UNIT`. Temporality is derived
|
|
643
|
+
from `VALUE`.
|
|
644
|
+
|
|
645
|
+
### `SAME_PERIOD(reference, offset, unit, mode?)`
|
|
646
|
+
|
|
647
|
+
Shifts a range, keeping its calendar shape.
|
|
648
|
+
|
|
649
|
+
```ts
|
|
650
|
+
SAME_PERIOD(PERIOD_AT("QUARTER", 1, 2026), -1, "YEAR");
|
|
651
|
+
// Q1 2025: 2025-01-01T00:00:00.000Z -> 2025-03-31T23:59:59.999Z
|
|
652
|
+
|
|
653
|
+
SAME_PERIOD(PERIOD_AT("MONTH", 2, 2026), 1, "MONTH");
|
|
654
|
+
// all of March, not the first 28 days of it
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
Parameters:
|
|
658
|
+
|
|
659
|
+
| Parameter | Type | Required | Notes |
|
|
660
|
+
| ----------- | ---------------- | -------- | ----------------------------------------- |
|
|
661
|
+
| `REFERENCE` | `dateRange` | Yes | A range, not a single date |
|
|
662
|
+
| `OFFSET` | Integer `number` | Yes | Negative, zero, or positive |
|
|
663
|
+
| `UNIT` | `string` | Yes | `DAY`, `WEEK`, `MONTH`, `QUARTER`, `YEAR` |
|
|
664
|
+
| `OVERFLOW_MODE` | `string` | No | `CLAMP` (default) or `OVERFLOW` |
|
|
665
|
+
|
|
666
|
+
Rules:
|
|
667
|
+
|
|
668
|
+
- The end is shifted as an **exclusive** boundary, one millisecond past the
|
|
669
|
+
inclusive end. That is what makes a whole period stay a whole period:
|
|
670
|
+
February plus one month is all of March, not its first 28 days.
|
|
671
|
+
- `UNIT` is the unit of the shift and is independent of the shape of
|
|
672
|
+
`REFERENCE`. A quarter can be shifted by a year.
|
|
673
|
+
- `OVERFLOW_MODE` behaves as it does on `OFFSET`.
|
|
674
|
+
- **Clamping can pull both shifted edges onto the same day.** 29 and 30
|
|
675
|
+
January both clamp onto 28 February. When the shifted end lands at or
|
|
676
|
+
before the shifted start, the result is the whole of the shifted start's
|
|
677
|
+
day, rather than a range whose end precedes its start or a range of zero
|
|
678
|
+
width. Under `OVERFLOW` the edges stay apart and no collapse happens.
|
|
679
|
+
- Clamping loses span, and the collapse only recovers the degenerate case. A
|
|
680
|
+
26-hour reference whose edges both clamp into the same month comes back
|
|
681
|
+
shorter: `SAME_PERIOD(DATE_RANGE(DATE("2026-01-29T08:00"), DATE("2026-01-30T10:00")), 1, "MONTH")`
|
|
682
|
+
is two hours on 28 February. Use `OVERFLOW` when the span matters more than
|
|
683
|
+
the day-of-month.
|
|
684
|
+
|
|
685
|
+
Output: `dateRange`.
|
|
686
|
+
|
|
687
|
+
Value type: delegated to `REFERENCE`. Temporality is derived from `REFERENCE`.
|
|
688
|
+
|
|
689
|
+
### `PRIOR_PERIOD(reference, unit?)`
|
|
690
|
+
|
|
691
|
+
The period immediately before `REFERENCE`, in one of two readings.
|
|
692
|
+
|
|
693
|
+
```ts
|
|
694
|
+
// Reference date 2026-07-22.
|
|
695
|
+
PRIOR_PERIOD(RELATIVE_PERIOD(-44, "DAY"));
|
|
696
|
+
// the 45 days before the reference 45:
|
|
697
|
+
// 2026-04-24T00:00:00.000Z -> 2026-06-07T23:59:59.999Z
|
|
698
|
+
|
|
699
|
+
PRIOR_PERIOD(CALENDAR_PERIOD("MONTH", 0), "MONTH");
|
|
700
|
+
// the whole of June: 2026-06-01T00:00:00.000Z -> 2026-06-30T23:59:59.999Z
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
Parameters:
|
|
704
|
+
|
|
705
|
+
| Parameter | Type | Required | Notes |
|
|
706
|
+
| --------------- | ----------- | -------- | ------------------------------- |
|
|
707
|
+
| `REFERENCE` | `dateRange` | Yes | A range, not a single date |
|
|
708
|
+
| `OVERFLOW_MODE` | `string` | No | `CLAMP` (default) or `OVERFLOW` |
|
|
709
|
+
|
|
710
|
+
Rules:
|
|
711
|
+
|
|
712
|
+
- **It shifts by the calendar unit the reference was built from**, read off
|
|
713
|
+
the expression rather than the resolved value:
|
|
714
|
+
`PRIOR_PERIOD(PERIOD_AT("MONTH", 2, 2026))` is January 2026 — the whole
|
|
715
|
+
month — not the 28 days before February.
|
|
716
|
+
- The unit is taken from `CALENDAR_PERIOD`'s `PERIOD` and `PERIOD_AT`'s
|
|
717
|
+
`PERIOD`. It cannot be taken from the value:
|
|
718
|
+
`DATE_RANGE(DATE("2026-07-01"), END_OF(DATE("2026-07-31")))` and
|
|
719
|
+
`CALENDAR_PERIOD("MONTH", 0)` are byte-identical in July 2026, and the two
|
|
720
|
+
deliberately answer differently.
|
|
721
|
+
- **A reference with no calendar unit behind it** — a `RELATIVE_PERIOD`, a
|
|
722
|
+
literal `DATE_RANGE`, anything composed — gets the immediately preceding
|
|
723
|
+
window of the same duration, abutting to the millisecond. That is the
|
|
724
|
+
epic's "mechanical comparison, independent of alignment":
|
|
725
|
+
`PRIOR_PERIOD(RELATIVE_PERIOD(-44, "DAY"))` is the 45 days before the
|
|
726
|
+
reference 45.
|
|
727
|
+
- The calendar reading follows the calendar, timezone, `weekStartsOn` and
|
|
728
|
+
`fiscalYearStartMonth` of the resolution context.
|
|
729
|
+
- `OVERFLOW_MODE` is accepted for symmetry with `OFFSET` and `SAME_PERIOD`
|
|
730
|
+
but currently reaches no arithmetic that can overflow — see
|
|
731
|
+
`docs/TECH-DEBT.md`.
|
|
732
|
+
|
|
733
|
+
Output: `dateRange`.
|
|
734
|
+
|
|
735
|
+
Value type: delegated to `REFERENCE`. Temporality is derived from `REFERENCE`.
|
|
736
|
+
|
|
737
|
+
### `FIELD(column)`
|
|
738
|
+
|
|
739
|
+
Resolves a dataset column to the date it holds for the row being resolved.
|
|
740
|
+
|
|
741
|
+
```ts
|
|
742
|
+
FIELD([shipping_date]);
|
|
743
|
+
// with columnValues: { shipping_date: "2026-03-14T09:20:00.000Z" }
|
|
744
|
+
// 2026-03-14T09:20:00.000Z
|
|
745
|
+
|
|
746
|
+
START_OF(FIELD([shipping_date]), "MONTH");
|
|
747
|
+
// 2026-03-01T00:00:00.000Z
|
|
450
748
|
```
|
|
451
749
|
|
|
452
750
|
Parameter:
|
|
453
751
|
|
|
454
|
-
| Parameter | Type
|
|
455
|
-
| --------- |
|
|
456
|
-
| `
|
|
752
|
+
| Parameter | Type | Required | Notes |
|
|
753
|
+
| --------- | -------- | -------- | --------------------------- |
|
|
754
|
+
| `COLUMN` | a column | Yes | `[column]`, not a string |
|
|
755
|
+
|
|
756
|
+
Rules:
|
|
757
|
+
|
|
758
|
+
- The argument **must be a column node**. `FIELD(TODAY())` and
|
|
759
|
+
`FIELD("2026-01-01")` are rejected. Using `[column]` rather than a string
|
|
760
|
+
keeps the dependency visible to the editor, which reads it off the parsed
|
|
761
|
+
expression.
|
|
762
|
+
- The value comes from `datePreset.columnValues`, keyed by column id. Three
|
|
763
|
+
distinct failures, each with its own error: the argument is not a column, no
|
|
764
|
+
value was supplied (`MISSING_COLUMN_VALUE`), or the value is not a date
|
|
765
|
+
(`INVALID_COLUMN_VALUE`).
|
|
766
|
+
- **A bare `[column]` is not a date preset.** It used to resolve to the
|
|
767
|
+
column's own name in the date slot, marked valid, and `START_OF([column])`
|
|
768
|
+
threw a `RangeError` out of the public API. `FIELD([column])` is the one
|
|
769
|
+
sanctioned spelling.
|
|
457
770
|
|
|
458
771
|
Output: `date`.
|
|
459
772
|
|
|
460
|
-
Value type
|
|
773
|
+
Value type: `dataBound`, always `dynamic`. It moves with the data rather than
|
|
774
|
+
with the clock, and nothing in the expression can make it static.
|
|
461
775
|
|
|
462
776
|
### `PART(date, part)`
|
|
463
777
|
|
|
@@ -465,7 +779,10 @@ Extracts one part of a date.
|
|
|
465
779
|
|
|
466
780
|
```ts
|
|
467
781
|
PART(DATE("2026-07-08T15:30"), "DAY_OF_WEEK_NAME");
|
|
468
|
-
//
|
|
782
|
+
// 3 — the weekday ordinal, tagged as a weekday name
|
|
783
|
+
|
|
784
|
+
FORMAT(PART(DATE("2026-07-08T15:30"), "DAY_OF_WEEK_NAME"));
|
|
785
|
+
// "Wednesday"
|
|
469
786
|
```
|
|
470
787
|
|
|
471
788
|
Parameters:
|
|
@@ -477,31 +794,230 @@ Parameters:
|
|
|
477
794
|
|
|
478
795
|
Supported parts:
|
|
479
796
|
|
|
797
|
+
Every part is read from the date **as it falls in the resolution timezone**, so
|
|
798
|
+
a part never disagrees with the calendar day the rest of the engine resolved.
|
|
799
|
+
`PART(DATE("2026-08-01T02:00"), "MONTH")` is `8` in UTC and `7` under
|
|
800
|
+
`America/Bogota`, where that instant is 21:00 on 31 July.
|
|
801
|
+
|
|
480
802
|
| Value | Output |
|
|
481
803
|
| ------------------ | ---------------------------------------- |
|
|
482
|
-
| `YEAR` |
|
|
804
|
+
| `YEAR` | Year |
|
|
483
805
|
| `QUARTER` | Quarter 1-4 |
|
|
484
806
|
| `MONTH` | Month 1-12 |
|
|
485
|
-
| `MONTH_NAME` |
|
|
807
|
+
| `MONTH_NAME` | Month 1-12, tagged as a month name |
|
|
486
808
|
| `DAY` | Day of month |
|
|
487
809
|
| `DAY_OF_MONTH` | Day of month |
|
|
488
|
-
| `DAY_OF_WEEK` |
|
|
489
|
-
| `DAY_OF_WEEK_NAME` |
|
|
810
|
+
| `DAY_OF_WEEK` | Weekday, Sunday = 0 |
|
|
811
|
+
| `DAY_OF_WEEK_NAME` | Weekday 0-6, tagged as a weekday name |
|
|
490
812
|
| `DAY_OF_YEAR` | Day of year |
|
|
491
813
|
| `WEEK_OF_YEAR` | Week according to the current convention |
|
|
492
|
-
| `HOUR` |
|
|
493
|
-
| `MINUTE` |
|
|
494
|
-
| `SECOND` |
|
|
814
|
+
| `HOUR` | Hour |
|
|
815
|
+
| `MINUTE` | Minute |
|
|
816
|
+
| `SECOND` | Second |
|
|
817
|
+
|
|
818
|
+
`WEEK_OF_YEAR` is the only part that also consults `weekStartsOn`; the weekday
|
|
819
|
+
ordinal is always Sunday-based.
|
|
820
|
+
|
|
821
|
+
**`PART` extracts and never localizes.** Every part returns a number, including
|
|
822
|
+
the two `_NAME` values: `MONTH_NAME` is the month number and `DAY_OF_WEEK_NAME`
|
|
823
|
+
the weekday ordinal. They exist so a result can record *what kind* of number it
|
|
824
|
+
is — rendering "September" or "Montag" is a presentation concern and belongs to
|
|
825
|
+
`FORMAT`, which is the one place locale is read.
|
|
826
|
+
|
|
827
|
+
Because a part is a number, it can fill a numeric argument and take part in
|
|
828
|
+
arithmetic: `PERIOD_AT("MONTH", 4, PART(TODAY(), "YEAR") - 1)` is April of last
|
|
829
|
+
year.
|
|
830
|
+
|
|
831
|
+
`PART` is a valid final result. It resolves to a **scalar**:
|
|
832
|
+
|
|
833
|
+
```ts
|
|
834
|
+
type ScalarValue = {
|
|
835
|
+
value: number | string;
|
|
836
|
+
sourcePart: DatePresetSourcePart | null;
|
|
837
|
+
};
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
`sourcePart` records which part the number came from, so a formatter can tell a
|
|
841
|
+
month from a weekday. `PART(TODAY(), "MONTH")` is
|
|
842
|
+
`{ value: 7, sourcePart: "MONTH" }`, and `PART(TODAY(), "MONTH_NAME")` is the
|
|
843
|
+
same number under a different tag — which is the only difference between the
|
|
844
|
+
two, and the whole reason both still exist.
|
|
845
|
+
|
|
846
|
+
**Arithmetic clears the tag.** `PART(TODAY(), "YEAR") - 1` is
|
|
847
|
+
`{ value: 2025, sourcePart: null }`: once a part has been computed with, there
|
|
848
|
+
is no honest way to say what it means any more. Nine is a month; eighteen is
|
|
849
|
+
not, and the engine cannot tell where it stopped being one.
|
|
850
|
+
|
|
851
|
+
Both facts are recorded on the node, by the parser, and not recomputed by
|
|
852
|
+
walking the tree. A node carries `dateScalar` when a date produced its value,
|
|
853
|
+
and `dateScalar.sourcePart` names the part it still is; arithmetic keeps the
|
|
854
|
+
first and nulls the second. That is why `1 + 2` is refused while
|
|
855
|
+
`PART(TODAY(), "YEAR") - 1` resolves — the primitive is `number` for both, and
|
|
856
|
+
provenance is the only thing that separates them. The field is declared on its
|
|
857
|
+
own interface, `DateScalarProvenance`, which `CommonAST` extends, and a function
|
|
858
|
+
opts in with `datePresetScalar` and `datePresetSourcePart` on its definition.
|
|
859
|
+
|
|
860
|
+
A number is a result only when a date produced it. `PART(TODAY(), "YEAR") - 1`
|
|
861
|
+
resolves; `1 + 2` does not, and neither does a bare `5`. The difference is
|
|
862
|
+
provenance, not type.
|
|
863
|
+
|
|
864
|
+
The response reports `primitive: "dateScalar"` for one. That field answers
|
|
865
|
+
*what shape is `expression`*, and a scalar is a record — reporting `number`
|
|
866
|
+
would describe something the caller never receives.
|
|
867
|
+
|
|
868
|
+
Inside the expression tree a part is still a number and a rendered value is
|
|
869
|
+
still a string, which is what decides that `PART(TODAY(), "MONTH") + 1` is
|
|
870
|
+
arithmetic while `"texto" + 1` is not, and what lets a part fill a numeric
|
|
871
|
+
argument. That distinction is internal; it does not reach the response.
|
|
872
|
+
|
|
873
|
+
A scalar's `value` may therefore be a number or a string.
|
|
874
|
+
`FunctionDefinition.primitiveResult` accepts a callable, so a function whose
|
|
875
|
+
result type depends on its arguments computes it per call rather than declaring
|
|
876
|
+
one type and returning another.
|
|
877
|
+
|
|
878
|
+
Use `isScalarValue` to narrow a `DatePresetValue`; `isDateRangeValue` is
|
|
879
|
+
exported alongside it.
|
|
880
|
+
|
|
881
|
+
### `FORMAT(value, style?)`
|
|
882
|
+
|
|
883
|
+
Renders a resolved value for a viewer. It is the one function in the language
|
|
884
|
+
that reads `locale`, and the only way to turn a name tag into a word.
|
|
885
|
+
|
|
886
|
+
```ts
|
|
887
|
+
FORMAT(TODAY());
|
|
888
|
+
// "Jul 22, 2026"
|
|
889
|
+
|
|
890
|
+
FORMAT(CALENDAR_PERIOD("MONTH", 0));
|
|
891
|
+
// "Jul 1, 2026 – Jul 31, 2026"
|
|
892
|
+
|
|
893
|
+
FORMAT(PART(TODAY(), "MONTH"), "full");
|
|
894
|
+
// "July" — and "Julio" or "Juli" for another viewer
|
|
895
|
+
|
|
896
|
+
FORMAT(PART(NOW(), "HOUR"), "00");
|
|
897
|
+
// "14"
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
Parameters:
|
|
901
|
+
|
|
902
|
+
| Parameter | Type | Values |
|
|
903
|
+
| --------- | ---------------------------------------- | -------------------------- |
|
|
904
|
+
| `VALUE` | `date`, `dateRange`, `partialDate`, part | What there is to render |
|
|
905
|
+
| `STYLE` | `string`, optional | Depends on the value below |
|
|
906
|
+
|
|
907
|
+
**The style is written, never computed.** It has to be a string literal in the
|
|
908
|
+
formula. A computed style makes validity move with the clock: the inner render
|
|
909
|
+
of `FORMAT(TODAY(), FORMAT(PART(NOW(), "HOUR"), "00"))` spells a valid pattern
|
|
910
|
+
only while the hour is under ten, so the same formula would be accepted in the
|
|
911
|
+
morning and rejected in the afternoon.
|
|
912
|
+
|
|
913
|
+
**The style has to belong to the shape being rendered.** An unsupported
|
|
914
|
+
combination is `INVALID_ALLOW_VALUE`, never a silent fallback — a permissive
|
|
915
|
+
renderer answers `"0909"` to `FORMAT(TODAY(), "MMMM")`, which is the CLDR
|
|
916
|
+
spelling a user reaches for to get a month name.
|
|
917
|
+
|
|
918
|
+
For a `date` or a `dateRange`:
|
|
919
|
+
|
|
920
|
+
| Style | Renders |
|
|
921
|
+
| ------------------------------------------------- | -------------------------------- |
|
|
922
|
+
| omitted | Locale medium |
|
|
923
|
+
| `full`, `long`, `medium`, `short`, `short-padded` | The corresponding `dateStyle` |
|
|
924
|
+
| A token pattern | `yyyy-MM-dd`, `dd/MM/yyyy HH:mm` |
|
|
925
|
+
|
|
926
|
+
A token pattern is read run by run, and every run of a letter has to be a
|
|
927
|
+
supported token: `yyyy`, `yy`, `MM`, `M`, `dd`, `d`, `HH`, `H`, `mm`, `m`, `ss`,
|
|
928
|
+
`s`. So `MMMM` is a run of four `M`s and is rejected rather than read as two
|
|
929
|
+
month numbers, and `yyyyy` is not `yyyy` followed by `y`.
|
|
930
|
+
|
|
931
|
+
**A date carries a time only when it has one.** `FORMAT(TODAY())` is
|
|
932
|
+
`"Jul 22, 2026"` and `FORMAT(NOW())` is `"Jul 22, 2026, 2:35 PM"`; the value
|
|
933
|
+
decides, read in the timezone it will be rendered in, so `FORMAT(END_OF(TODAY()))`
|
|
934
|
+
still shows `11:59 PM`. A range takes the decision from its start, because an
|
|
935
|
+
endpoint that lands on 23:59:59.999 is a boundary and not a time anyone asked
|
|
936
|
+
to see. `FormatDatePreset` is unchanged: its default still includes the time.
|
|
937
|
+
|
|
938
|
+
For a `partialDate`, the vocabulary is `long` and `short`. A partial date has no
|
|
939
|
+
instant, so a token pattern has nothing to read.
|
|
940
|
+
|
|
941
|
+
For a **scalar**, the style depends on the part the number came from — this is
|
|
942
|
+
QRV-1069 §5.7, and it is why `PART` carries a tag at all:
|
|
943
|
+
|
|
944
|
+
| Scalar source | Pattern (`00`) | Name (`full`, `short`) | Omitted |
|
|
945
|
+
| ------------------------------------------------------------------------------------------ | -------------- | ---------------------- | --------- |
|
|
946
|
+
| Numeric part with a name mapping — `MONTH`, `DAY_OF_WEEK` | yes | yes | the digit |
|
|
947
|
+
| Numeric part with no name mapping — `YEAR`, `QUARTER`, `DAY`, `DAY_OF_MONTH`, `DAY_OF_YEAR`, `WEEK_OF_YEAR`, `HOUR`, `MINUTE`, `SECOND` | yes | rejected | the digit |
|
|
948
|
+
| Name part — `MONTH_NAME`, `DAY_OF_WEEK_NAME` | rejected | yes | the name |
|
|
949
|
+
| `TIMEZONE()` / `CONTEXT(property)` — neither exists yet | rejected | rejected | — |
|
|
950
|
+
|
|
951
|
+
A scalar pattern is a run of one to four zeros and means the width to pad to:
|
|
952
|
+
`FORMAT(PART(TODAY(), "MONTH"), "00")` is `"07"`.
|
|
953
|
+
|
|
954
|
+
**A scalar has to still carry its tag.** `FORMAT(PART(TODAY(), "YEAR") - 1)` is
|
|
955
|
+
rejected: arithmetic clears the tag, and a formatter must not guess it back. So
|
|
956
|
+
is `FORMAT(FORMAT(x))`, since a rendered value has nothing left to render.
|
|
495
957
|
|
|
496
|
-
|
|
958
|
+
A malformed `locale` in the resolution context is `UNRENDERABLE_VALUE` rather
|
|
959
|
+
than a `RangeError` out of the public API.
|
|
960
|
+
|
|
961
|
+
Output: `dateScalar`, always with `sourcePart: null`.
|
|
962
|
+
|
|
963
|
+
Value type: `scalar`. Temporality follows the value it rendered — `FORMAT(TODAY())`
|
|
964
|
+
is `dynamic`, `FORMAT(DATE("2026-07-08"))` is `static`. Presentation is not an
|
|
965
|
+
axis of the classification: the same formula classifies identically for every
|
|
966
|
+
viewer, and only its value differs.
|
|
967
|
+
|
|
968
|
+
`FORMAT` has no case in `TranspileJSONToDatePreset`, on the same reading that
|
|
969
|
+
gives `PART` and `FIELD` none: the picker constructs date preset nodes, and
|
|
970
|
+
these three are read out of one.
|
|
971
|
+
|
|
972
|
+
## Calendar-part matching
|
|
973
|
+
|
|
974
|
+
A field can be matched against a `partialDate` pattern regardless of year — "all
|
|
975
|
+
orders shipped in September", "orders placed on a Monday". QRV-1069 defines that
|
|
976
|
+
as a **filter operator**, not an expression-language function, so there is no
|
|
977
|
+
`MATCHES(...)` to call. What this library ships is the engine behind it:
|
|
978
|
+
|
|
979
|
+
```ts
|
|
980
|
+
DatePresetMatches(value: string | Date, pattern, context?): boolean
|
|
981
|
+
PartialDateToRanges(pattern: PartialDateValue, horizon: DateRangeValue, context?): DateRangeValue[]
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
`DatePresetMatches` takes a `partialDate` or a `dateRange`. A range is an
|
|
985
|
+
instant comparison, inclusive at both ends. A partial date is a **conjunction**:
|
|
986
|
+
every stated part must match, an omitted part matches anything.
|
|
987
|
+
|
|
988
|
+
```ts
|
|
989
|
+
DatePresetMatches("2026-09-14T00:00:00.000Z", { month: 9 }); // true
|
|
990
|
+
DatePresetMatches("2026-09-15T00:00:00.000Z", { month: 9, day: 14 }); // false
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
Three things worth knowing, all consequences of matching through `PART`:
|
|
994
|
+
|
|
995
|
+
- It resolves in the context timezone. `2026-08-01T02:00Z` matches
|
|
996
|
+
`{ month: 7 }` under `America/Bogota`, where that instant is 31 July.
|
|
997
|
+
- The week follows `weekStartsOn` and is **not ISO-8601**. The same date is
|
|
998
|
+
week 30 with Sunday weeks and week 29 with Monday weeks.
|
|
999
|
+
- The quarter is **Gregorian**, even under a fiscal calendar. Extraction is
|
|
1000
|
+
Gregorian, and a fiscal quarter is a different question from a calendar one.
|
|
1001
|
+
|
|
1002
|
+
`PartialDateToRanges` expands a pattern into the concrete ranges it covers
|
|
1003
|
+
inside a horizon, so a future SQL emission can be
|
|
1004
|
+
`col BETWEEN … OR col BETWEEN …` rather than per-engine `EXTRACT`. The
|
|
1005
|
+
boundaries are computed once, by the same code that answers
|
|
1006
|
+
`DatePresetMatches`, which is what keeps the two in agreement: PostgreSQL's
|
|
1007
|
+
`EXTRACT(WEEK …)` is ISO-only, and `DAYOFWEEK` ships with five different bases
|
|
1008
|
+
across the engines. Its cost is linear in the days of the horizon.
|
|
497
1009
|
|
|
498
1010
|
## Differences Between Period Functions
|
|
499
1011
|
|
|
500
|
-
| Function | Question it answers | Example | Type
|
|
501
|
-
| ----------------- | -------------------------------------------- | ----------------------------- |
|
|
502
|
-
| `CALENDAR_PERIOD` | What is this/previous/next calendar period? | This month, last week | `
|
|
503
|
-
| `
|
|
504
|
-
| `
|
|
1012
|
+
| Function | Question it answers | Example | Type |
|
|
1013
|
+
| ----------------- | -------------------------------------------- | ----------------------------- | --------- |
|
|
1014
|
+
| `CALENDAR_PERIOD` | What is this/previous/next calendar period? | This month, last week | `aligned` |
|
|
1015
|
+
| `TO_DATE` | How much of the current period has elapsed? | Month to date, year to date | `aligned` |
|
|
1016
|
+
| `PERIOD_AT` | What is period N inside a year? | April 2026, Q2 2026, W40 2026 | `aligned` |
|
|
1017
|
+
| `RELATIVE_PERIOD` | What is the moving window from today/anchor? | Last 30 days | `rolling` |
|
|
1018
|
+
| `OFFSET` | What is the single instant N units away? | 7 days ago, tomorrow | `rolling` |
|
|
1019
|
+
| `SAME_PERIOD` | What is this range, N units away? | The same quarter a year back | delegated |
|
|
1020
|
+
| `PRIOR_PERIOD` | What came immediately before this range? | The previous 45 days | delegated |
|
|
505
1021
|
|
|
506
1022
|
Examples with current date `2026-07-22`:
|
|
507
1023
|
|
|
@@ -532,9 +1048,11 @@ It is not recommended to merge them into a single public function because they e
|
|
|
532
1048
|
{ type: "PARTIAL_DATE", value: { year?, quarter?, month?, week?, day? } }
|
|
533
1049
|
{ type: "RELATIVE_PERIOD", offset: -30, unit: "DAY", anchor?: DatePresetJSON }
|
|
534
1050
|
{ type: "CALENDAR_PERIOD", period: "MONTH", offset?: 0 }
|
|
1051
|
+
{ type: "TO_DATE", unit: "MONTH" }
|
|
535
1052
|
{ type: "PERIOD_AT", period: "MONTH", position: 4 | "FIRST" | "LAST", year?: 2026 }
|
|
536
|
-
{ type: "
|
|
537
|
-
{ type: "
|
|
1053
|
+
{ type: "OFFSET", point: DatePresetJSON, offset: -7, unit: "DAY", overflowMode?: "CLAMP" }
|
|
1054
|
+
{ type: "START_OF", input: DatePresetJSON, unit?: "MONTH" }
|
|
1055
|
+
{ type: "END_OF", input: DatePresetJSON, unit?: "MONTH" }
|
|
538
1056
|
```
|
|
539
1057
|
|
|
540
1058
|
Example:
|
|
@@ -576,6 +1094,12 @@ Rules:
|
|
|
576
1094
|
|
|
577
1095
|
## Formatting
|
|
578
1096
|
|
|
1097
|
+
There are two doors into rendering, and they are not the same door.
|
|
1098
|
+
`FormatDatePreset` is a function a caller applies to a value it already has;
|
|
1099
|
+
`FORMAT` is a function the *formula* applies, so its result is the date preset
|
|
1100
|
+
and the style is persisted with the expression. Both read the same renderers in
|
|
1101
|
+
`src/utils/datePresetFormat.ts`; only `FORMAT` enforces §5.7.
|
|
1102
|
+
|
|
579
1103
|
`FormatDatePreset` formats values using `Intl.DateTimeFormat`.
|
|
580
1104
|
|
|
581
1105
|
By default, `date` and `dateRange` values include time using `timeStyle: "short"`. When `partialDateStyle: "short"` is used and no explicit `timeStyle` is provided, `date` and `dateRange` values omit time so they align with picker-style labels. Passing `timeStyle` explicitly keeps time in the output.
|
|
@@ -671,19 +1195,44 @@ FormatDatePreset(
|
|
|
671
1195
|
|
|
672
1196
|
Legacy tokens are resolved with `TranspileJSONToDatePreset(token)` using `DATE_PRESET_TOKEN_MAP`.
|
|
673
1197
|
|
|
1198
|
+
Five of these resolve to a **scalar** rather than a date: `CURRENT_DAY`,
|
|
1199
|
+
`CURRENT_DAY_OF_WEEK`, `CURRENT_DAY_OF_YEAR`, `CURRENT_MONTH_NAME` and
|
|
1200
|
+
`LAST_MONTH_NAME`. The two `_NAME` ones carry a number with a name tag, which
|
|
1201
|
+
`FORMAT` renders as a word. `CURRENT_TIMEZONE` is not here yet — it needs
|
|
1202
|
+
`TIMEZONE()`.
|
|
1203
|
+
|
|
1204
|
+
`LAST_MONTH_NAME` wraps its period in `START_OF`. Without it, `PART` receives a
|
|
1205
|
+
`dateRange` where it validates a `date`, and the expression does not resolve.
|
|
1206
|
+
|
|
1207
|
+
The token name is matched case-insensitively, and may be wrapped in `{{ }}`:
|
|
1208
|
+
`TODAY-7`, `today-7` and `{{ Today-7 }}` all resolve to the same expression. A
|
|
1209
|
+
name that is not a token returns `undefined` from `resolveDatePresetToken` and
|
|
1210
|
+
throws from `TranspileJSONToDatePreset`.
|
|
1211
|
+
|
|
674
1212
|
| Token | QDP expression |
|
|
675
1213
|
| ----------------------------- | --------------------------------------------- |
|
|
676
1214
|
| `NOW` | `NOW()` |
|
|
677
1215
|
| `TODAY` | `TODAY()` |
|
|
678
1216
|
| `CURRENT_DATE` | `TODAY()` |
|
|
679
|
-
| `
|
|
680
|
-
| `
|
|
681
|
-
| `
|
|
682
|
-
| `
|
|
683
|
-
| `
|
|
684
|
-
| `
|
|
685
|
-
| `
|
|
686
|
-
| `
|
|
1217
|
+
| `CURRENT_TIME` | `NOW()` |
|
|
1218
|
+
| `CURRENT_DAY` | `PART(TODAY(), "DAY_OF_WEEK_NAME")` |
|
|
1219
|
+
| `CURRENT_DAY_OF_WEEK` | `PART(TODAY(), "DAY_OF_WEEK")` |
|
|
1220
|
+
| `CURRENT_DAY_OF_YEAR` | `PART(TODAY(), "DAY_OF_YEAR")` |
|
|
1221
|
+
| `CURRENT_MONTH_NAME` | `PART(TODAY(), "MONTH_NAME")` |
|
|
1222
|
+
| `LAST_MONTH_NAME` | `PART(START_OF(CALENDAR_PERIOD("MONTH", -1)), "MONTH_NAME")` |
|
|
1223
|
+
| `TODAY-7` | `OFFSET(TODAY(), -7, "DAY")` |
|
|
1224
|
+
| `TODAY-30` | `OFFSET(TODAY(), -30, "DAY")` |
|
|
1225
|
+
| `TODAY-60` | `OFFSET(TODAY(), -60, "DAY")` |
|
|
1226
|
+
| `TODAY-90` | `OFFSET(TODAY(), -90, "DAY")` |
|
|
1227
|
+
| `TODAY-120` | `OFFSET(TODAY(), -120, "DAY")` |
|
|
1228
|
+
| `TODAY-365` | `OFFSET(TODAY(), -365, "DAY")` |
|
|
1229
|
+
| `YESTERDAY` | `OFFSET(TODAY(), -1, "DAY")` |
|
|
1230
|
+
| `LAST_7_DAYS` | `RELATIVE_PERIOD(-6, "DAY")` |
|
|
1231
|
+
| `LAST_30_DAYS` | `RELATIVE_PERIOD(-29, "DAY")` |
|
|
1232
|
+
| `TOMORROW` | `OFFSET(TODAY(), 1, "DAY")` |
|
|
1233
|
+
| `MTD` | `TO_DATE("MONTH")` |
|
|
1234
|
+
| `QTD` | `TO_DATE("QUARTER")` |
|
|
1235
|
+
| `YTD` | `TO_DATE("YEAR")` |
|
|
687
1236
|
| `CURRENT_MONTH` | `CALENDAR_PERIOD("MONTH", 0)` |
|
|
688
1237
|
| `CURRENT_MONTH_START` | `START_OF(CALENDAR_PERIOD("MONTH", 0))` |
|
|
689
1238
|
| `CURRENT_MONTH_END` | `END_OF(CALENDAR_PERIOD("MONTH", 0))` |
|
|
@@ -704,6 +1253,7 @@ Legacy tokens are resolved with `TranspileJSONToDatePreset(token)` using `DATE_P
|
|
|
704
1253
|
| `LAST_QUARTER_END` | `END_OF(CALENDAR_PERIOD("QUARTER", -1))` |
|
|
705
1254
|
| `CURRENT_YEAR` | `CALENDAR_PERIOD("YEAR", 0)` |
|
|
706
1255
|
| `CURRENT_YEAR_START` | `START_OF(CALENDAR_PERIOD("YEAR", 0))` |
|
|
1256
|
+
| `CURRENT_YEAR_END` | `END_OF(CALENDAR_PERIOD("YEAR", 0))` |
|
|
707
1257
|
| `LAST_YEAR` | `CALENDAR_PERIOD("YEAR", -1)` |
|
|
708
1258
|
| `LAST_YEAR_START` | `START_OF(CALENDAR_PERIOD("YEAR", -1))` |
|
|
709
1259
|
| `LAST_YEAR_END` | `END_OF(CALENDAR_PERIOD("YEAR", -1))` |
|
|
@@ -712,18 +1262,20 @@ Legacy tokens are resolved with `TranspileJSONToDatePreset(token)` using `DATE_P
|
|
|
712
1262
|
|
|
713
1263
|
Tokens can be wrapped like `{{TODAY-7}}`. The normalizer removes braces and outer spaces.
|
|
714
1264
|
|
|
715
|
-
|
|
1265
|
+
`CURRENT_TIMEZONE` is not supported yet: it needs `TIMEZONE()`, which reads a property of the resolution context rather than extracting one from a date.
|
|
716
1266
|
|
|
717
1267
|
## Main Equivalences With The Date Picker
|
|
718
1268
|
|
|
719
1269
|
| Picker expression | QDP expression | Note |
|
|
720
1270
|
| --------------------------- | ------------------------------------------------------------ | -------------------------------------------------- |
|
|
721
1271
|
| Today | `TODAY()` | Fixed date at the start of the current day |
|
|
722
|
-
| Yesterday | `
|
|
723
|
-
| Tomorrow | `
|
|
1272
|
+
| Yesterday | `OFFSET(TODAY(), -1, "DAY")` | Start of yesterday |
|
|
1273
|
+
| Tomorrow | `OFFSET(TODAY(), 1, "DAY")` | Start of tomorrow |
|
|
724
1274
|
| This month | `CALENDAR_PERIOD("MONTH", 0)` | Current calendar month |
|
|
725
1275
|
| Previous month | `CALENDAR_PERIOD("MONTH", -1)` | Previous calendar month |
|
|
1276
|
+
| Last 7 days inclusive | `RELATIVE_PERIOD(-6, "DAY")` | Inclusive 7-day rolling window counting today |
|
|
726
1277
|
| Last 30 days inclusive | `RELATIVE_PERIOD(-29, "DAY")` | Inclusive 30-day rolling window counting today |
|
|
1278
|
+
| Month to date | `TO_DATE("MONTH")` | Start of the month to the end of today |
|
|
727
1279
|
| April | `PARTIAL_DATE("ANY", 4)` | Recurring month |
|
|
728
1280
|
| April 2026 | `PERIOD_AT("MONTH", 4, 2026)` | Positional month in a year |
|
|
729
1281
|
| Apr 15 | `PARTIAL_DATE("ANY", 4, "ANY", "ANY", 15)` | Recurring month/day |
|
|
@@ -764,29 +1316,38 @@ DATE_RANGE(DATE("2026-07-31T00:00"), DATE("2026-07-01T00:00"));
|
|
|
764
1316
|
// INVALID_DATE_RANGE: end before start
|
|
765
1317
|
```
|
|
766
1318
|
|
|
767
|
-
Additionally, any QDP expression whose final result is not `date`, `dateRange`,
|
|
1319
|
+
Additionally, any QDP expression whose final result is not `date`, `dateRange`,
|
|
1320
|
+
`partialDate`, or a scalar a date produced returns `INVALID_DATE_PRESET_RESULT`.
|
|
768
1321
|
|
|
769
1322
|
## Relevant Files
|
|
770
1323
|
|
|
771
1324
|
| File | Responsibility |
|
|
772
1325
|
| --------------------------------------------- | ----------------------------------------------------------------------- |
|
|
773
|
-
| `src/date-presets/date-presets.ts` | Public API, JSON conversion,
|
|
1326
|
+
| `src/date-presets/date-presets.ts` | Public API, JSON conversion, and the result guard |
|
|
1327
|
+
| `src/date-presets/date-preset-classification.ts` | The `valueType` / `temporality` slot map and classifier |
|
|
774
1328
|
| `src/date-presets/date-preset-tokens.ts` | Legacy token map to QDP expressions |
|
|
775
1329
|
| `src/functions/index.ts` | Function registration for `ENGINES.QDP` |
|
|
1330
|
+
| `src/utils/extractContextInfo.ts` | Splits the appended resolution context off a transpiler's arguments |
|
|
776
1331
|
| `src/utils/datePresetUtils.ts` | Date, range, calendar, week, and partial date resolution |
|
|
1332
|
+
| `src/utils/datePresetFormat.ts` | Every renderer, and the only module that calls `Intl.DateTimeFormat` |
|
|
1333
|
+
| `src/parser/formula-parser.ts` | Stamps `isDateScalar` and `sourcePart` onto a call node |
|
|
1334
|
+
| `src/functions/format.ts` | `FORMAT`: the §5.7 validity table and the routing to a renderer |
|
|
777
1335
|
| `src/utils/timezone.ts` | Timezone definition normalization and offset helpers |
|
|
778
1336
|
| `src/functions/*.ts` | Individual QDP function definitions |
|
|
779
|
-
| `__tests__/unit/
|
|
780
|
-
| `__tests__/unit/
|
|
781
|
-
| `__tests__/unit/datePresetFormat.test.ts` |
|
|
1337
|
+
| `__tests__/unit/datePreset*.test.ts` | Transpilation, classification, every function, tokens and JSON conversion |
|
|
1338
|
+
| `__tests__/unit/extractContextInfo.test.ts` | The context-info split and its registration guard |
|
|
1339
|
+
| `__tests__/unit/datePresetFormat.test.ts` | `FormatDatePreset`: date and partial date formatting |
|
|
1340
|
+
| `__tests__/unit/datePresetFormatFunction.test.ts` | `FORMAT` in the language, and the §5.7 table cell by cell |
|
|
782
1341
|
| `__tests__/unit/datePresetTokens.test.ts` | Legacy tokens |
|
|
1342
|
+
| `__tests__/unit/datePresetCalendar.test.ts` | Fiscal, retail and timezone calendar resolution |
|
|
1343
|
+
| `scripts/generate-date-preset-examples.js` | Generator for the examples CSV |
|
|
783
1344
|
| `date-preset-qdp-examples.csv` | Generated examples with tokens and picker expressions |
|
|
784
1345
|
|
|
785
1346
|
## Current State And Design Notes
|
|
786
1347
|
|
|
787
1348
|
- QDP is limited to date preset functions in `ENGINE_FN_MAP[ENGINES.QDP]`.
|
|
788
|
-
- Date preset expressions can be nested as long as the final result is `date`, `dateRange`, or
|
|
1349
|
+
- Date preset expressions can be nested as long as the final result is `date`, `dateRange`, `partialDate`, or a scalar read from a date.
|
|
789
1350
|
- `partialDate` was added as a primitive to represent incomplete date picker selections.
|
|
790
1351
|
- Week compatibility with the date picker uses Sunday as the default start day and supports W54.
|
|
791
1352
|
- `PERIOD_AT` supports symbolic positions `FIRST` and `LAST` to reduce ambiguity in variable-length periods.
|
|
792
|
-
- For documentation and example auditing, the CSV `date-preset-qdp-examples.csv` contains resolved, formatted, and classified values.
|
|
1353
|
+
- For documentation and example auditing, the CSV `date-preset-qdp-examples.csv` contains resolved, formatted, and classified values. Regenerate it with `npm run examples:csv`; the reference date defaults to `2026-07-22`, matching this document. Note the unit tests pin a different instant (`2026-07-15T14:35:27.000Z`).
|