time: Date, Weekday and RFC 3339 parsing of Timestamp (gap 11, slice 1) #111

Closed
opened 2026-10-05 21:07:21 +00:00 by orion · 6 comments
Collaborator

Context

First slice of FIRST-PARTY.md gap 11 (calendar types and parsing for std/time). Gap 11 is too large for one PR (wall-clock types, offsets, named zones, formats), so it is split; this issue is the part every other slice builds on: Date, Weekday, a TimeParseError, and reading a Timestamp from RFC 3339 text. torb/sql keeps its private Date, timestampOf, dateOf and Hinnant's algorithms until it can move onto these (not part of this issue).

Follow-up slices: #387 (wall-clock TimeOfDay/DateTime/UtcOffset/OffsetDateTime), #388 (HTTP-date and basic ISO 8601 formats), #389 (named time zones, design record first).

Design (decided by the loop, see the comment of 2026-10-20)

All in std/time/src/lib.trb (split into src/date.trb etc. if the file grows), re-exported from the package; no prelude change, no syntax change, no new native, no seed impact.

  • Date: a day in the proleptic Gregorian calendar, years 0000 to 9999. Private field days (days since 1970-01-01), so every value is valid by construction. with Compare, Equals, Hash, Show.
    • static fn of(year: Int64, month: Int64, day: Int64): Result<Date, TimeParseError> (fails for a month outside 1..12 or a day the month does not have)
    • static fn fromDaysSinceEpoch(days: Int64): Result<Date, TimeParseError> and fn daysSinceEpoch(): Int64
    • fn year(): Int64, fn month(): Int64, fn day(): Int64, fn weekday(): Weekday, fn isLeapYear(): Bool, static fn daysInMonth(year, month)
    • fn plusDays(days: Int64): Result<Date, TimeParseError> (fails outside years 0000..9999) and fn daysUntil(other: Date): Int64; no operator overloads (a Date plus a Duration would hide the day-versus-24-hours question)
    • static fn parse(text: String): Result<Date, TimeParseError> reads YYYY-MM-DD only (4-digit year, strict); show() writes the same
  • Weekday: enum Monday .. Sunday with Show ("Monday"), Compare by ISO order, fn number(): Int64 (ISO: Monday = 1).
  • TimeParseError: a record with message: String and Show; this is what Date and Timestamp fail with. Naming follows NumberParseError.
  • Timestamp.parse(text: String): Result<Timestamp, TimeParseError>, RFC 3339 date-time:
    • T, t or one space between date and time; Z/z or +hh:mm/-hh:mm offset (hours 00-23, minutes 00-59; -00:00 is UTC); the offset is applied, the result is the UTC instant.
    • Fraction of a second: one or more digits after .; more than nine are truncated (not rounded), documented.
    • Leap second (:60) is refused with a clear message: Timestamp is POSIX time and has no leap seconds. Relaxing that later is not breaking.
    • Out of the range of an Int64 of nanoseconds (1677-09-21T00:12:43.145224192Z to 2262-04-11T23:47:16.854775807Z) fails with a message naming that range. Widening Timestamp is out of scope; dates outside the range are what Date is for.
    • Show of Timestamp already writes RFC 3339 UTC; Timestamp.parse(t.show()) must round-trip for every Timestamp.
  • Timestamp.date(): Date (the UTC day), Date.startOfDay(): Result<Timestamp, TimeParseError> (00:00:00 UTC, fails outside the Timestamp range).
  • Timestamp's private helpers civilDate, floorDivided, padded are reused (not duplicated); daysSinceEpoch is Hinnant's days_from_civil.
  • std/toml keeps its own TomlDate (TOML-specific rules); moving it onto Date is a separate refactor, not here.

Seed

Touches the seed: no. No syntax change, no native, no driver flag, no std name the compiler looks up by string. One compiler edit: capabilityOfImport in compiler/src/package/archive.trb lists the value names of std/time that reach no capability; add Date, Weekday, TimeParseError there (plus a case in compiler/tests/capability.test.trb), otherwise use Date from "std/time" reports the clock capability. Because compiler/src changes, this is a normal tier A bootstrap in CI, no seed refresh.

Acceptance criteria

  • Date, Weekday, TimeParseError and Timestamp.parse, Timestamp.date, Date.startOfDay as designed above, in std/time
  • capabilityOfImport lists Date, Weekday, TimeParseError as capability-free; compiler/tests/capability.test.trb covers it (use Date from "std/time" reaches nothing, use Clock from "std/time" still reaches clock)
  • Tests in std/time/tests/ (date.test.trb, parse.test.trb): leap years (1900, 2000, 2024, 2100), year 0000 and 9999, Date before 1970, weekday of known dates (1970-01-01 Thursday, 2000-02-29 Tuesday, 2024-12-31 Tuesday), invalid dates (2023-02-29, month 13, day 0), Timestamp.parse with Z, z, +02:00, -05:30, -00:00, lowercase t, space separator, 0/1/3/9/12 fraction digits, leap second refused, missing offset refused (2026-09-27T08:15:00 has no offset), trailing garbage refused, out-of-range refused with the range in the message, round trip Timestamp.parse(t.show()) == Ok t for the epoch, a negative value, and the Int64 limits
  • RFC 3339 section 5.8 example values parse to the expected instants (1985-04-12T23:20:50.52Z, 1996-12-19T16:39:57-08:00, 1990-12-31T23:59:60Z refused)
  • Docs: docs/standard-library/time.md documents the new declarations with trb check examples; torb docs skill docs skills regenerated and committed
  • FIRST-PARTY.md gap 11 is marked "partly done" with the date, naming what this slice delivered and the follow-up issues; it is marked done only when the last slice lands
  • Lands as at most two Conventional Commits, e.g. feat(time): Date, Weekday and RFC 3339 parsing of Timestamp and docs(time): ...; gates a green in forge CI

Dependencies

None.

Moved from the planning lists of the docs; the design record stays the specification.

## Context First slice of [FIRST-PARTY.md gap 11](https://git.torb.dev/torbscript/language/src/commit/393ef2608e4a60b8ff76d8b6b408951b822d887d/docs/design/FIRST-PARTY.md#L271) (calendar types and parsing for `std/time`). Gap 11 is too large for one PR (wall-clock types, offsets, named zones, formats), so it is split; this issue is the part every other slice builds on: `Date`, `Weekday`, a `TimeParseError`, and reading a `Timestamp` from RFC 3339 text. `torb/sql` keeps its private `Date`, `timestampOf`, `dateOf` and Hinnant's algorithms until it can move onto these (not part of this issue). Follow-up slices: #387 (wall-clock `TimeOfDay`/`DateTime`/`UtcOffset`/`OffsetDateTime`), #388 (HTTP-date and basic ISO 8601 formats), #389 (named time zones, design record first). ## Design (decided by the loop, see the comment of 2026-10-20) All in `std/time/src/lib.trb` (split into `src/date.trb` etc. if the file grows), re-exported from the package; no prelude change, no syntax change, no new native, no seed impact. - `Date`: a day in the proleptic Gregorian calendar, years 0000 to 9999. Private field `days` (days since 1970-01-01), so every value is valid by construction. `with Compare, Equals, Hash, Show`. - `static fn of(year: Int64, month: Int64, day: Int64): Result<Date, TimeParseError>` (fails for a month outside 1..12 or a day the month does not have) - `static fn fromDaysSinceEpoch(days: Int64): Result<Date, TimeParseError>` and `fn daysSinceEpoch(): Int64` - `fn year(): Int64`, `fn month(): Int64`, `fn day(): Int64`, `fn weekday(): Weekday`, `fn isLeapYear(): Bool`, `static fn daysInMonth(year, month)` - `fn plusDays(days: Int64): Result<Date, TimeParseError>` (fails outside years 0000..9999) and `fn daysUntil(other: Date): Int64`; no operator overloads (a `Date` plus a `Duration` would hide the day-versus-24-hours question) - `static fn parse(text: String): Result<Date, TimeParseError>` reads `YYYY-MM-DD` only (4-digit year, strict); `show()` writes the same - `Weekday`: enum `Monday` .. `Sunday` with `Show` ("Monday"), `Compare` by ISO order, `fn number(): Int64` (ISO: Monday = 1). - `TimeParseError`: a record with `message: String` and `Show`; this is what `Date` and `Timestamp` fail with. Naming follows `NumberParseError`. - `Timestamp.parse(text: String): Result<Timestamp, TimeParseError>`, RFC 3339 `date-time`: - `T`, `t` or one space between date and time; `Z`/`z` or `+hh:mm`/`-hh:mm` offset (hours 00-23, minutes 00-59; `-00:00` is UTC); the offset is applied, the result is the UTC instant. - Fraction of a second: one or more digits after `.`; more than nine are truncated (not rounded), documented. - Leap second (`:60`) is refused with a clear message: `Timestamp` is POSIX time and has no leap seconds. Relaxing that later is not breaking. - Out of the range of an `Int64` of nanoseconds (1677-09-21T00:12:43.145224192Z to 2262-04-11T23:47:16.854775807Z) fails with a message naming that range. Widening `Timestamp` is out of scope; dates outside the range are what `Date` is for. - `Show` of `Timestamp` already writes RFC 3339 UTC; `Timestamp.parse(t.show())` must round-trip for every `Timestamp`. - `Timestamp.date(): Date` (the UTC day), `Date.startOfDay(): Result<Timestamp, TimeParseError>` (00:00:00 UTC, fails outside the Timestamp range). - `Timestamp`'s private helpers `civilDate`, `floorDivided`, `padded` are reused (not duplicated); `daysSinceEpoch` is Hinnant's `days_from_civil`. - `std/toml` keeps its own `TomlDate` (TOML-specific rules); moving it onto `Date` is a separate refactor, not here. ## Seed Touches the seed: no. No syntax change, no native, no driver flag, no std name the compiler looks up by string. One compiler edit: `capabilityOfImport` in `compiler/src/package/archive.trb` lists the value names of `std/time` that reach no capability; add `Date`, `Weekday`, `TimeParseError` there (plus a case in `compiler/tests/capability.test.trb`), otherwise `use Date from "std/time"` reports the `clock` capability. Because `compiler/src` changes, this is a normal tier A bootstrap in CI, no seed refresh. ## Acceptance criteria - [ ] `Date`, `Weekday`, `TimeParseError` and `Timestamp.parse`, `Timestamp.date`, `Date.startOfDay` as designed above, in `std/time` - [ ] `capabilityOfImport` lists `Date`, `Weekday`, `TimeParseError` as capability-free; `compiler/tests/capability.test.trb` covers it (`use Date from "std/time"` reaches nothing, `use Clock from "std/time"` still reaches `clock`) - [ ] Tests in `std/time/tests/` (`date.test.trb`, `parse.test.trb`): leap years (1900, 2000, 2024, 2100), year 0000 and 9999, `Date` before 1970, weekday of known dates (1970-01-01 Thursday, 2000-02-29 Tuesday, 2024-12-31 Tuesday), invalid dates (`2023-02-29`, month 13, day 0), `Timestamp.parse` with `Z`, `z`, `+02:00`, `-05:30`, `-00:00`, lowercase `t`, space separator, 0/1/3/9/12 fraction digits, leap second refused, missing offset refused (`2026-09-27T08:15:00` has no offset), trailing garbage refused, out-of-range refused with the range in the message, round trip `Timestamp.parse(t.show()) == Ok t` for the epoch, a negative value, and the Int64 limits - [ ] RFC 3339 section 5.8 example values parse to the expected instants (`1985-04-12T23:20:50.52Z`, `1996-12-19T16:39:57-08:00`, `1990-12-31T23:59:60Z` refused) - [ ] Docs: `docs/standard-library/time.md` documents the new declarations with `trb check` examples; `torb docs skill docs skills` regenerated and committed - [ ] `FIRST-PARTY.md` gap 11 is marked "partly done" with the date, naming what this slice delivered and the follow-up issues; it is marked done only when the last slice lands - [ ] Lands as at most two Conventional Commits, e.g. `feat(time): Date, Weekday and RFC 3339 parsing of Timestamp` and `docs(time): ...`; gates `a` green in forge CI ## Dependencies None. _Moved from the planning lists of the docs; the design record stays the specification._
Author
Collaborator

[loop:torbscript] intake: in scope (author orion, label loop), source FIRST-PARTY.md gap 11, not a duplicate. No dependencies. next: waiting (refine queued, ready buffer full) until 2026-10-10T04:00:00Z

[loop:torbscript] intake: in scope (author orion, label loop), source FIRST-PARTY.md gap 11, not a duplicate. No dependencies. next: waiting (refine queued, ready buffer full) until 2026-10-10T04:00:00Z
Author
Collaborator

[loop:torbscript] claim refine by session:loop-torbscript-20261020 until 2026-10-20T00:00Z

[loop:torbscript] claim refine by session:loop-torbscript-20261020 until 2026-10-20T00:00Z
orion changed title from time: calendar types and RFC 3339 parsing to time: Date, Weekday and RFC 3339 parsing of Timestamp (gap 11, slice 1) 2026-10-19 22:05:55 +00:00
Author
Collaborator

[loop:torbscript] decided by the loop: gap 11 is split into four issues, #111 (this one: Date, Weekday, TimeParseError, Timestamp.parse), #387 (TimeOfDay, DateTime, UtcOffset, OffsetDateTime), #388 (HTTP-date, basic ISO 8601), #389 (named time zones, design record docs/design/TIME.md first). Why: the whole gap is four independent pieces of API plus a tz-database design question; one PR would be far over two commits, and the first slice is what the drivers and Timestamp consumers need soonest.

Decisions in #111, all technical, none an owner category:

  • Date stores days since 1970-01-01 privately (valid by construction, cheap Compare/Hash), accessors are methods (year(), ...), years 0000-9999; no operator overloads, because Date + Duration hides the day-versus-24-hours question; plusDays instead.
  • Not in the prelude (unlike Timestamp): a name like Date in every file would clash with user types; it is use Date from "std/time", and capabilityOfImport (compiler/src/package/archive.trb) lists it as capability-free so the import does not report clock. Not a seed change: a pure list addition, no syntax, native or looked-up std name.
  • Timestamp is an Int64 of nanoseconds, i.e. 1677-09-21 to 2262-04-11; text outside it fails with a message naming the range. Widening the representation would change a published value type, so out of scope; Date covers the wider years.
  • RFC 3339 parse: T/t/space, Z/z/numeric offset applied to give UTC, offset mandatory (a text without one is not an instant), fraction truncated beyond nine digits, :60 leap second refused (POSIX time has none; accepting later is non-breaking).
  • Error type TimeParseError, named after NumberParseError; parsing is static fn parse as in std/toml's TomlDate. TomlDate stays; moving it onto Date is a separate refactor with no user value now.
  • Hinnant's algorithms already private in std/time/src/lib.trb are reused, not duplicated; torb/sql moves onto the public API later.
    Seed: untouched. Two Conventional Commits at most (feat + docs).
[loop:torbscript] decided by the loop: gap 11 is split into four issues, #111 (this one: `Date`, `Weekday`, `TimeParseError`, `Timestamp.parse`), #387 (`TimeOfDay`, `DateTime`, `UtcOffset`, `OffsetDateTime`), #388 (HTTP-date, basic ISO 8601), #389 (named time zones, design record `docs/design/TIME.md` first). Why: the whole gap is four independent pieces of API plus a tz-database design question; one PR would be far over two commits, and the first slice is what the drivers and `Timestamp` consumers need soonest. Decisions in #111, all technical, none an owner category: - `Date` stores days since 1970-01-01 privately (valid by construction, cheap `Compare`/`Hash`), accessors are methods (`year()`, ...), years 0000-9999; no operator overloads, because `Date + Duration` hides the day-versus-24-hours question; `plusDays` instead. - Not in the prelude (unlike `Timestamp`): a name like `Date` in every file would clash with user types; it is `use Date from "std/time"`, and `capabilityOfImport` (`compiler/src/package/archive.trb`) lists it as capability-free so the import does not report `clock`. Not a seed change: a pure list addition, no syntax, native or looked-up std name. - `Timestamp` is an `Int64` of nanoseconds, i.e. 1677-09-21 to 2262-04-11; text outside it fails with a message naming the range. Widening the representation would change a published value type, so out of scope; `Date` covers the wider years. - RFC 3339 parse: `T`/`t`/space, `Z`/`z`/numeric offset applied to give UTC, offset mandatory (a text without one is not an instant), fraction truncated beyond nine digits, `:60` leap second refused (POSIX time has none; accepting later is non-breaking). - Error type `TimeParseError`, named after `NumberParseError`; parsing is `static fn parse` as in `std/toml`'s `TomlDate`. `TomlDate` stays; moving it onto `Date` is a separate refactor with no user value now. - Hinnant's algorithms already private in `std/time/src/lib.trb` are reused, not duplicated; `torb/sql` moves onto the public API later. Seed: untouched. Two Conventional Commits at most (feat + docs).
Author
Collaborator

[loop:torbscript] refine: #111 is ready as slice 1 of gap 11 (Date, Weekday, TimeParseError, Timestamp.parse; body and acceptance criteria rewritten, decisions in the comment above); the rest is split into #387 (wall-clock types, offsets), #388 (HTTP-date, basic ISO 8601) and #389 (named zones, design record first), all label loop, not yet refined. Seed untouched. Tokens: about 85k (sonnet, estimate), next: implement

[loop:torbscript] refine: #111 is ready as slice 1 of gap 11 (Date, Weekday, TimeParseError, Timestamp.parse; body and acceptance criteria rewritten, decisions in the comment above); the rest is split into #387 (wall-clock types, offsets), #388 (HTTP-date, basic ISO 8601) and #389 (named zones, design record first), all label loop, not yet refined. Seed untouched. Tokens: about 85k (sonnet, estimate), next: implement
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261020 until 2026-10-20T04:46Z

[loop:torbscript] claim implement by session:loop-torbscript-20261020 until 2026-10-20T04:46Z
Author
Collaborator

[loop:torbscript] implement: Date, Weekday, TimeParseError, Timestamp.parse, Timestamp.date and Date.startOfDay in std/time, capability-free in capabilityOfImport, tests, docs, regenerated skills/ and gap 11 marked partly done in FIRST-PARTY.md. PR #390 (two commits). It also fixes an overflow in Timestamp.show at the first instant of the range. Forge CI is green on 8927a418 (gates, tier A, bootstrap linux-x64, playground, commit messages, pdisc). Token usage: roughly 1.1M across the implementation worker, including one context compaction. next: review

[loop:torbscript] implement: Date, Weekday, TimeParseError, Timestamp.parse, Timestamp.date and Date.startOfDay in std/time, capability-free in capabilityOfImport, tests, docs, regenerated skills/ and gap 11 marked partly done in FIRST-PARTY.md. PR https://git.torb.dev/torbscript/language/pulls/390 (two commits). It also fixes an overflow in Timestamp.show at the first instant of the range. Forge CI is green on 8927a418 (gates, tier A, bootstrap linux-x64, playground, commit messages, pdisc). Token usage: roughly 1.1M across the implementation worker, including one context compaction. next: review
argus closed this issue 2026-10-19 23:15:57 +00:00
Sign in to join this conversation.
No labels
loop
ready
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language#111
No description provided.