Skip to content

Swift

dtrexp-swift is the Swift implementation of DTRExp — a compact string expression for date-time ranges and recurrence, evaluated by coverage rather than enumeration. Its scope is parsing, validation and coverage evaluation (the spec’s core interface); rendering, description and RRULE export are out of scope, and live in the reference implementation. Foundation only, zero dependencies, driven by the shared conformance vectors.

Add the package to your Package.swift:

.package(url: "https://github.com/DTRExp/dtrexp-swift", from: "1.2.0")

Then depend on the DTRExp product from your target:

.product(name: "DTRExp", package: "dtrexp-swift")

Swift 6.0+, Foundation only (TimeZone and Date for IANA zones and instants).

import DTRExp
let dtr = try DTRExp("T0900:1800 E1:5") // business hours, Mon–Fri
let berlin = TimeZone(identifier: "Europe/Berlin")!
dtr.covers(Date(), timeZone: berlin)
// —> true on a weekday, 09:00–18:00 Berlin local time
// By IANA name; throws on an unknown identifier:
let ok = try dtr.covers(Date(), tz: "Europe/Berlin")

DTRExp("…") and the equivalent static DTRExp.parse("…") both parse; parse is the fixed cross-language name for what the initializer does natively.

Note that you parse once (at write/config time) and evaluate many; a DTRExp value is an immutable struct, Sendable, and safe to share across concurrent evaluations. covers is a single calendar-field extraction followed by integer comparisons; no occurrence iteration. The zone is an evaluation parameter, never part of the expression; omit it and evaluation is in UTC.

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 DTRExp
// grant.scope = "T0900:1800 E1:5" — stored in your DB / ACL
let scope = try DTRExp(grant.scope)
func authorize(_ request: Request) throws {
guard try scope.covers(request.now, tz: request.user.timeZone) else {
throw AuthError.forbidden("outside the permitted window")
}
// … proceed
}

Validation with the spec’s unsatisfiability lint; D30 M2 parses but can never match, and the library tells you so instead of failing silently:

let result = DTRExp.validate("D30 M2") // never throws
result.valid // true — it parses
result.warnings // [Warning] — no February has 30 days
for warning in result.warnings {
print(warning)
// —> "unsatisfiable: 'D' selector never matches in any parent instance (at offset 0)"
}

A parse error carries the offending position, caught with a typed catch:

do {
_ = try DTRExp("Y*/3") // anchorless stride — a syntax error
} catch let error { // typed catch — error is a ParseError
error.message // human-readable reason
error.position // 0-based offset of the offending character
}
Member Description
DTRExp(_:) Failable initializer; parses the source into an immutable DTRExp or throws a ParseError. Typed throws(ParseError).
DTRExp.parse(_:) Static equivalent of the initializer; the fixed cross-language name for the parse operation. Typed throws(ParseError).
DTRExp.validate(_:) Non-throwing variant. Returns a ValidationResult; typo-shaped input comes back as data.
Member Description
covers(_:timeZone:) Whether the expression covers the instant, evaluated in the given TimeZone: by default the bound zone, else UTC; a timeZone passed here wins. A single calendar-field extraction followed by integer comparisons. Cannot fail.
covers(_:tz:) Same, by IANA identifier String; throws an EvaluationError for an unknown zone.
withZone(_:) A copy whose evaluation defaults to the given TimeZone, or the zone an IANA identifier names (throwing EvaluationError at once for an unknown, empty or "localtime" one); the original is left as it is, and the copy’s source and warnings are the same.
zone The bound TimeZone?, nil when none is bound.
warnings The §9.1 unsatisfiability warnings of the parsed expression; same content as validate(_:).warnings.
source The original expression, verbatim.
Type Description
ParseError Parse failure: message String, position Int? (0-based offset of the offending character, when known).
ValidationResult valid Bool, errors [ParseError] (parsing stops at the first syntax error, so at most one), warnings [Warning].
Warning A non-fatal finding — the expression parses but can never match: message String, position Int?.
EvaluationError Raised by covers(_:tz:) for an unknown IANA identifier.
  • Instants: a Foundation Date.
  • Time zone: a TimeZone value, or an IANA identifier String via covers(_:tz:). The zone is always an evaluation parameter, never part of the expression; omitted, evaluation is in UTC. DST is handled per spec §9.3: spring-forward gap times cover nothing; repeated fall-back times are covered on both passes.
  • Warnings: the spec’s §9.1 unsatisfiability lint — expressions that parse but can never match. The parsed value’s warnings property and validate(_:).warnings carry the same content.

The test suite is driven by the shared vectors.json from the spec repo, vendored at Tests/DTRExpTests/Resources/vectors.json: 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). Run the suite with swift test; see VECTORS.md for how the vectors are structured. 100% line coverage, and mutation-tested by a scripted exit-code harness that applies the classic mutant classes and treats any nonzero swift test exit as a kill. Zero dependencies.

  • Repository: DTRExp/dtrexp-swift
  • Add via SwiftPM: .package(url: "https://github.com/DTRExp/dtrexp-swift", from: "1.2.0")