diff --git a/.changeset/dataset-measure-format-date-styles.md b/.changeset/dataset-measure-format-date-styles.md new file mode 100644 index 0000000000..9a1c025715 --- /dev/null +++ b/.changeset/dataset-measure-format-date-styles.md @@ -0,0 +1,13 @@ +--- +"@objectstack/spec": patch +--- + +`DatasetMeasureSchema.format` now documents what a DATE-valued measure can and cannot say, and the numeral-pattern examples no longer stand as the whole story. + +The field was silent about date measures while advertising `e.g. "$0,0.00", "0.0%"` — the pattern grammar a date measure is precisely unable to read. An author with a `min` / `max` over a date field read that line, wrote `format: 'YYYY-MM-DD'`, parsed clean, and got the locale default. + +The statement is carried by a `.describe()` where there was none, so it reaches the published surfaces an author actually reads: the generated JSON Schema (`json-schema/ui/Dataset.json`, `DatasetMeasure.json`) and the reference table in `content/docs/references/ui/dataset.mdx`, whose Description cell for `format` had been rendering the silence as a blank. The docblock above it carries the longer measured record. + +What it now says, measured rather than assumed against the objectui pin this repo builds against: a numeral pattern applies to a numeric measure; a date-valued measure never reads a date PATTERN — a date-only value reads `format` as a display STYLE (`short`, `relative`), and a datetime value ignores `format` altogether. + +Nothing accepts or rejects differently: `format` remains `z.string().optional()` and no measure is refused. Documentation over a published schema (objectui#7178 ruled A). diff --git a/content/docs/references/ui/dataset.mdx b/content/docs/references/ui/dataset.mdx index 96b4344cc9..5dab244d1e 100644 --- a/content/docs/references/ui/dataset.mdx +++ b/content/docs/references/ui/dataset.mdx @@ -84,7 +84,7 @@ const result = DatasetSchema.parse(data); | **aggregate** | `Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>` | optional | Aggregation (sum/avg/count/...); omit when `derived` is set | | **field** | `string` | optional | Aggregated field; optional for count(*) | | **filter** | `any` | optional | | -| **format** | `string` | optional | | +| **format** | `string` | optional | Numeral pattern for a NUMERIC measure — grouping, decimals, percent; e.g. "0,0.00", "0.0%". An amount takes its symbol from `currency`, not from a "$" in the pattern. A DATE-valued measure never reads a date pattern: `"YYYY-MM-DD"` renders the locale default. A date-only value reads `format` as a display style (`short`, `relative`); a datetime value ignores it. | | **currency** | `string` | optional | Display currency code (ISO 4217) | | **derived** | `{ op: Enum<'ratio' \| 'sum' \| 'difference' \| 'product'>; of: string[] }` | optional | | @@ -125,7 +125,7 @@ const result = DatasetSchema.parse(data); | **aggregate** | `Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>` | optional | Aggregation (sum/avg/count/...); omit when `derived` is set | | **field** | `string` | optional | Aggregated field; optional for count(*) | | **filter** | `any` | optional | | -| **format** | `string` | optional | | +| **format** | `string` | optional | Numeral pattern for a NUMERIC measure — grouping, decimals, percent; e.g. "0,0.00", "0.0%". An amount takes its symbol from `currency`, not from a "$" in the pattern. A DATE-valued measure never reads a date pattern: `"YYYY-MM-DD"` renders the locale default. A date-only value reads `format` as a display style (`short`, `relative`); a datetime value ignores it. | | **currency** | `string` | optional | Display currency code (ISO 4217) | | **derived** | `{ op: Enum<'ratio' \| 'sum' \| 'difference' \| 'product'>; of: string[] }` | optional | | diff --git a/packages/spec/src/ui/dataset.zod.ts b/packages/spec/src/ui/dataset.zod.ts index 353b34d4e8..57e5e857ab 100644 --- a/packages/spec/src/ui/dataset.zod.ts +++ b/packages/spec/src/ui/dataset.zod.ts @@ -188,8 +188,39 @@ export const DatasetMeasureSchema = lazySchema(() => strictObject({ field: z.string().optional().describe('Aggregated field; optional for count(*)'), /** Measure-scoped filter (e.g. only won deals for "won_amount"). */ filter: FilterConditionSchema.optional(), - /** Display format, e.g. "$0,0.00", "0.0%". */ - format: z.string().optional(), + /** + * Display format — a NUMERAL pattern controlling grouping, decimals and + * percent: `"0,0.00"`, `"0.0%"`. A `$` in the pattern is still honoured as a + * legacy literal, but a real amount takes its symbol from `currency` below, + * never from the pattern — see that field's note. + * + * A DATE-valued measure (`min` / `max` over a date field) never reads a date + * PATTERN here: `"YYYY-MM-DD"` is accepted by this schema, reaches the + * renderer, and produces the locale default. The shared date path takes a + * named STYLE instead, so a date-only value reads `format` as `short` + * (`Jul 4, '24`) or `relative` (`3 days ago` inside a ±7-day window, the + * absolute locale form outside it) — the same two words `DateCellRenderer` + * honours from `field.format` — while a DATETIME value ignores `format` + * altogether. + * + * Measured at the pin this repo builds against (`.objectui-sha` = + * `a472b0716`; re-derived at that pin 2026-09-07) in objectui + * `packages/core/src/utils/dataset-format.ts`: `formatMeasure` routes a + * non-numeric value through `formatMeasureDate` (`:184-197`), whose + * date-only arm threads `format` into the STYLE parameter of `formatDate` + * (`utils/date-display.ts:104-137`, whose `relative` branch falls back to + * the absolute form beyond ±7 days at `:90`), while its datetime arm calls + * `formatDateTime(v, { locale })` with no style at all (`:194`). Teaching + * the shared path a pattern grammar would change every list cell that reads + * it, so the gap is DOCUMENTED here rather than closed (objectui#7178 ruled + * A; the datetime half is objectui#7443). + */ + format: z.string().optional().describe( + 'Numeral pattern for a NUMERIC measure — grouping, decimals, percent; e.g. "0,0.00", "0.0%". ' + + 'An amount takes its symbol from `currency`, not from a "$" in the pattern. A DATE-valued ' + + 'measure never reads a date pattern: `"YYYY-MM-DD"` renders the locale default. A date-only ' + + 'value reads `format` as a display style (`short`, `relative`); a datetime value ignores it.', + ), /** * Display currency (ISO 4217, e.g. "USD", "CNY"). Carried onto the result * field so presentations render a locale-correct symbol via `Intl` rather