@qrvey/formula-lang 3.2.0-rc.1085 → 3.2.0-rc.1104

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.
@@ -38,18 +38,18 @@ Formats dates, ranges, and partial dates for UI.
38
38
  type DatePresetValue = string | DateRangeValue | PartialDateValue;
39
39
  ```
40
40
 
41
- `date` is represented as an ISO string:
41
+ By default, `date` is represented as a UTC ISO string:
42
42
 
43
43
  ```json
44
44
  "2026-07-22T00:00:00.000Z"
45
45
  ```
46
46
 
47
- `dateRange` is represented as an object with `start` and `end`:
47
+ By default, `dateRange` is represented as an object with UTC ISO `start` and `end` values:
48
48
 
49
49
  ```json
50
50
  {
51
- "start": "2026-07-01T00:00:00.000Z",
52
- "end": "2026-07-31T23:59:59.999Z"
51
+ "start": "2026-07-01T00:00:00.000Z",
52
+ "end": "2026-07-31T23:59:59.999Z"
53
53
  }
54
54
  ```
55
55
 
@@ -57,31 +57,31 @@ type DatePresetValue = string | DateRangeValue | PartialDateValue;
57
57
 
58
58
  ```json
59
59
  {
60
- "month": 4,
61
- "day": 15
60
+ "month": 4,
61
+ "day": 15
62
62
  }
63
63
  ```
64
64
 
65
65
  Supported fields for `partialDate`:
66
66
 
67
- | Field | Meaning | Range |
68
- | --- | --- | --- |
69
- | `year` | Specific year | Integer |
70
- | `month` | Month | 1-12 |
71
- | `quarter` | Quarter | 1-4 |
72
- | `week` | Week | 1-54 |
73
- | `day` | Day of month | 1-31 |
67
+ | Field | Meaning | Range |
68
+ | --------- | ------------- | ------- |
69
+ | `year` | Specific year | Integer |
70
+ | `month` | Month | 1-12 |
71
+ | `quarter` | Quarter | 1-4 |
72
+ | `week` | Week | 1-54 |
73
+ | `day` | Day of month | 1-31 |
74
74
 
75
75
  ## Value Types
76
76
 
77
77
  `TranspileDatePreset` returns `valueType` to describe the nature of the value.
78
78
 
79
- | Value type | Meaning | Examples |
80
- | --- | --- | --- |
81
- | `fixed` | Fixed value or materialized range | `DATE("2026-07-20")`, `DATE_RANGE(...)` |
82
- | `recurring` | Partial pattern that repeats | `PARTIAL_DATE("ANY", 4)`, `PARTIAL_DATE("ANY", 4, "ANY", "ANY", 15)` |
83
- | `relative` | Period relative to the calendar or positioned inside a period | `CALENDAR_PERIOD("MONTH", 0)`, `PERIOD_AT("MONTH", 4, 2026)` |
84
- | `rolling` | Moving window from an anchor | `RELATIVE_PERIOD(-29, "DAY")` |
79
+ | Value type | Meaning | Examples |
80
+ | ----------- | ------------------------------------------------------------- | -------------------------------------------------------------------- |
81
+ | `fixed` | Fixed value or materialized range | `DATE("2026-07-20")`, `DATE_RANGE(...)` |
82
+ | `recurring` | Partial pattern that repeats | `PARTIAL_DATE("ANY", 4)`, `PARTIAL_DATE("ANY", 4, "ANY", "ANY", 15)` |
83
+ | `relative` | Period relative to the calendar or positioned inside a period | `CALENDAR_PERIOD("MONTH", 0)`, `PERIOD_AT("MONTH", 4, 2026)` |
84
+ | `rolling` | Moving window from an anchor | `RELATIVE_PERIOD(-29, "DAY")` |
85
85
 
86
86
  Current note: `PERIOD_AT("MONTH", 4, 2026)` produces a concrete range, but it is classified as `relative` because the root expression is `PERIOD_AT`. If the business goal requires distinguishing `April 2026` as `fixed`, that would be a classification improvement, not a resolution change.
87
87
 
@@ -91,39 +91,75 @@ QDP functions can receive `FormulaContext` with date preset configuration:
91
91
 
92
92
  ```ts
93
93
  {
94
- timezone: {
95
- timeZone: 'America/Bogota'
96
- },
97
- datePreset: {
98
- calendar: 'gregorian',
99
- locale: 'en-US',
100
- fiscalYearStartMonth: 1,
101
- fiscalYearStartDay: 1,
102
- weekStartsOn: 0
103
- }
94
+ timezone: { timeZone: "America/Bogota" },
95
+ datePreset: {
96
+ calendar: "gregorian",
97
+ locale: "en-US",
98
+ fiscalYearStartMonth: 1,
99
+ fiscalYearStartDay: 1,
100
+ weekStartsOn: 0,
101
+ applyTimezoneToExpression: "default",
102
+ },
104
103
  }
105
104
  ```
106
105
 
107
106
  Current defaults:
108
107
 
109
- | Option | Default | Notes |
110
- | --- | --- | --- |
111
- | `timezone.offset` | `+00:00` | Supports `default`, `browser`, or custom offsets such as `UTC-5`, `-05:00`, `+5:30` |
112
- | `timezone.timeZone` | none | Optional IANA time zone identifier such as `America/Bogota`; when present it is used for calendar resolution |
113
- | `calendar` | `gregorian` | Also supports `corporate-fiscal`, `retail-4-4-5`, `retail-4-5-4` |
114
- | `locale` | `en-US` | Used by formatting options |
115
- | `fiscalYearStartMonth` | `1` | Clamped to 1-12 |
116
- | `fiscalYearStartDay` | `1` | Clamped to 1-31 and adjusted to the last valid day of the month |
117
- | `weekStartsOn` | `0` | Sunday. Accepts 0-6 or English weekday names |
108
+ | Option | Default | Notes |
109
+ | ----------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------ |
110
+ | `timezone.offset` | `+00:00` | Supports `default`, `browser`, or custom offsets such as `UTC-5`, `-05:00`, `+5:30` |
111
+ | `timezone.timeZone` | none | Optional IANA time zone identifier such as `America/Bogota`; when present it is used for calendar resolution |
112
+ | `calendar` | `gregorian` | Also supports `corporate-fiscal`, `retail-4-4-5`, `retail-4-5-4` |
113
+ | `locale` | `en-US` | Used by formatting options |
114
+ | `fiscalYearStartMonth` | `1` | Clamped to 1-12 |
115
+ | `fiscalYearStartDay` | `1` | Clamped to 1-31 and adjusted to the last valid day of the month |
116
+ | `weekStartsOn` | `0` | Sunday. Accepts 0-6 or English weekday names |
117
+ | `applyTimezoneToExpression` | `default` | Controls the timezone format used in the returned public `expression` value |
118
118
 
119
- 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' }`.
119
+ 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
+
121
+ ### Expression timezone output
122
+
123
+ Date preset resolution always calculates concrete instants internally. The `datePreset.applyTimezoneToExpression` option controls only the public `expression` format returned by `TranspileDatePreset`.
124
+
125
+ Supported values:
126
+
127
+ | Value | Output behavior |
128
+ | ---------- | ----------------------------------------------------------------------------------------------------- |
129
+ | `default` | Returns the resolved expression in UTC, preserving the existing `Z` ISO output |
130
+ | `timezone` | Converts the resolved expression to the configured timezone and appends the timezone offset |
131
+ | `local` | Converts the resolved expression to the configured timezone and removes the trailing timezone metadata |
132
+
133
+ Example using a fixed offset:
134
+
135
+ ```ts
136
+ TranspileDatePreset('TODAY()', {
137
+ timezone: { offset: '+13:00' },
138
+ datePreset: { applyTimezoneToExpression: 'default' },
139
+ });
140
+ // expression: "2026-07-31T11:00:00.000Z"
141
+
142
+ TranspileDatePreset('TODAY()', {
143
+ timezone: { offset: '+13:00' },
144
+ datePreset: { applyTimezoneToExpression: 'timezone' },
145
+ });
146
+ // expression: "2026-08-01T00:00:00.000+13:00"
147
+
148
+ TranspileDatePreset('TODAY()', {
149
+ timezone: { offset: '+13:00' },
150
+ datePreset: { applyTimezoneToExpression: 'local' },
151
+ });
152
+ // expression: "2026-08-01T00:00:00.000"
153
+ ```
154
+
155
+ For `dateRange` values, the same mode is applied to both `start` and `end`. `partialDate` values are not transformed because they do not represent a concrete instant.
120
156
 
121
157
  Current week convention:
122
158
 
123
- - The default is Sunday (`weekStartsOn: 0`).
124
- - Week 1 of a year starts on the Sunday on or before Jan 1.
125
- - Some years can have W54 under this convention.
126
- - `PERIOD_AT("WEEK", "LAST", year)` is preferred for representing the last week of the year because it avoids hardcoding 52, 53, or 54.
159
+ - The default is Sunday (`weekStartsOn: 0`).
160
+ - Week 1 of a year starts on the Sunday on or before Jan 1.
161
+ - Some years can have W54 under this convention.
162
+ - `PERIOD_AT("WEEK", "LAST", year)` is preferred for representing the last week of the year because it avoids hardcoding 52, 53, or 54.
127
163
 
128
164
  ## QDP Date Preset Functions
129
165
 
@@ -132,7 +168,7 @@ Current week convention:
132
168
  Returns the current timestamp.
133
169
 
134
170
  ```ts
135
- NOW()
171
+ NOW();
136
172
  // 2026-07-22T23:42:50.413Z
137
173
  ```
138
174
 
@@ -145,7 +181,7 @@ Value type when used as the final result: `fixed`.
145
181
  Returns the start of the current day in the context timezone.
146
182
 
147
183
  ```ts
148
- TODAY()
184
+ TODAY();
149
185
  // 2026-07-22T00:00:00.000Z
150
186
  ```
151
187
 
@@ -158,30 +194,30 @@ Value type when used as the final result: `fixed`.
158
194
  Normalizes an ISO date or ISO date-time string to UTC ISO.
159
195
 
160
196
  ```ts
161
- DATE("2026-07-20")
197
+ DATE("2026-07-20");
162
198
  // 2026-07-20T00:00:00.000Z
163
199
  ```
164
200
 
165
201
  Supported input formats:
166
202
 
167
- | Input | Example | Resolved value |
168
- | --- | --- | --- |
169
- | ISO date | `DATE("2026-07-08")` | `2026-07-08T00:00:00.000Z` |
170
- | ISO date-time without timezone | `DATE("2026-07-08T15:30")` | `2026-07-08T15:30:00.000Z` |
171
- | ISO date-time with UTC timezone | `DATE("2026-07-08T15:30:45Z")` | `2026-07-08T15:30:45.000Z` |
203
+ | Input | Example | Resolved value |
204
+ | ---------------------------------- | ----------------------------------- | -------------------------- |
205
+ | ISO date | `DATE("2026-07-08")` | `2026-07-08T00:00:00.000Z` |
206
+ | ISO date-time without timezone | `DATE("2026-07-08T15:30")` | `2026-07-08T15:30:00.000Z` |
207
+ | ISO date-time with UTC timezone | `DATE("2026-07-08T15:30:45Z")` | `2026-07-08T15:30:45.000Z` |
172
208
  | ISO date-time with offset timezone | `DATE("2026-07-08T15:30:45-05:00")` | `2026-07-08T20:30:45.000Z` |
173
209
 
174
210
  Parameters:
175
211
 
176
- | Parameter | Type | Required | Validation |
177
- | --- | --- | --- | --- |
178
- | `VALUE` | `string` | Yes | ISO date or ISO date-time |
212
+ | Parameter | Type | Required | Validation |
213
+ | --------- | -------- | -------- | ------------------------- |
214
+ | `VALUE` | `string` | Yes | ISO date or ISO date-time |
179
215
 
180
216
  Rules:
181
217
 
182
- - `DATE("2026-07-08")` is valid and resolves to `2026-07-08T00:00:00.000Z`.
183
- - Calendrically invalid dates, such as `DATE("2026-02-31T00:00")`, are rejected.
184
- - If the string does not include a timezone, UTC is assumed.
218
+ - `DATE("2026-07-08")` is valid and resolves to `2026-07-08T00:00:00.000Z`.
219
+ - Calendrically invalid dates, such as `DATE("2026-02-31T00:00")`, are rejected.
220
+ - If the string does not include a timezone, UTC is assumed.
185
221
 
186
222
  Output: `date`.
187
223
 
@@ -192,24 +228,21 @@ Value type: `fixed`.
192
228
  Builds a range between two dates.
193
229
 
194
230
  ```ts
195
- DATE_RANGE(
196
- DATE("2026-07-20"),
197
- END_OF(DATE("2026-07-25"))
198
- )
231
+ DATE_RANGE(DATE("2026-07-20"), END_OF(DATE("2026-07-25")));
199
232
  // { start: "2026-07-20T00:00:00.000Z", end: "2026-07-25T23:59:59.999Z" }
200
233
  ```
201
234
 
202
235
  Parameters:
203
236
 
204
- | Parameter | Type | Required |
205
- | --- | --- | --- |
206
- | `START` | `date` | Yes |
207
- | `END` | `date` | Yes |
237
+ | Parameter | Type | Required |
238
+ | --------- | ------ | -------- |
239
+ | `START` | `date` | Yes |
240
+ | `END` | `date` | Yes |
208
241
 
209
242
  Rules:
210
243
 
211
- - `END` must be greater than or equal to `START`.
212
- - If `END < START`, the function returns `INVALID_DATE_RANGE`.
244
+ - `END` must be greater than or equal to `START`.
245
+ - If `END < START`, the function returns `INVALID_DATE_RANGE`.
213
246
 
214
247
  Output: `dateRange`.
215
248
 
@@ -220,28 +253,28 @@ Value type: `fixed`.
220
253
  Creates a moving window relative to an anchor. If no anchor is passed, it uses `TODAY()`.
221
254
 
222
255
  ```ts
223
- RELATIVE_PERIOD(-29, "DAY")
256
+ RELATIVE_PERIOD(-29, "DAY");
224
257
  // { start: "2026-06-23T00:00:00.000Z", end: "2026-07-22T23:59:59.999Z" }
225
258
  ```
226
259
 
227
260
  Parameters:
228
261
 
229
- | Parameter | Type | Required | Values |
230
- | --- | --- | --- | --- |
231
- | `OFFSET` | Integer `number` | Yes | Negative, zero, or positive |
232
- | `UNIT` | `string` | Yes | `DAY`, `WEEK`, `MONTH`, `QUARTER`, `YEAR` |
233
- | `ANCHOR` | `date` | No | Base date |
262
+ | Parameter | Type | Required | Values |
263
+ | --------- | ---------------- | -------- | ----------------------------------------- |
264
+ | `OFFSET` | Integer `number` | Yes | Negative, zero, or positive |
265
+ | `UNIT` | `string` | Yes | `DAY`, `WEEK`, `MONTH`, `QUARTER`, `YEAR` |
266
+ | `ANCHOR` | `date` | No | Base date |
234
267
 
235
268
  Semantics:
236
269
 
237
- - Negative offset: from `anchor + offset` to the end of the anchor day.
238
- - Positive offset: from the start of the anchor day to `anchor + offset`.
239
- - It is rolling and not necessarily aligned to calendar boundaries.
270
+ - Negative offset: from `anchor + offset` to the end of the anchor day.
271
+ - Positive offset: from the start of the anchor day to `anchor + offset`.
272
+ - It is rolling and not necessarily aligned to calendar boundaries.
240
273
 
241
274
  Examples:
242
275
 
243
276
  ```ts
244
- RELATIVE_PERIOD(-2, "WEEK", DATE("2026-07-08T15:30"))
277
+ RELATIVE_PERIOD(-2, "WEEK", DATE("2026-07-08T15:30"));
245
278
  // 2026-06-24T00:00:00.000Z -> 2026-07-08T23:59:59.999Z
246
279
  ```
247
280
 
@@ -254,31 +287,31 @@ Value type: `rolling`.
254
287
  Returns a complete calendar period containing the current day, shifted by `offset`.
255
288
 
256
289
  ```ts
257
- CALENDAR_PERIOD("MONTH", 0)
290
+ CALENDAR_PERIOD("MONTH", 0);
258
291
  // 2026-07-01T00:00:00.000Z -> 2026-07-31T23:59:59.999Z
259
292
  ```
260
293
 
261
294
  Parameters:
262
295
 
263
- | Parameter | Type | Required | Values |
264
- | --- | --- | --- | --- |
265
- | `PERIOD` | `string` | Yes | `DAY`, `WEEK`, `MONTH`, `QUARTER`, `YEAR` |
266
- | `OFFSET` | Integer `number` | No | Default `0` |
296
+ | Parameter | Type | Required | Values |
297
+ | --------- | ---------------- | -------- | ----------------------------------------- |
298
+ | `PERIOD` | `string` | Yes | `DAY`, `WEEK`, `MONTH`, `QUARTER`, `YEAR` |
299
+ | `OFFSET` | Integer `number` | No | Default `0` |
267
300
 
268
301
  Semantics:
269
302
 
270
- - `0` means the current period.
271
- - `-1` means the previous period.
272
- - `1` means the next period.
273
- - Respects calendar boundaries, timezone, fiscal calendar, and week start from the context.
303
+ - `0` means the current period.
304
+ - `-1` means the previous period.
305
+ - `1` means the next period.
306
+ - Respects calendar boundaries, timezone, fiscal calendar, and week start from the context.
274
307
 
275
308
  Examples:
276
309
 
277
310
  ```ts
278
- CALENDAR_PERIOD("WEEK", 0)
311
+ CALENDAR_PERIOD("WEEK", 0);
279
312
  // with weekStartsOn Sunday: 2026-07-19T00:00:00.000Z -> 2026-07-25T23:59:59.999Z
280
313
 
281
- CALENDAR_PERIOD("QUARTER", -1)
314
+ CALENDAR_PERIOD("QUARTER", -1);
282
315
  // 2026-04-01T00:00:00.000Z -> 2026-06-30T23:59:59.999Z
283
316
  ```
284
317
 
@@ -291,37 +324,37 @@ Value type: `relative`.
291
324
  Returns the period located at a position inside a year. If `year` is not passed, it uses the current year from the context.
292
325
 
293
326
  ```ts
294
- PERIOD_AT("MONTH", 4, 2026)
327
+ PERIOD_AT("MONTH", 4, 2026);
295
328
  // 2026-04-01T00:00:00.000Z -> 2026-04-30T23:59:59.999Z
296
329
  ```
297
330
 
298
331
  Parameters:
299
332
 
300
- | Parameter | Type | Required | Values |
301
- | --- | --- | --- | --- |
302
- | `PERIOD` | `string` | Yes | `DAY`, `WEEK`, `MONTH`, `QUARTER` |
303
- | `POSITION` | Integer `number` or `string` | Yes | Number, `FIRST`, `LAST` |
304
- | `YEAR` | Integer `number` | No | Default: current year |
333
+ | Parameter | Type | Required | Values |
334
+ | ---------- | ---------------------------- | -------- | --------------------------------- |
335
+ | `PERIOD` | `string` | Yes | `DAY`, `WEEK`, `MONTH`, `QUARTER` |
336
+ | `POSITION` | Integer `number` or `string` | Yes | Number, `FIRST`, `LAST` |
337
+ | `YEAR` | Integer `number` | No | Default: current year |
305
338
 
306
339
  Examples:
307
340
 
308
341
  ```ts
309
- PERIOD_AT("QUARTER", 2, 2026)
342
+ PERIOD_AT("QUARTER", 2, 2026);
310
343
  // 2026-04-01T00:00:00.000Z -> 2026-06-30T23:59:59.999Z
311
344
 
312
- PERIOD_AT("MONTH", "LAST", 2025)
345
+ PERIOD_AT("MONTH", "LAST", 2025);
313
346
  // 2025-12-01T00:00:00.000Z -> 2025-12-31T23:59:59.999Z
314
347
 
315
- PERIOD_AT("WEEK", "LAST", 2028)
348
+ PERIOD_AT("WEEK", "LAST", 2028);
316
349
  // 2028-12-31T00:00:00.000Z -> 2029-01-06T23:59:59.999Z
317
350
  ```
318
351
 
319
352
  Week rules:
320
353
 
321
- - For `WEEK`, the start of the year is anchored to the start of the week that falls on or before the start of the year.
322
- - The last week can cross into the next year.
323
- - Numeric `POSITION` does not force an error if it points to a computable range outside the nominal year. For example, W54 can resolve by shifting even when the year does not nominally have W54.
324
- - `"MIDDLE"` and other strings different from `FIRST` or `LAST` are invalid.
354
+ - For `WEEK`, the start of the year is anchored to the start of the week that falls on or before the start of the year.
355
+ - The last week can cross into the next year.
356
+ - Numeric `POSITION` does not force an error if it points to a computable range outside the nominal year. For example, W54 can resolve by shifting even when the year does not nominally have W54.
357
+ - `"MIDDLE"` and other strings different from `FIRST` or `LAST` are invalid.
325
358
 
326
359
  Output: `dateRange`.
327
360
 
@@ -332,71 +365,71 @@ Current value type: `relative`.
332
365
  Represents a partial or recurring date. Each field can be a number or `"ANY"`. Omitted fields are treated as `ANY`.
333
366
 
334
367
  ```ts
335
- PARTIAL_DATE("ANY", 4, "ANY", "ANY", 15)
368
+ PARTIAL_DATE("ANY", 4, "ANY", "ANY", 15);
336
369
  // { month: 4, day: 15 }
337
370
  ```
338
371
 
339
372
  Parameters:
340
373
 
341
- | Position | Field | Range | Example |
342
- | --- | --- | --- | --- |
343
- | 1 | `YEAR` | Integer or `ANY` | `2026` |
344
- | 2 | `MONTH` | 1-12 or `ANY` | `4` |
345
- | 3 | `QUARTER` | 1-4 or `ANY` | `2` |
346
- | 4 | `WEEK` | 1-54 or `ANY` | `40` |
347
- | 5 | `DAY` | 1-31 or `ANY` | `15` |
374
+ | Position | Field | Range | Example |
375
+ | -------- | --------- | ---------------- | ------- |
376
+ | 1 | `YEAR` | Integer or `ANY` | `2026` |
377
+ | 2 | `MONTH` | 1-12 or `ANY` | `4` |
378
+ | 3 | `QUARTER` | 1-4 or `ANY` | `2` |
379
+ | 4 | `WEEK` | 1-54 or `ANY` | `40` |
380
+ | 5 | `DAY` | 1-31 or `ANY` | `15` |
348
381
 
349
382
  Date picker examples:
350
383
 
351
384
  ```ts
352
- PARTIAL_DATE()
385
+ PARTIAL_DATE();
353
386
  // {}
354
387
  // Short format: Any date
355
388
 
356
- PARTIAL_DATE("ANY", 4)
389
+ PARTIAL_DATE("ANY", 4);
357
390
  // { month: 4 }
358
391
  // Short format: Apr
359
392
 
360
- PARTIAL_DATE("ANY", 4, "ANY", "ANY", 15)
393
+ PARTIAL_DATE("ANY", 4, "ANY", "ANY", 15);
361
394
  // { month: 4, day: 15 }
362
395
  // Short format: Apr 15
363
396
 
364
- PARTIAL_DATE("ANY", "ANY", 2)
397
+ PARTIAL_DATE("ANY", "ANY", 2);
365
398
  // { quarter: 2 }
366
399
  // Short format: Q2
367
400
 
368
- PARTIAL_DATE("ANY", "ANY", "ANY", 40)
401
+ PARTIAL_DATE("ANY", "ANY", "ANY", 40);
369
402
  // { week: 40 }
370
403
  // Short format: W40
371
404
 
372
- PARTIAL_DATE("ANY", "ANY", "ANY", "ANY", 15)
405
+ PARTIAL_DATE("ANY", "ANY", "ANY", "ANY", 15);
373
406
  // { day: 15 }
374
407
  // Short format: Day 15
375
408
  ```
376
409
 
377
410
  Value type:
378
411
 
379
- - `fixed` if it includes `year`, `month`, and `day`, without `week`.
380
- - `relative` if it includes `week`.
381
- - `recurring` for all other partial dates.
412
+ - `fixed` if it includes `year`, `month`, and `day`, without `week`.
413
+ - `relative` if it includes `week`.
414
+ - `recurring` for all other partial dates.
382
415
 
383
416
  ### `START_OF(value)`
384
417
 
385
418
  Returns the start of a `date` or the `start` of a `dateRange`.
386
419
 
387
420
  ```ts
388
- START_OF(DATE("2026-07-08T15:30"))
421
+ START_OF(DATE("2026-07-08T15:30"));
389
422
  // 2026-07-08T00:00:00.000Z
390
423
 
391
- START_OF(CALENDAR_PERIOD("MONTH", -1))
424
+ START_OF(CALENDAR_PERIOD("MONTH", -1));
392
425
  // 2026-06-01T00:00:00.000Z
393
426
  ```
394
427
 
395
428
  Parameter:
396
429
 
397
- | Parameter | Type |
398
- | --- | --- |
399
- | `VALUE` | `date` or `dateRange` |
430
+ | Parameter | Type |
431
+ | --------- | --------------------- |
432
+ | `VALUE` | `date` or `dateRange` |
400
433
 
401
434
  Output: `date`.
402
435
 
@@ -407,18 +440,18 @@ Value type when used as root: `relative`.
407
440
  Returns the end of a `date` or the `end` of a `dateRange`.
408
441
 
409
442
  ```ts
410
- END_OF(DATE("2026-07-08T15:30"))
443
+ END_OF(DATE("2026-07-08T15:30"));
411
444
  // 2026-07-08T23:59:59.999Z
412
445
 
413
- END_OF(CALENDAR_PERIOD("YEAR", -1))
446
+ END_OF(CALENDAR_PERIOD("YEAR", -1));
414
447
  // 2025-12-31T23:59:59.999Z
415
448
  ```
416
449
 
417
450
  Parameter:
418
451
 
419
- | Parameter | Type |
420
- | --- | --- |
421
- | `VALUE` | `date` or `dateRange` |
452
+ | Parameter | Type |
453
+ | --------- | --------------------- |
454
+ | `VALUE` | `date` or `dateRange` |
422
455
 
423
456
  Output: `date`.
424
457
 
@@ -429,55 +462,55 @@ Value type when used as root: `relative`.
429
462
  Extracts one part of a date.
430
463
 
431
464
  ```ts
432
- PART(DATE("2026-07-08T15:30"), "DAY_OF_WEEK_NAME")
465
+ PART(DATE("2026-07-08T15:30"), "DAY_OF_WEEK_NAME");
433
466
  // Wednesday
434
467
  ```
435
468
 
436
469
  Parameters:
437
470
 
438
- | Parameter | Type | Values |
439
- | --- | --- | --- |
440
- | `DATE` | `date` | Resolved ISO date |
441
- | `PART` | `string` | See the table below |
471
+ | Parameter | Type | Values |
472
+ | --------- | -------- | ------------------- |
473
+ | `DATE` | `date` | Resolved ISO date |
474
+ | `PART` | `string` | See the table below |
442
475
 
443
476
  Supported parts:
444
477
 
445
- | Value | Output |
446
- | --- | --- |
447
- | `YEAR` | UTC year |
448
- | `QUARTER` | Quarter 1-4 |
449
- | `MONTH` | Month 1-12 |
450
- | `MONTH_NAME` | English month name |
451
- | `DAY` | Day of month |
452
- | `DAY_OF_MONTH` | Day of month |
453
- | `DAY_OF_WEEK` | UTC weekday, Sunday = 0 |
454
- | `DAY_OF_WEEK_NAME` | English weekday name |
455
- | `DAY_OF_YEAR` | Day of year |
456
- | `WEEK_OF_YEAR` | Week according to the current convention |
457
- | `HOUR` | UTC hour |
458
- | `MINUTE` | UTC minute |
459
- | `SECOND` | UTC second |
478
+ | Value | Output |
479
+ | ------------------ | ---------------------------------------- |
480
+ | `YEAR` | UTC year |
481
+ | `QUARTER` | Quarter 1-4 |
482
+ | `MONTH` | Month 1-12 |
483
+ | `MONTH_NAME` | English month name |
484
+ | `DAY` | Day of month |
485
+ | `DAY_OF_MONTH` | Day of month |
486
+ | `DAY_OF_WEEK` | UTC weekday, Sunday = 0 |
487
+ | `DAY_OF_WEEK_NAME` | English weekday name |
488
+ | `DAY_OF_YEAR` | Day of year |
489
+ | `WEEK_OF_YEAR` | Week according to the current convention |
490
+ | `HOUR` | UTC hour |
491
+ | `MINUTE` | UTC minute |
492
+ | `SECOND` | UTC second |
460
493
 
461
494
  Important: `PART` is available as a QDP function, but it is not a valid final result for `TranspileDatePreset` because it returns `string`. It can be used as a helper function when accessing the QDP transpiler directly, but a final date preset expression must return `date`, `dateRange`, or `partialDate`.
462
495
 
463
496
  ## Differences Between Period Functions
464
497
 
465
- | Function | Question it answers | Example | Type |
466
- | --- | --- | --- | --- |
467
- | `CALENDAR_PERIOD` | What is this/previous/next calendar period? | This month, last week | `relative` |
468
- | `PERIOD_AT` | What is period N inside a year? | April 2026, Q2 2026, W40 2026 | Current `relative` |
469
- | `RELATIVE_PERIOD` | What is the moving window from today/anchor? | Last 30 days | `rolling` |
498
+ | Function | Question it answers | Example | Type |
499
+ | ----------------- | -------------------------------------------- | ----------------------------- | ------------------ |
500
+ | `CALENDAR_PERIOD` | What is this/previous/next calendar period? | This month, last week | `relative` |
501
+ | `PERIOD_AT` | What is period N inside a year? | April 2026, Q2 2026, W40 2026 | Current `relative` |
502
+ | `RELATIVE_PERIOD` | What is the moving window from today/anchor? | Last 30 days | `rolling` |
470
503
 
471
504
  Examples with current date `2026-07-22`:
472
505
 
473
506
  ```ts
474
- CALENDAR_PERIOD("MONTH", -1)
507
+ CALENDAR_PERIOD("MONTH", -1);
475
508
  // previous calendar month: Jun 1 -> Jun 30
476
509
 
477
- PERIOD_AT("MONTH", 4, 2026)
510
+ PERIOD_AT("MONTH", 4, 2026);
478
511
  // fourth month of 2026: Apr 1 -> Apr 30
479
512
 
480
- RELATIVE_PERIOD(-29, "DAY")
513
+ RELATIVE_PERIOD(-29, "DAY");
481
514
  // last 30 days inclusive: Jun 23 -> Jul 22
482
515
  ```
483
516
 
@@ -490,27 +523,27 @@ It is not recommended to merge them into a single public function because they e
490
523
  ### Expression Nodes
491
524
 
492
525
  ```ts
493
- { type: 'NOW' }
494
- { type: 'TODAY' }
495
- { type: 'DATE', value: '2026-07-20' }
496
- { type: 'DATE_RANGE', start: DatePresetJSON, end: DatePresetJSON }
497
- { type: 'PARTIAL_DATE', value: { year?, quarter?, month?, week?, day? } }
498
- { type: 'RELATIVE_PERIOD', offset: -30, unit: 'DAY', anchor?: DatePresetJSON }
499
- { type: 'CALENDAR_PERIOD', period: 'MONTH', offset?: 0 }
500
- { type: 'PERIOD_AT', period: 'MONTH', position: 4 | 'FIRST' | 'LAST', year?: 2026 }
501
- { type: 'START_OF', input: DatePresetJSON }
502
- { type: 'END_OF', input: DatePresetJSON }
526
+ { type: "NOW" }
527
+ { type: "TODAY" }
528
+ { type: "DATE", value: "2026-07-20" }
529
+ { type: "DATE_RANGE", start: DatePresetJSON, end: DatePresetJSON }
530
+ { type: "PARTIAL_DATE", value: { year?, quarter?, month?, week?, day? } }
531
+ { type: "RELATIVE_PERIOD", offset: -30, unit: "DAY", anchor?: DatePresetJSON }
532
+ { type: "CALENDAR_PERIOD", period: "MONTH", offset?: 0 }
533
+ { type: "PERIOD_AT", period: "MONTH", position: 4 | "FIRST" | "LAST", year?: 2026 }
534
+ { type: "START_OF", input: DatePresetJSON }
535
+ { type: "END_OF", input: DatePresetJSON }
503
536
  ```
504
537
 
505
538
  Example:
506
539
 
507
540
  ```ts
508
541
  TranspileJSONToDatePreset({
509
- type: 'PERIOD_AT',
510
- period: 'WEEK',
511
- position: 'LAST',
512
- year: 2028,
513
- })
542
+ type: "PERIOD_AT",
543
+ period: "WEEK",
544
+ position: "LAST",
545
+ year: 2028,
546
+ });
514
547
  // PERIOD_AT("WEEK", "LAST", 2028)
515
548
  ```
516
549
 
@@ -519,126 +552,161 @@ TranspileJSONToDatePreset({
519
552
  Date picker oriented shapes are also supported:
520
553
 
521
554
  ```ts
522
- { type: 'SINGLE_PERIOD', date: { year: 2026, month: 7 } }
555
+ { type: "SINGLE_PERIOD", date: { year: 2026, month: 7 } }
523
556
  // DATE_RANGE(DATE("2026-07-01"), END_OF(DATE("2026-07-31")))
524
557
 
525
- { type: 'SINGLE_PERIOD', period: 'month', offset: -1 }
558
+ { type: "SINGLE_PERIOD", period: "month", offset: -1 }
526
559
  // CALENDAR_PERIOD("MONTH", -1)
527
560
 
528
- { type: 'CUSTOM_RANGE', from: { year: 2026, month: 5, day: 31 }, to: { year: 2026, month: 6, day: 30 } }
561
+ { type: "CUSTOM_RANGE", from: { year: 2026, month: 5, day: 31 }, to: { year: 2026, month: 6, day: 30 } }
529
562
  // DATE_RANGE(DATE("2026-05-31"), END_OF(DATE("2026-06-30")))
530
563
 
531
- { type: 'ROLLING_WINDOW', direction: 'last', amount: 30, unit: 'days' }
564
+ { type: "ROLLING_WINDOW", direction: "last", amount: 30, unit: "days" }
532
565
  // RELATIVE_PERIOD(-30, "DAY")
533
566
  ```
534
567
 
535
568
  Rules:
536
569
 
537
- - `SINGLE_PERIOD` with `date` requires `year`.
538
- - `CUSTOM_RANGE` materializes dates with start-of-day and end-of-day boundaries.
539
- - `ROLLING_WINDOW` normalizes plural units by removing the final `s`.
540
- - `direction: 'NEXT'` produces a positive offset; any other value behaves as `LAST`.
570
+ - `SINGLE_PERIOD` with `date` requires `year`.
571
+ - `CUSTOM_RANGE` materializes dates with start-of-day and end-of-day boundaries.
572
+ - `ROLLING_WINDOW` normalizes plural units by removing the final `s`.
573
+ - `direction: "NEXT"` produces a positive offset; any other value behaves as `LAST`.
541
574
 
542
575
  ## Formatting
543
576
 
544
577
  `FormatDatePreset` formats values using `Intl.DateTimeFormat`.
545
578
 
546
- 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.
579
+ 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.
580
+
581
+ `dateStyle: "short-padded"` is a Formula Lang formatting extension. It keeps the date order and separators from the selected locale, but renders year, month, and day with two digits. For example, `en-US` renders `07/30/26`, while `es-CO` renders `30/07/26`.
582
+
583
+ `customFormat` has precedence over `dateStyle`, `timeStyle`, and locale date ordering. It is intended for explicit UI contracts that require a fixed token pattern. Supported tokens are `yyyy`, `yy`, `MM`, `M`, `dd`, `d`, `HH`, `H`, `mm`, `m`, `ss`, and `s`.
547
584
 
548
585
  Main options:
549
586
 
550
587
  ```ts
551
588
  {
552
- locale?: 'en-US',
553
- timezone?: {
554
- offset?: string,
555
- timeZone?: string,
556
- name?: string,
557
- type?: string,
558
- timeZoneId?: string,
559
- numericFormat?: number
560
- },
561
- dateStyle?: 'full' | 'long' | 'medium' | 'short',
562
- timeStyle?: 'full' | 'long' | 'medium' | 'short',
563
- partialDateStyle?: 'long' | 'short'
589
+ locale?: "en-US",
590
+ timezone?: { offset?: string; timeZone?: string; name?: string; type?: string; timeZoneId?: string; numericFormat?: number },
591
+ customFormat?: string,
592
+ dateStyle?: "full" | "long" | "medium" | "short" | "short-padded",
593
+ timeStyle?: "full" | "long" | "medium" | "short",
594
+ partialDateStyle?: "long" | "short"
564
595
  }
565
596
  ```
566
597
 
567
598
  Examples:
568
599
 
569
600
  ```ts
570
- FormatDatePreset('2026-07-15T14:35:27.000Z', {
571
- locale: 'en-US',
572
- timezone: { timeZone: 'America/Chicago' },
573
- dateStyle: 'medium',
574
- timeStyle: 'short',
575
- })
601
+ FormatDatePreset("2026-07-15T14:35:27.000Z", {
602
+ locale: "en-US",
603
+ timezone: { timeZone: "America/Chicago" },
604
+ dateStyle: "medium",
605
+ timeStyle: "short",
606
+ });
576
607
  // Jul 15, 2026, 9:35 AM
577
608
 
578
- FormatDatePreset('2026-07-15T14:35:27.000Z', { partialDateStyle: 'short' })
609
+ FormatDatePreset("2026-07-15T14:35:27.000Z", { partialDateStyle: "short" });
579
610
  // Jul 15, 2026
580
611
 
612
+ FormatDatePreset("2026-07-30T14:35:27.000Z", {
613
+ locale: "en-US",
614
+ dateStyle: "short-padded",
615
+ partialDateStyle: "short",
616
+ });
617
+ // 07/30/26
618
+
619
+ FormatDatePreset("2026-07-30T14:35:27.000Z", {
620
+ locale: "es-CO",
621
+ dateStyle: "short-padded",
622
+ partialDateStyle: "short",
623
+ });
624
+ // 30/07/26
625
+
626
+ FormatDatePreset("2026-07-30T14:35:27.000Z", {
627
+ locale: "es-CO",
628
+ customFormat: "MM/dd/yy",
629
+ });
630
+ // 07/30/26
631
+
581
632
  FormatDatePreset(
582
- { start: '2026-07-01T00:00:00.000Z', end: '2026-07-31T23:59:59.999Z' },
583
- { partialDateStyle: 'short' },
584
- )
585
- // { start: 'Jul 1, 2026', end: 'Jul 31, 2026' }
633
+ { start: "2026-07-01T00:00:00.000Z", end: "2026-07-31T23:59:59.999Z" },
634
+ { partialDateStyle: "short" },
635
+ );
636
+ // { start: "Jul 1, 2026", end: "Jul 31, 2026" }
586
637
 
587
- FormatDatePreset({ month: 4, day: 15 })
638
+ FormatDatePreset(
639
+ { start: "2026-07-01T05:00:00.000Z", end: "2026-08-01T04:59:59.999Z" },
640
+ {
641
+ locale: "en-US",
642
+ timezone: { timeZone: "America/Chicago" },
643
+ dateStyle: "short-padded",
644
+ partialDateStyle: "short",
645
+ },
646
+ );
647
+ // { start: "07/01/26", end: "07/31/26" }
648
+
649
+ FormatDatePreset({ month: 4, day: 15 });
588
650
  // April 15
589
651
 
590
- FormatDatePreset({ month: 4, day: 15 }, { partialDateStyle: 'short' })
652
+ FormatDatePreset({ month: 4, day: 15 }, { partialDateStyle: "short" });
591
653
  // Apr 15
592
654
 
593
- FormatDatePreset({ week: 40 }, { partialDateStyle: 'short' })
655
+ FormatDatePreset({ week: 40 }, { partialDateStyle: "short" });
594
656
  // W40
595
657
 
596
- FormatDatePreset({ day: 15 }, { partialDateStyle: 'short' })
658
+ FormatDatePreset({ day: 15 }, { partialDateStyle: "short" });
597
659
  // Day 15
660
+
661
+ FormatDatePreset(
662
+ { year: 2026, month: 7, day: 3 },
663
+ { locale: "en-US", dateStyle: "short-padded" },
664
+ );
665
+ // 07/03/26
598
666
  ```
599
667
 
600
668
  ## Legacy Tokens
601
669
 
602
670
  Legacy tokens are resolved with `TranspileJSONToDatePreset(token)` using `DATE_PRESET_TOKEN_MAP`.
603
671
 
604
- | Token | QDP expression |
605
- | --- | --- |
606
- | `NOW` | `NOW()` |
607
- | `TODAY` | `TODAY()` |
608
- | `CURRENT_DATE` | `TODAY()` |
609
- | `TODAY-7` | `START_OF(RELATIVE_PERIOD(-7, "DAY"))` |
610
- | `TODAY-30` | `START_OF(RELATIVE_PERIOD(-30, "DAY"))` |
611
- | `TODAY-60` | `START_OF(RELATIVE_PERIOD(-60, "DAY"))` |
612
- | `TODAY-90` | `START_OF(RELATIVE_PERIOD(-90, "DAY"))` |
613
- | `TODAY-120` | `START_OF(RELATIVE_PERIOD(-120, "DAY"))` |
614
- | `TODAY-365` | `START_OF(RELATIVE_PERIOD(-365, "DAY"))` |
615
- | `YESTERDAY` | `START_OF(RELATIVE_PERIOD(-1, "DAY"))` |
616
- | `TOMORROW` | `START_OF(END_OF(RELATIVE_PERIOD(1, "DAY")))` |
617
- | `CURRENT_MONTH` | `CALENDAR_PERIOD("MONTH", 0)` |
618
- | `CURRENT_MONTH_START` | `START_OF(CALENDAR_PERIOD("MONTH", 0))` |
619
- | `CURRENT_MONTH_END` | `END_OF(CALENDAR_PERIOD("MONTH", 0))` |
620
- | `LAST_MONTH` | `CALENDAR_PERIOD("MONTH", -1)` |
621
- | `LAST_MONTH_START` | `START_OF(CALENDAR_PERIOD("MONTH", -1))` |
622
- | `LAST_MONTH_END` | `END_OF(CALENDAR_PERIOD("MONTH", -1))` |
623
- | `CURRENT_WEEK` | `CALENDAR_PERIOD("WEEK", 0)` |
624
- | `CURRENT_WEEK_START` | `START_OF(CALENDAR_PERIOD("WEEK", 0))` |
625
- | `CURRENT_WEEK_END` | `END_OF(CALENDAR_PERIOD("WEEK", 0))` |
626
- | `LAST_WEEK` | `CALENDAR_PERIOD("WEEK", -1)` |
627
- | `LAST_WEEK_START` | `START_OF(CALENDAR_PERIOD("WEEK", -1))` |
628
- | `LAST_WEEK_END` | `END_OF(CALENDAR_PERIOD("WEEK", -1))` |
629
- | `CURRENT_QUARTER` | `CALENDAR_PERIOD("QUARTER", 0)` |
630
- | `CURRENT_QUARTER_START` | `START_OF(CALENDAR_PERIOD("QUARTER", 0))` |
631
- | `CURRENT_QUARTER_END` | `END_OF(CALENDAR_PERIOD("QUARTER", 0))` |
632
- | `LAST_QUARTER` | `CALENDAR_PERIOD("QUARTER", -1)` |
633
- | `LAST_QUARTER_START` | `START_OF(CALENDAR_PERIOD("QUARTER", -1))` |
634
- | `LAST_QUARTER_END` | `END_OF(CALENDAR_PERIOD("QUARTER", -1))` |
635
- | `CURRENT_YEAR` | `CALENDAR_PERIOD("YEAR", 0)` |
636
- | `CURRENT_YEAR_START` | `START_OF(CALENDAR_PERIOD("YEAR", 0))` |
637
- | `LAST_YEAR` | `CALENDAR_PERIOD("YEAR", -1)` |
638
- | `LAST_YEAR_START` | `START_OF(CALENDAR_PERIOD("YEAR", -1))` |
639
- | `LAST_YEAR_END` | `END_OF(CALENDAR_PERIOD("YEAR", -1))` |
640
- | `YEAR_BEFORE_LAST_YEAR_START` | `START_OF(CALENDAR_PERIOD("YEAR", -2))` |
641
- | `YEAR_BEFORE_LAST_YEAR_END` | `END_OF(CALENDAR_PERIOD("YEAR", -2))` |
672
+ | Token | QDP expression |
673
+ | ----------------------------- | --------------------------------------------- |
674
+ | `NOW` | `NOW()` |
675
+ | `TODAY` | `TODAY()` |
676
+ | `CURRENT_DATE` | `TODAY()` |
677
+ | `TODAY-7` | `START_OF(RELATIVE_PERIOD(-7, "DAY"))` |
678
+ | `TODAY-30` | `START_OF(RELATIVE_PERIOD(-30, "DAY"))` |
679
+ | `TODAY-60` | `START_OF(RELATIVE_PERIOD(-60, "DAY"))` |
680
+ | `TODAY-90` | `START_OF(RELATIVE_PERIOD(-90, "DAY"))` |
681
+ | `TODAY-120` | `START_OF(RELATIVE_PERIOD(-120, "DAY"))` |
682
+ | `TODAY-365` | `START_OF(RELATIVE_PERIOD(-365, "DAY"))` |
683
+ | `YESTERDAY` | `START_OF(RELATIVE_PERIOD(-1, "DAY"))` |
684
+ | `TOMORROW` | `START_OF(END_OF(RELATIVE_PERIOD(1, "DAY")))` |
685
+ | `CURRENT_MONTH` | `CALENDAR_PERIOD("MONTH", 0)` |
686
+ | `CURRENT_MONTH_START` | `START_OF(CALENDAR_PERIOD("MONTH", 0))` |
687
+ | `CURRENT_MONTH_END` | `END_OF(CALENDAR_PERIOD("MONTH", 0))` |
688
+ | `LAST_MONTH` | `CALENDAR_PERIOD("MONTH", -1)` |
689
+ | `LAST_MONTH_START` | `START_OF(CALENDAR_PERIOD("MONTH", -1))` |
690
+ | `LAST_MONTH_END` | `END_OF(CALENDAR_PERIOD("MONTH", -1))` |
691
+ | `CURRENT_WEEK` | `CALENDAR_PERIOD("WEEK", 0)` |
692
+ | `CURRENT_WEEK_START` | `START_OF(CALENDAR_PERIOD("WEEK", 0))` |
693
+ | `CURRENT_WEEK_END` | `END_OF(CALENDAR_PERIOD("WEEK", 0))` |
694
+ | `LAST_WEEK` | `CALENDAR_PERIOD("WEEK", -1)` |
695
+ | `LAST_WEEK_START` | `START_OF(CALENDAR_PERIOD("WEEK", -1))` |
696
+ | `LAST_WEEK_END` | `END_OF(CALENDAR_PERIOD("WEEK", -1))` |
697
+ | `CURRENT_QUARTER` | `CALENDAR_PERIOD("QUARTER", 0)` |
698
+ | `CURRENT_QUARTER_START` | `START_OF(CALENDAR_PERIOD("QUARTER", 0))` |
699
+ | `CURRENT_QUARTER_END` | `END_OF(CALENDAR_PERIOD("QUARTER", 0))` |
700
+ | `LAST_QUARTER` | `CALENDAR_PERIOD("QUARTER", -1)` |
701
+ | `LAST_QUARTER_START` | `START_OF(CALENDAR_PERIOD("QUARTER", -1))` |
702
+ | `LAST_QUARTER_END` | `END_OF(CALENDAR_PERIOD("QUARTER", -1))` |
703
+ | `CURRENT_YEAR` | `CALENDAR_PERIOD("YEAR", 0)` |
704
+ | `CURRENT_YEAR_START` | `START_OF(CALENDAR_PERIOD("YEAR", 0))` |
705
+ | `LAST_YEAR` | `CALENDAR_PERIOD("YEAR", -1)` |
706
+ | `LAST_YEAR_START` | `START_OF(CALENDAR_PERIOD("YEAR", -1))` |
707
+ | `LAST_YEAR_END` | `END_OF(CALENDAR_PERIOD("YEAR", -1))` |
708
+ | `YEAR_BEFORE_LAST_YEAR_START` | `START_OF(CALENDAR_PERIOD("YEAR", -2))` |
709
+ | `YEAR_BEFORE_LAST_YEAR_END` | `END_OF(CALENDAR_PERIOD("YEAR", -2))` |
642
710
 
643
711
  Tokens can be wrapped like `{{TODAY-7}}`. The normalizer removes braces and outer spaces.
644
712
 
@@ -646,51 +714,51 @@ Scalar tokens such as `CURRENT_TIME`, `CURRENT_TIMEZONE`, and `CURRENT_DAY_OF_WE
646
714
 
647
715
  ## Main Equivalences With The Date Picker
648
716
 
649
- | Picker expression | QDP expression | Note |
650
- | --- | --- | --- |
651
- | Today | `TODAY()` | Fixed date at the start of the current day |
652
- | Yesterday | `START_OF(RELATIVE_PERIOD(-1, "DAY"))` | Start of yesterday |
653
- | Tomorrow | `START_OF(END_OF(RELATIVE_PERIOD(1, "DAY")))` | Start of tomorrow |
654
- | This month | `CALENDAR_PERIOD("MONTH", 0)` | Current calendar month |
655
- | Previous month | `CALENDAR_PERIOD("MONTH", -1)` | Previous calendar month |
656
- | Last 30 days inclusive | `RELATIVE_PERIOD(-29, "DAY")` | Inclusive 30-day rolling window counting today |
657
- | April | `PARTIAL_DATE("ANY", 4)` | Recurring month |
658
- | April 2026 | `PERIOD_AT("MONTH", 4, 2026)` | Positional month in a year |
659
- | Apr 15 | `PARTIAL_DATE("ANY", 4, "ANY", "ANY", 15)` | Recurring month/day |
660
- | Day 15 | `PARTIAL_DATE("ANY", "ANY", "ANY", "ANY", 15)` | Recurring day of month |
661
- | Q2 | `PARTIAL_DATE("ANY", "ANY", 2)` | Recurring quarter |
662
- | Q2 2026 | `PERIOD_AT("QUARTER", 2, 2026)` | Positional quarter in a year |
663
- | W40 | `PARTIAL_DATE("ANY", "ANY", "ANY", 40)` | Recurring week |
664
- | W40 2026 | `PERIOD_AT("WEEK", 40, 2026)` | Positional week in a year |
665
- | Last week of 2028 | `PERIOD_AT("WEEK", "LAST", 2028)` | Avoids assuming W53/W54 |
666
- | Last month of the past year | `PERIOD_AT("MONTH", "LAST", 2025)` | Equivalent to December 2025 with current date 2026 |
667
- | Between inclusive | `DATE_RANGE(DATE(start), END_OF(DATE(end)))` | Materialize inclusive boundaries |
668
- | Between exclusive | `DATE_RANGE(DATE(adjustedStart), END_OF(DATE(adjustedEnd)))` | Materialize exclusion in the dates |
717
+ | Picker expression | QDP expression | Note |
718
+ | --------------------------- | ------------------------------------------------------------ | -------------------------------------------------- |
719
+ | Today | `TODAY()` | Fixed date at the start of the current day |
720
+ | Yesterday | `START_OF(RELATIVE_PERIOD(-1, "DAY"))` | Start of yesterday |
721
+ | Tomorrow | `START_OF(END_OF(RELATIVE_PERIOD(1, "DAY")))` | Start of tomorrow |
722
+ | This month | `CALENDAR_PERIOD("MONTH", 0)` | Current calendar month |
723
+ | Previous month | `CALENDAR_PERIOD("MONTH", -1)` | Previous calendar month |
724
+ | Last 30 days inclusive | `RELATIVE_PERIOD(-29, "DAY")` | Inclusive 30-day rolling window counting today |
725
+ | April | `PARTIAL_DATE("ANY", 4)` | Recurring month |
726
+ | April 2026 | `PERIOD_AT("MONTH", 4, 2026)` | Positional month in a year |
727
+ | Apr 15 | `PARTIAL_DATE("ANY", 4, "ANY", "ANY", 15)` | Recurring month/day |
728
+ | Day 15 | `PARTIAL_DATE("ANY", "ANY", "ANY", "ANY", 15)` | Recurring day of month |
729
+ | Q2 | `PARTIAL_DATE("ANY", "ANY", 2)` | Recurring quarter |
730
+ | Q2 2026 | `PERIOD_AT("QUARTER", 2, 2026)` | Positional quarter in a year |
731
+ | W40 | `PARTIAL_DATE("ANY", "ANY", "ANY", 40)` | Recurring week |
732
+ | W40 2026 | `PERIOD_AT("WEEK", 40, 2026)` | Positional week in a year |
733
+ | Last week of 2028 | `PERIOD_AT("WEEK", "LAST", 2028)` | Avoids assuming W53/W54 |
734
+ | Last month of the past year | `PERIOD_AT("MONTH", "LAST", 2025)` | Equivalent to December 2025 with current date 2026 |
735
+ | Between inclusive | `DATE_RANGE(DATE(start), END_OF(DATE(end)))` | Materialize inclusive boundaries |
736
+ | Between exclusive | `DATE_RANGE(DATE(adjustedStart), END_OF(DATE(adjustedEnd)))` | Materialize exclusion in the dates |
669
737
 
670
738
  ## Validations And Expected Errors
671
739
 
672
740
  Important invalid cases:
673
741
 
674
742
  ```ts
675
- DATE("2026-02-31")
743
+ DATE("2026-02-31");
676
744
  // INVALID_ALLOW_VALUE: calendrically invalid date
677
745
 
678
- DATE("2026-02-31T00:00")
746
+ DATE("2026-02-31T00:00");
679
747
  // INVALID_ALLOW_VALUE: calendrically invalid date
680
748
 
681
- RELATIVE_PERIOD(-1, "HOUR")
749
+ RELATIVE_PERIOD(-1, "HOUR");
682
750
  // INVALID_ALLOW_VALUE: unsupported unit
683
751
 
684
- CALENDAR_PERIOD("DECADE", 0)
752
+ CALENDAR_PERIOD("DECADE", 0);
685
753
  // INVALID_ALLOW_VALUE: unsupported period
686
754
 
687
- PERIOD_AT("MONTH", "MIDDLE", 2026)
755
+ PERIOD_AT("MONTH", "MIDDLE", 2026);
688
756
  // INVALID_ALLOW_VALUE: unsupported symbolic position
689
757
 
690
- PARTIAL_DATE("ANY", 13)
758
+ PARTIAL_DATE("ANY", 13);
691
759
  // INVALID_ALLOW_VALUE: month out of range
692
760
 
693
- DATE_RANGE(DATE("2026-07-31T00:00"), DATE("2026-07-01T00:00"))
761
+ DATE_RANGE(DATE("2026-07-31T00:00"), DATE("2026-07-01T00:00"));
694
762
  // INVALID_DATE_RANGE: end before start
695
763
  ```
696
764
 
@@ -698,25 +766,25 @@ Additionally, any QDP expression whose final result is not `date`, `dateRange`,
698
766
 
699
767
  ## Relevant Files
700
768
 
701
- | File | Responsibility |
702
- | --- | --- |
703
- | `src/date-presets/date-presets.ts` | Public API, JSON conversion, formatting, and `valueType` classification |
704
- | `src/date-presets/date-preset-tokens.ts` | Legacy token map to QDP expressions |
705
- | `src/functions/index.ts` | Function registration for `ENGINES.QDP` |
706
- | `src/utils/datePresetUtils.ts` | Date, range, calendar, week, and partial date resolution |
707
- | `src/utils/timezone.ts` | Timezone definition normalization and offset helpers |
708
- | `src/functions/*.ts` | Individual QDP function definitions |
709
- | `__tests__/unit/datePresetTranspiler.test.ts` | Main transpilation and picker compatibility cases |
710
- | `__tests__/unit/datePresetJson.test.ts` | JSON to QDP conversion |
711
- | `__tests__/unit/datePresetFormat.test.ts` | Date and partial date formatting |
712
- | `__tests__/unit/datePresetTokens.test.ts` | Legacy tokens |
713
- | `date-preset-qdp-examples.csv` | Generated examples with tokens and picker expressions |
769
+ | File | Responsibility |
770
+ | --------------------------------------------- | ----------------------------------------------------------------------- |
771
+ | `src/date-presets/date-presets.ts` | Public API, JSON conversion, formatting, and `valueType` classification |
772
+ | `src/date-presets/date-preset-tokens.ts` | Legacy token map to QDP expressions |
773
+ | `src/functions/index.ts` | Function registration for `ENGINES.QDP` |
774
+ | `src/utils/datePresetUtils.ts` | Date, range, calendar, week, and partial date resolution |
775
+ | `src/utils/timezone.ts` | Timezone definition normalization and offset helpers |
776
+ | `src/functions/*.ts` | Individual QDP function definitions |
777
+ | `__tests__/unit/datePresetTranspiler.test.ts` | Main transpilation and picker compatibility cases |
778
+ | `__tests__/unit/datePresetJson.test.ts` | JSON to QDP conversion |
779
+ | `__tests__/unit/datePresetFormat.test.ts` | Date and partial date formatting |
780
+ | `__tests__/unit/datePresetTokens.test.ts` | Legacy tokens |
781
+ | `date-preset-qdp-examples.csv` | Generated examples with tokens and picker expressions |
714
782
 
715
783
  ## Current State And Design Notes
716
784
 
717
- - QDP is limited to date preset functions in `ENGINE_FN_MAP[ENGINES.QDP]`.
718
- - Date preset expressions can be nested as long as the final result is `date`, `dateRange`, or `partialDate`.
719
- - `partialDate` was added as a primitive to represent incomplete date picker selections.
720
- - Week compatibility with the date picker uses Sunday as the default start day and supports W54.
721
- - `PERIOD_AT` supports symbolic positions `FIRST` and `LAST` to reduce ambiguity in variable-length periods.
722
- - For documentation and example auditing, the CSV `date-preset-qdp-examples.csv` contains resolved, formatted, and classified values.
785
+ - 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 `partialDate`.
787
+ - `partialDate` was added as a primitive to represent incomplete date picker selections.
788
+ - Week compatibility with the date picker uses Sunday as the default start day and supports W54.
789
+ - `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.