Skip to content

Adds decoding and the validity checks - #6

Merged
johnnyt merged 1 commit into
mainfrom
uxjv-nyt-decode-and-checks
Oct 21, 2026
Merged

johnnyt merged 1 commit into
mainfrom
uxjv-nyt-decode-and-checks

Conversation

@johnnyt

@johnnyt johnnyt commented Oct 21, 2026

Copy link
Copy Markdown
Member

Why

The package could generate ids but not read them back. A caller needs to decode an id to what it says about itself, and to ask the two common questions of a string (is it a well-formed id, is it a deterministic one) without handling an exception for ordinary malformed input.

What

  • Uxid.decode(String) and Uxid.decode(String, DecodeOptions) return a sealed DecodeResult: a DecodeResult.Decoded record (prefix, size, compact flag, deterministic flag, time in milliseconds) or a DecodeResult.Invalid record whose InvalidReason has a wireName() of empty, invalid_length or invalid_character. Malformed input is answered, never thrown; the refusals are checked in the wire format's order, with the body length counted in UTF-8 bytes.
  • DecodeOptions (a builder, like UxidOptions) takes the delimiter and a java.time.Clock; the default is _ and Clock.systemUTC(), read inside the call and only for a compact body.
  • A compact time takes the clock's bits above bit 40 and steps back one 2^40 ms window when the result is more than one day ahead of the clock, with no clamp at zero: a clock in the first window after the epoch gives a negative time, and a clock before the epoch is in the window ending at the epoch (the window is the clock's rounded down).
  • The body lengths 10 and 18 always decode as standard time, as the wire format states.
  • Uxid.isValid(id) and Uxid.isValid(id, delimiter, requiredPrefix); Uxid.isDeterministic(id) and Uxid.isDeterministic(id, delimiter). Each is "decode answers a Decoded" (plus the prefix match, or the marker); neither reads a clock.

The Java API shape (static methods on Uxid, the sealed result of two records, the InvalidReason enum, the clock a java.time.Clock) was decided by the conductor under a standing consent, 2026-10-21.

Tests

  • Every decode.json case and every decode refusal of invalid.json from the uxid-ex v2.10.0 corpus, each named by its case id. The decode cases run at both ends of the corpus clock window.
  • The window ends are computed from the corpus README's arithmetic (one day before 1700000000000 ms; 2^40 + 1065151889407 - one day) and asserted equal to the README's 1699913600000 ms and 2164577117183 ms; the millisecond before the start misses a case and the end itself misses the guard case.
  • A negative compact time is pinned (a clock at 0 ms reads usr_hf7yat0004hm5 as -499023255552 ms), as is a clock before the epoch and a clock that is never read for a standard, deterministic or refused body.
  • Every new test carries an in-name sabotage note for a mutation that was run, turned the test red on an assertion, and was reverted.

Notes

Choices the bead did not fix, each taken as the reference or the spec reads:

  • A null id, delimiter or options throws NullPointerException, the Java convention; an empty delimiter throws IllegalArgumentException from DecodeOptions.Builder.delimiter, isValid and isDeterministic, as UxidOptions already does. Only the string being decoded is "malformed input".
  • Decoded holds no bytes: the wire format says a decoder does not report the random field, so there is no array component and no equals or hashCode concern. The prefix is an Optional<String> and the time an OptionalLong, so the record carries no nulls; its constructor refuses null components.
  • The split is the reference's: the last delimiter a left-to-right scan finds, matches not overlapping, so under -- the string usr---01hf7yat00 splits after usr. An id that starts with the delimiter has the prefix "", as the spec says.
  • A null or empty required prefix accepts any prefix or none, as the reference's valid? does. isValid and isDeterministic require a well-formed body (the reference's deterministic? reads only the first body character); this matches the released ports.
  • An unpaired surrogate counts as three UTF-8 bytes, the width of U+FFFD.

Gate: mise run gate green on this head (format check, compile with Error Prone, tests, JaCoCo, the javadoc and sources jars).

Closes uxjv-nyt

Uxid.decode reads an id back to a sealed DecodeResult: a Decoded record
with the prefix, size, time form, deterministic flag and time, or an
Invalid record whose InvalidReason carries the wire format's refusal
name. Malformed input is answered, never thrown, and the refusals are
checked in the spec's order: empty, invalid_length, invalid_character,
with the length counted in UTF-8 bytes.

DecodeOptions takes the delimiter and a java.time.Clock. A compact time
takes its high bits from the clock and steps back one 2^40 ms window
when it would be more than a day ahead, with no clamp at zero, so a
clock in the first window gives a negative time. The tests pin both
ends of the corpus clock window from the corpus README's arithmetic.

Uxid.isValid takes a delimiter and an optional required prefix and
Uxid.isDeterministic takes a delimiter; neither reads a clock.

Refs: uxjv-nyt
@johnnyt
johnnyt merged commit 4f3e509 into main Oct 21, 2026
4 checks passed
@johnnyt
johnnyt deleted the uxjv-nyt-decode-and-checks branch October 21, 2026 22:21
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