Java
dtrexp-java implements DTRExp in Java. Its scope is parsing, validation and coverage evaluation — the spec’s core interface; rendering, description and RRULE export are out of scope, the reference implementation has them. Pure Java 17+, zero dependencies (java.time for IANA zones), driven by the shared conformance vectors.
Install
Section titled “Install”<dependency> <groupId>io.onury</groupId> <artifactId>dtrexp</artifactId> <version>1.0.1</version></dependency>Gradle: implementation("io.onury:dtrexp:1.0.1"). Or skip the build tool entirely — zero dependencies, so building from source is just ./run.sh (compiles the sources under src/ and runs the conformance suite).
Quick Start
Section titled “Quick Start”import io.onury.dtrexp.DTRExp;import java.time.Instant;
DTRExp dtr = DTRExp.parse("T0900:1800 E1:5"); // Mon–Fri, 09:00–18:00// throws a positioned DTRExpParseException on a syntax or static-validity error
boolean open = dtr.covers(Instant.now(), "Europe/Berlin");// —> true on a weekday, 09:00–18:00 Berlin local timeYou parse once (at write/config time) and evaluate many. A DTRExp is immutable after parse and safe to share across threads; covers is a single calendar-field extraction followed by integer comparisons, with no occurrence iteration. toString() returns the source expression verbatim.
Examples
Section titled “Examples”An access-control grant that only applies during business hours; the expression lives in the grant as data, and the check runs on every request:
import io.onury.dtrexp.DTRExp;import java.time.Instant;
// grant.scope = "T0900:1800 E1:5" — stored in your DB / ACLDTRExp scope = DTRExp.parse(grant.scope());
boolean allowed = scope.covers(Instant.now(), user.zone());// —> true inside Mon–Fri 09:00–18:00 in the user's zone, false outside itif (!allowed) { throw new ForbiddenException("outside the permitted window");}The zone is an evaluation parameter, never part of the expression. Three covers overloads take it three ways:
dtr.covers(instant); // UTC — the default zonedtr.covers(instant, ZoneId.of("Asia/Tokyo")); // a preloaded ZoneIddtr.covers(instant, "Asia/Tokyo"); // an IANA identifierValidation with the spec’s unsatisfiability lint; D30 M2 parses but can never match, and validate tells you so instead of failing silently:
ValidationResult res = DTRExp.validate("D30 M2"); // never throwsres.valid(); // —> true — it parsesres.warnings(); // —> [DTRExpWarning{position=…, message="unsatisfiable …"}] // no February has 30 daysErrors and warnings both carry a position; the 0-based character offset into the source, rendered into the message as (at N):
DTRExp.parse("Y*/3"); // anchorless stride — throws DTRExpParseException// e.position() points at the offending characterStatic Methods
Section titled “Static Methods”| Method | Description |
|---|---|
DTRExp.parse(String) |
Parses a DTRExp string into an immutable DTRExp, or throws a DTRExpParseException with a position() on a syntax or static-validity error. The only way to construct a DTRExp. |
DTRExp.validate(String) |
Non-throwing variant. Returns a ValidationResult record; typo-shaped input comes back as data, never an exception. |
DTRExp Instance
Section titled “DTRExp Instance”| Member | Description |
|---|---|
covers(Instant) |
Whether the expression covers the instant, evaluated in UTC. A single calendar-field extraction followed by integer comparisons. |
covers(Instant, ZoneId) |
Same, evaluated in a preloaded ZoneId. |
covers(Instant, String) |
Same, evaluated in a zone given as an IANA identifier. |
warnings() |
The §9.1 unsatisfiability warnings of the parsed expression; a List<DTRExpWarning>, same content as validate(s).warnings(). |
toString() |
The source expression, verbatim. |
Records and Exceptions
Section titled “Records and Exceptions”| Type | Description |
|---|---|
ValidationResult |
Record with valid() boolean, errors() (parsing stops at the first syntax error, so at most one DTRExpParseException) and warnings() List<DTRExpWarning>. |
DTRExpWarning |
Record of (int position, String message). |
DTRExpParseException |
Thrown by parse on invalid input; carries a position() int and the message. |
The zone is always an evaluation parameter, never part of the expression. Its default is UTC. DST is handled per spec §9.3: spring-forward gap times cover nothing; repeated fall-back times are covered on both passes.
Quality
Section titled “Quality”The test suite is driven by the shared vectors.json from the spec repo: every coverage, rejection, warning and quiet vector, including the calendar traps (Feb 29 across 2000/2024/2100, W53 existence, DST gap/overlap in Europe/Berlin). The vectors are vendored at test/resources/vectors.json; see VECTORS.md for how the suite works. The build compiles under javac -Xlint:all -Werror, so a warning fails it. Zero dependencies.
- Repository: DTRExp/dtrexp-java
- Maven Central:
io.onury:dtrexp - Reference implementation with
intersect,next,describeandtoRRule: JavaScript / TypeScript.