@qrvey/formula-lang 3.2.0-rc.1104 → 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 +633 -70
- 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/cjs/utils/timezone.js +7 -3
- package/dist/cjs/utils/timezone.js.map +1 -1
- 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/dist/module/utils/timezone.js +7 -3
- package/dist/module/utils/timezone.js.map +1 -1
- 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
|
```
|
|
@@ -107,7 +147,7 @@ Current defaults:
|
|
|
107
147
|
|
|
108
148
|
| Option | Default | Notes |
|
|
109
149
|
| ----------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------ |
|
|
110
|
-
| `timezone.offset` | `+00:00` | Supports `default`, `browser`, or custom offsets such as `UTC-5`, `-05:00`, `+5:30`
|
|
150
|
+
| `timezone.offset` | `+00:00` | Supports `default`, `browser`, or custom offsets such as `UTC-5`, `-05:00`, `+5:30`. Valid range is `-12:00` to `+14:00`; offsets outside it fall back to `+00:00` |
|
|
111
151
|
| `timezone.timeZone` | none | Optional IANA time zone identifier such as `America/Bogota`; when present it is used for calendar resolution |
|
|
112
152
|
| `calendar` | `gregorian` | Also supports `corporate-fiscal`, `retail-4-4-5`, `retail-4-5-4` |
|
|
113
153
|
| `locale` | `en-US` | Used by formatting options |
|
|
@@ -115,9 +155,12 @@ 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
|
|
|
162
|
+
Custom offsets accept the full UTC range `-12:00` to `+14:00`, including fractional offsets such as `+05:30` (India), `+05:45` (Nepal), or `-09:30` (Marquesas). Any minute value `00`-`59` is accepted; offsets outside the valid range fall back to `+00:00`.
|
|
163
|
+
|
|
121
164
|
### Expression timezone output
|
|
122
165
|
|
|
123
166
|
Date preset resolution always calculates concrete instants internally. The `datePreset.applyTimezoneToExpression` option controls only the public `expression` format returned by `TranspileDatePreset`.
|
|
@@ -174,7 +217,7 @@ NOW();
|
|
|
174
217
|
|
|
175
218
|
Output: `date`.
|
|
176
219
|
|
|
177
|
-
Value type when used as the final result: `
|
|
220
|
+
Value type when used as the final result: `explicit`, `dynamic`.
|
|
178
221
|
|
|
179
222
|
### `TODAY()`
|
|
180
223
|
|
|
@@ -187,7 +230,7 @@ TODAY();
|
|
|
187
230
|
|
|
188
231
|
Output: `date`.
|
|
189
232
|
|
|
190
|
-
Value type when used as the final result: `
|
|
233
|
+
Value type when used as the final result: `explicit`, `dynamic`.
|
|
191
234
|
|
|
192
235
|
### `DATE(value)`
|
|
193
236
|
|
|
@@ -221,7 +264,7 @@ Rules:
|
|
|
221
264
|
|
|
222
265
|
Output: `date`.
|
|
223
266
|
|
|
224
|
-
Value type: `
|
|
267
|
+
Value type: `explicit`, `static`.
|
|
225
268
|
|
|
226
269
|
### `DATE_RANGE(start, end)`
|
|
227
270
|
|
|
@@ -246,7 +289,7 @@ Rules:
|
|
|
246
289
|
|
|
247
290
|
Output: `dateRange`.
|
|
248
291
|
|
|
249
|
-
Value type: `
|
|
292
|
+
Value type: `explicit`. Temporality is derived from `START` and `END`.
|
|
250
293
|
|
|
251
294
|
### `RELATIVE_PERIOD(offset, unit, anchor?)`
|
|
252
295
|
|
|
@@ -280,7 +323,61 @@ RELATIVE_PERIOD(-2, "WEEK", DATE("2026-07-08T15:30"));
|
|
|
280
323
|
|
|
281
324
|
Output: `dateRange`.
|
|
282
325
|
|
|
283
|
-
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`.
|
|
284
381
|
|
|
285
382
|
### `CALENDAR_PERIOD(period, offset?)`
|
|
286
383
|
|
|
@@ -317,7 +414,47 @@ CALENDAR_PERIOD("QUARTER", -1);
|
|
|
317
414
|
|
|
318
415
|
Output: `dateRange`.
|
|
319
416
|
|
|
320
|
-
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.
|
|
321
458
|
|
|
322
459
|
### `PERIOD_AT(period, position, year?)`
|
|
323
460
|
|
|
@@ -358,7 +495,8 @@ Week rules:
|
|
|
358
495
|
|
|
359
496
|
Output: `dateRange`.
|
|
360
497
|
|
|
361
|
-
|
|
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.
|
|
362
500
|
|
|
363
501
|
### `PARTIAL_DATE(year?, month?, quarter?, week?, day?)`
|
|
364
502
|
|
|
@@ -407,15 +545,27 @@ PARTIAL_DATE("ANY", "ANY", "ANY", "ANY", 15);
|
|
|
407
545
|
// Short format: Day 15
|
|
408
546
|
```
|
|
409
547
|
|
|
410
|
-
|
|
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.
|
|
411
560
|
|
|
412
|
-
|
|
413
|
-
- `relative` if it includes `week`.
|
|
414
|
-
- `recurring` for all other partial dates.
|
|
561
|
+
Value type, always with temporality `timeless`:
|
|
415
562
|
|
|
416
|
-
|
|
563
|
+
- `explicit` if it includes `year`, `month`, and `day`, without `week`.
|
|
564
|
+
- `recurring` for every other partial date, `week` included.
|
|
417
565
|
|
|
418
|
-
|
|
566
|
+
### `START_OF(value, unit?)`
|
|
567
|
+
|
|
568
|
+
Returns the start of `VALUE`, or the start of the `UNIT` period containing it.
|
|
419
569
|
|
|
420
570
|
```ts
|
|
421
571
|
START_OF(DATE("2026-07-08T15:30"));
|
|
@@ -423,21 +573,45 @@ START_OF(DATE("2026-07-08T15:30"));
|
|
|
423
573
|
|
|
424
574
|
START_OF(CALENDAR_PERIOD("MONTH", -1));
|
|
425
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
|
|
426
579
|
```
|
|
427
580
|
|
|
428
|
-
|
|
581
|
+
Parameters:
|
|
582
|
+
|
|
583
|
+
| Parameter | Type | Required |
|
|
584
|
+
| --------- | --------------------- | -------- |
|
|
585
|
+
| `VALUE` | `date` or `dateRange` | Yes |
|
|
586
|
+
| `UNIT` | `string` | No |
|
|
429
587
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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.
|
|
433
606
|
|
|
434
607
|
Output: `date`.
|
|
435
608
|
|
|
436
|
-
Value type
|
|
609
|
+
Value type: delegated to `VALUE`, with or without `UNIT`. Temporality is derived
|
|
610
|
+
from `VALUE`.
|
|
437
611
|
|
|
438
|
-
### `END_OF(value)`
|
|
612
|
+
### `END_OF(value, unit?)`
|
|
439
613
|
|
|
440
|
-
Returns the end of
|
|
614
|
+
Returns the end of `VALUE`, or the end of the `UNIT` period containing it.
|
|
441
615
|
|
|
442
616
|
```ts
|
|
443
617
|
END_OF(DATE("2026-07-08T15:30"));
|
|
@@ -445,17 +619,159 @@ END_OF(DATE("2026-07-08T15:30"));
|
|
|
445
619
|
|
|
446
620
|
END_OF(CALENDAR_PERIOD("YEAR", -1));
|
|
447
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
|
|
448
748
|
```
|
|
449
749
|
|
|
450
750
|
Parameter:
|
|
451
751
|
|
|
452
|
-
| Parameter | Type
|
|
453
|
-
| --------- |
|
|
454
|
-
| `
|
|
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.
|
|
455
770
|
|
|
456
771
|
Output: `date`.
|
|
457
772
|
|
|
458
|
-
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.
|
|
459
775
|
|
|
460
776
|
### `PART(date, part)`
|
|
461
777
|
|
|
@@ -463,7 +779,10 @@ Extracts one part of a date.
|
|
|
463
779
|
|
|
464
780
|
```ts
|
|
465
781
|
PART(DATE("2026-07-08T15:30"), "DAY_OF_WEEK_NAME");
|
|
466
|
-
//
|
|
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"
|
|
467
786
|
```
|
|
468
787
|
|
|
469
788
|
Parameters:
|
|
@@ -475,31 +794,230 @@ Parameters:
|
|
|
475
794
|
|
|
476
795
|
Supported parts:
|
|
477
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
|
+
|
|
478
802
|
| Value | Output |
|
|
479
803
|
| ------------------ | ---------------------------------------- |
|
|
480
|
-
| `YEAR` |
|
|
804
|
+
| `YEAR` | Year |
|
|
481
805
|
| `QUARTER` | Quarter 1-4 |
|
|
482
806
|
| `MONTH` | Month 1-12 |
|
|
483
|
-
| `MONTH_NAME` |
|
|
807
|
+
| `MONTH_NAME` | Month 1-12, tagged as a month name |
|
|
484
808
|
| `DAY` | Day of month |
|
|
485
809
|
| `DAY_OF_MONTH` | Day of month |
|
|
486
|
-
| `DAY_OF_WEEK` |
|
|
487
|
-
| `DAY_OF_WEEK_NAME` |
|
|
810
|
+
| `DAY_OF_WEEK` | Weekday, Sunday = 0 |
|
|
811
|
+
| `DAY_OF_WEEK_NAME` | Weekday 0-6, tagged as a weekday name |
|
|
488
812
|
| `DAY_OF_YEAR` | Day of year |
|
|
489
813
|
| `WEEK_OF_YEAR` | Week according to the current convention |
|
|
490
|
-
| `HOUR` |
|
|
491
|
-
| `MINUTE` |
|
|
492
|
-
| `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.
|
|
493
957
|
|
|
494
|
-
|
|
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.
|
|
495
1009
|
|
|
496
1010
|
## Differences Between Period Functions
|
|
497
1011
|
|
|
498
|
-
| Function | Question it answers | Example | Type
|
|
499
|
-
| ----------------- | -------------------------------------------- | ----------------------------- |
|
|
500
|
-
| `CALENDAR_PERIOD` | What is this/previous/next calendar period? | This month, last week | `
|
|
501
|
-
| `
|
|
502
|
-
| `
|
|
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 |
|
|
503
1021
|
|
|
504
1022
|
Examples with current date `2026-07-22`:
|
|
505
1023
|
|
|
@@ -530,9 +1048,11 @@ It is not recommended to merge them into a single public function because they e
|
|
|
530
1048
|
{ type: "PARTIAL_DATE", value: { year?, quarter?, month?, week?, day? } }
|
|
531
1049
|
{ type: "RELATIVE_PERIOD", offset: -30, unit: "DAY", anchor?: DatePresetJSON }
|
|
532
1050
|
{ type: "CALENDAR_PERIOD", period: "MONTH", offset?: 0 }
|
|
1051
|
+
{ type: "TO_DATE", unit: "MONTH" }
|
|
533
1052
|
{ type: "PERIOD_AT", period: "MONTH", position: 4 | "FIRST" | "LAST", year?: 2026 }
|
|
534
|
-
{ type: "
|
|
535
|
-
{ 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" }
|
|
536
1056
|
```
|
|
537
1057
|
|
|
538
1058
|
Example:
|
|
@@ -574,6 +1094,12 @@ Rules:
|
|
|
574
1094
|
|
|
575
1095
|
## Formatting
|
|
576
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
|
+
|
|
577
1103
|
`FormatDatePreset` formats values using `Intl.DateTimeFormat`.
|
|
578
1104
|
|
|
579
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.
|
|
@@ -669,19 +1195,44 @@ FormatDatePreset(
|
|
|
669
1195
|
|
|
670
1196
|
Legacy tokens are resolved with `TranspileJSONToDatePreset(token)` using `DATE_PRESET_TOKEN_MAP`.
|
|
671
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
|
+
|
|
672
1212
|
| Token | QDP expression |
|
|
673
1213
|
| ----------------------------- | --------------------------------------------- |
|
|
674
1214
|
| `NOW` | `NOW()` |
|
|
675
1215
|
| `TODAY` | `TODAY()` |
|
|
676
1216
|
| `CURRENT_DATE` | `TODAY()` |
|
|
677
|
-
| `
|
|
678
|
-
| `
|
|
679
|
-
| `
|
|
680
|
-
| `
|
|
681
|
-
| `
|
|
682
|
-
| `
|
|
683
|
-
| `
|
|
684
|
-
| `
|
|
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")` |
|
|
685
1236
|
| `CURRENT_MONTH` | `CALENDAR_PERIOD("MONTH", 0)` |
|
|
686
1237
|
| `CURRENT_MONTH_START` | `START_OF(CALENDAR_PERIOD("MONTH", 0))` |
|
|
687
1238
|
| `CURRENT_MONTH_END` | `END_OF(CALENDAR_PERIOD("MONTH", 0))` |
|
|
@@ -702,6 +1253,7 @@ Legacy tokens are resolved with `TranspileJSONToDatePreset(token)` using `DATE_P
|
|
|
702
1253
|
| `LAST_QUARTER_END` | `END_OF(CALENDAR_PERIOD("QUARTER", -1))` |
|
|
703
1254
|
| `CURRENT_YEAR` | `CALENDAR_PERIOD("YEAR", 0)` |
|
|
704
1255
|
| `CURRENT_YEAR_START` | `START_OF(CALENDAR_PERIOD("YEAR", 0))` |
|
|
1256
|
+
| `CURRENT_YEAR_END` | `END_OF(CALENDAR_PERIOD("YEAR", 0))` |
|
|
705
1257
|
| `LAST_YEAR` | `CALENDAR_PERIOD("YEAR", -1)` |
|
|
706
1258
|
| `LAST_YEAR_START` | `START_OF(CALENDAR_PERIOD("YEAR", -1))` |
|
|
707
1259
|
| `LAST_YEAR_END` | `END_OF(CALENDAR_PERIOD("YEAR", -1))` |
|
|
@@ -710,18 +1262,20 @@ Legacy tokens are resolved with `TranspileJSONToDatePreset(token)` using `DATE_P
|
|
|
710
1262
|
|
|
711
1263
|
Tokens can be wrapped like `{{TODAY-7}}`. The normalizer removes braces and outer spaces.
|
|
712
1264
|
|
|
713
|
-
|
|
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.
|
|
714
1266
|
|
|
715
1267
|
## Main Equivalences With The Date Picker
|
|
716
1268
|
|
|
717
1269
|
| Picker expression | QDP expression | Note |
|
|
718
1270
|
| --------------------------- | ------------------------------------------------------------ | -------------------------------------------------- |
|
|
719
1271
|
| Today | `TODAY()` | Fixed date at the start of the current day |
|
|
720
|
-
| Yesterday | `
|
|
721
|
-
| Tomorrow | `
|
|
1272
|
+
| Yesterday | `OFFSET(TODAY(), -1, "DAY")` | Start of yesterday |
|
|
1273
|
+
| Tomorrow | `OFFSET(TODAY(), 1, "DAY")` | Start of tomorrow |
|
|
722
1274
|
| This month | `CALENDAR_PERIOD("MONTH", 0)` | Current calendar month |
|
|
723
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 |
|
|
724
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 |
|
|
725
1279
|
| April | `PARTIAL_DATE("ANY", 4)` | Recurring month |
|
|
726
1280
|
| April 2026 | `PERIOD_AT("MONTH", 4, 2026)` | Positional month in a year |
|
|
727
1281
|
| Apr 15 | `PARTIAL_DATE("ANY", 4, "ANY", "ANY", 15)` | Recurring month/day |
|
|
@@ -762,29 +1316,38 @@ DATE_RANGE(DATE("2026-07-31T00:00"), DATE("2026-07-01T00:00"));
|
|
|
762
1316
|
// INVALID_DATE_RANGE: end before start
|
|
763
1317
|
```
|
|
764
1318
|
|
|
765
|
-
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`.
|
|
766
1321
|
|
|
767
1322
|
## Relevant Files
|
|
768
1323
|
|
|
769
1324
|
| File | Responsibility |
|
|
770
1325
|
| --------------------------------------------- | ----------------------------------------------------------------------- |
|
|
771
|
-
| `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 |
|
|
772
1328
|
| `src/date-presets/date-preset-tokens.ts` | Legacy token map to QDP expressions |
|
|
773
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 |
|
|
774
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 |
|
|
775
1335
|
| `src/utils/timezone.ts` | Timezone definition normalization and offset helpers |
|
|
776
1336
|
| `src/functions/*.ts` | Individual QDP function definitions |
|
|
777
|
-
| `__tests__/unit/
|
|
778
|
-
| `__tests__/unit/
|
|
779
|
-
| `__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 |
|
|
780
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 |
|
|
781
1344
|
| `date-preset-qdp-examples.csv` | Generated examples with tokens and picker expressions |
|
|
782
1345
|
|
|
783
1346
|
## Current State And Design Notes
|
|
784
1347
|
|
|
785
1348
|
- QDP is limited to date preset functions in `ENGINE_FN_MAP[ENGINES.QDP]`.
|
|
786
|
-
- 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.
|
|
787
1350
|
- `partialDate` was added as a primitive to represent incomplete date picker selections.
|
|
788
1351
|
- Week compatibility with the date picker uses Sunday as the default start day and supports W54.
|
|
789
1352
|
- `PERIOD_AT` supports symbolic positions `FIRST` and `LAST` to reduce ambiguity in variable-length periods.
|
|
790
|
-
- 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`).
|