Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/20771-wall-clock-one-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@objectstack/core': patch
---

`isUninterpretableTemporalComparand` reads a bare wall clock on a `time` column by the spec's `ClockTimeValueSchema` (`@objectstack/spec/data`), not by a private copy of it (#20771)

Clause-②: no

The wall-clock half of core's `time` rule (`HH:MM[:SS[.fraction]]`, hours 00 to 23, minutes and seconds 00 to 59, no time zone) was spelled twice: once as the spec's `ClockTimeValueSchema`, the stored form of a `time` value, and once as a private regex in `@objectstack/core`. The two admitted the same strings, but nothing tied them together, so an edit to either one changed one side only. Core now asks the spec schema. Every caller of `isUninterpretableTemporalComparand('time', …)` therefore answers from the rule the spec's `time` default gate uses: the engine's temporal-comparand door, the analytics comparand check, the record validator's `time` arm and the import's `time` coercion.

Unchanged:

- Every string gets the verdict it got before. Measured over 8,655,360 generated strings: 8,640 read by both the old regex and the schema, the rest refused by both, 0 answered differently.
- An instant, a number or a `Date` on a `time` column is judged as before.
18 changes: 18 additions & 0 deletions packages/core/src/utils/temporal-comparand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,3 +372,21 @@ describe('[#20480] isUninterpretableTemporalComparand — a time column\'s insta
}
});
});

// [#20771] The bare-wall-clock half of the `time` rule is the spec's
// `ClockTimeValueSchema` (`@objectstack/spec/data`), not a second regex in
// core: the private `readsAsWallClock` asks that schema. These are the
// boundary cases on each side, asserted through the one public door that
// reaches `readsAsWallClock`. None of them is an ISO instant, so the
// instant half of the rule cannot rescue one: the verdict is the wall-clock
// half's alone.
describe('[#20771] isUninterpretableTemporalComparand — the wall-clock half is the spec\'s time-of-day rule', () => {
it('reads the last wall clock of the day, to the fraction', () => {
expect(isUninterpretableTemporalComparand('time', '23:59:59.999')).toBe(false);
});

it('refuses the hour past the last one, and a time of day with a zone', () => {
expect(isUninterpretableTemporalComparand('time', '24:00')).toBe(true);
expect(isUninterpretableTemporalComparand('time', '10:00Z')).toBe(true);
});
});
13 changes: 9 additions & 4 deletions packages/core/src/utils/temporal-comparand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@
* test), so it is left alone too.
*/

import { classifyFilterToken } from '@objectstack/spec/data';
import { ClockTimeValueSchema, classifyFilterToken } from '@objectstack/spec/data';
import { isOutsideTemporalYearRange, temporalStorageForm } from './temporal-storage-form.js';

/** Which temporal storage rule a declared field takes. */
Expand Down Expand Up @@ -246,11 +246,16 @@ function readsAsCalendarDay(s: string): boolean {
* `temporalStorageForm`'s `time` reading: a bare wall clock whose components are in
* range. Out-of-range (`25:00`) is uninterpretable — the rule it mirrors
* returns such a value untouched rather than wrapping it.
*
* [#20771] The set is the spec's: `ClockTimeValueSchema`, the `time` stored
* form — `HH:MM[:SS[.fraction]]`, hours 00..23, minutes and seconds 00..59, no
* time zone (ADR-0053 D-C1). This function asks that schema rather than
* spelling a second regex, so the comparand door, the record validator's
* `time` arm and the spec's `time` default gate answer from one rule and
* cannot drift apart on an edit to either side.
*/
function readsAsWallClock(s: string): boolean {
const m = /^(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?$/.exec(s);
if (!m) return false;
return Number(m[1]) <= 23 && Number(m[2]) <= 59 && Number(m[3] ?? '0') <= 59;
return ClockTimeValueSchema.safeParse(s).success;
}

/**
Expand Down
Loading