Skip to content

Fill in DateTimeFormatterBuilder, and apply a parsed offset - #5

Open
yogthos wants to merge 1 commit into
mainfrom
feat/formatter-builder
Open

Fill in DateTimeFormatterBuilder, and apply a parsed offset#5
yogthos wants to merge 1 commit into
mainfrom
feat/formatter-builder

Conversation

@yogthos

@yogthos yogthos commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

malli.transform builds its inst decoder with the builder API — appendFraction, appendOffset, parseDefaulting, optionalStart/optionalEnd — and only appendPattern and appendLiteral were implemented, so the namespace could not load at all.

The builder

It composes a pattern string, which the formatter and parse-with-pattern already speak, so each append* maps to the letters that mean the same thing:

  • appendFractionSs behind an optional decimal point
  • appendOffset → the generic X (every JVM offset pattern — +HHMM, +HH:MM:ss, +HHMMss — is the same run of sign/digits/colons to this parser, and the no-offset text is the Z it already accepts)
  • optionalStart/optionalEnd → the pattern language's own [ … ]
  • parseDefaulting records the fallback and returns the builder; parse-with-pattern already starts each field at the JVM's default

Optional sections

[ … ] needed real parser support. It used to fall through to the plain-literal arm, which consumed an input character per bracket and threw the whole walk out of alignment. A section is now attempted and, if a literal does not match or a numeric field finds no digits, skipped whole. Sections nest, since appendFraction with a zero minimum brackets itself and callers commonly wrap it again.

That strictness applies only inside a section — at the top level literals stay lenient, so every pattern that parsed before parses identically.

Two bugs found while certifying against reference java.time

  • .parse dropped a parsed offset. It returned a LocalDateTime, so (Instant/from (.parse f "2020-03-05T13:45:30+0200")) read 13:45 UTC instead of 11:45. An offset-bearing pattern now parses to an OffsetDateTime; a pattern without an offset letter still gives a LocalDateTime, which is what tick's parse-* expect.
  • A fraction scaled by the pattern width rather than the digits present, so a 9-wide field given .417 came out as nanoseconds instead of milliseconds.

ChronoField is deliberately not registered here — jolt core already models it as a real enum (jolt.time.enums), and re-registering the statics shadowed that with something weaker. Caught because it broke 10 existing assertions.

Gate

6 assertions in fmt_test, values certified against JDK 20, including the full malli-shaped formatter parsing with every optional section present, with only the fraction, with only the offset, and with none.

102 tests / 1043 assertions / 0 fail / 0 error. No regression — the 99 pre-existing tests and their 1037 assertions are unchanged, verified by running the suite with the source change stashed.

malli.transform builds its inst decoder with the builder API — appendFraction,
appendOffset, parseDefaulting, optionalStart/End — and only appendPattern and
appendLiteral were here, so the namespace could not load.

The builder composes a pattern string, which the formatter and parse-with-pattern
already speak, so each append* maps to the letters meaning the same thing:
appendFraction to S's behind an optional decimal point, appendOffset to the
generic X (every JVM offset pattern is the same run of sign/digits/colons to this
parser), optionalStart/End to the pattern language's own [ … ]. parseDefaulting
records the fallback and returns the builder; parse-with-pattern already starts
each field at the JVM's default.

Optional sections needed real support in the parser: [ … ] used to fall through
to the plain-literal arm, which consumed an input character per bracket and threw
the walk out of alignment. A section is now attempted and, if a literal does not
match or a numeric field finds no digits, skipped whole. Sections nest, since
appendFraction with a zero minimum brackets itself and callers wrap it again.
That strictness applies only inside a section — at the top level literals stay
lenient, so patterns that parsed before parse identically.

Two fixes fell out of testing it against reference java.time:
- .parse on an offset-bearing pattern now returns an OffsetDateTime, so the
  offset survives into (Instant/from (.parse f s)). It was returning a
  LocalDateTime, which read "…T13:45:30+0200" as 13:45 UTC instead of 11:45.
- a fraction scales by the digits actually present, not the pattern's width, so
  a 9-wide field given ".417" is milliseconds rather than nanoseconds.

ChronoField is deliberately not registered here: jolt core already models it as a
real enum, and re-registering the statics shadowed it with something weaker.

Gate: 6 assertions in fmt_test, values certified against JDK 20. 102 tests,
1043 assertions, no regression (1037 before, same 99 tests).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant