Full reference for the Pragmatic.Temporal value types. Every signature on this page is verified
against the current implementation.
The rule of thumb is type = scope:
Type
Has Date
Has Time
Has Zone
Meaning
LocalDate
✅
❌
❌
A calendar date (“June 15th”)
LocalTime
❌
✅
❌
A wall-clock time (“9:00 AM”)
LocalDateTime
✅
✅
❌
A wall-clock date+time, not an instant
ZonedDateTime
✅
✅
✅
An exact instant + the zone it belongs to
Duration
n/a
n/a
n/a
Exact elapsed physical time (24h is always 24h)
Period
n/a
n/a
n/a
Calendar units (years/months/days; “1 month” varies)
DateRange
✅
❌
❌
An inclusive range of dates [Start, End]
CronExpression
n/a
n/a
n/a
A recurrence schedule
There is no LocalDate.Today or static “now” on any type. “Now” always comes from an injected
IClock (clock.Today, clock.UtcNow) so your code stays testable. See Testing.
When you need it: birthdays, invoice dates, check-in dates, deadlines, anything where “the
date” is meaningful on its own and must not shift with timezones. A readonly struct wrapping
DateOnly.
vardate=newLocalDate(2026, 6, 15); // from components
varfromDo=newLocalDate(someDateOnly); // from DateOnly
varfromDt= LocalDate.FromDateTime(dateTime); // date part of a DateTime
varmin= LocalDate.MinValue; // 0001-01-01
varmax= LocalDate.MaxValue; // 9999-12-31
// "today" comes from the clock, never from a static property:
LocalDatetoday= clock.Today; // IClock.Today is DateOnly → implicit conversion
⚠️ LocalDate.FromDateTimeOffset(dto) takes the wall-clock date of the offset value and
ignores the offset itself. For “what date is this instant in zone X?” use
dto.ToLocalDate(zone) from Pragmatic.Temporal.Extensions instead: it converts the UTC
instant into the zone first.
Implicit conversions both ways with DateOnly: you can pass a LocalDate wherever a
DateOnly is expected and vice versa (this is why clock.Today assigns directly).
ToString() → ISO yyyy-MM-dd (invariant). ToString(format) for custom formats.
Parse / TryParse: tries exact yyyy-MM-dd first, then falls back to invariant
DateOnly.TryParse. Prefer feeding it ISO strings only: the fallback accepts other
invariant-culture formats (e.g. 06/15/2026 parses as month/day), which can surprise you.
⚠️ IsBetween delegates to TimeOnly.IsBetween: the start is inclusive, the end is
exclusive, and ranges that cross midnight (e.g. 22:00 → 02:00) are supported.
When you need it: a wall-clock date+time where the zone is context, not data, as in “the meeting
is at 2026-03-21 14:30” (in whatever office you’re in). Internally a DateTime with
Kind.Unspecified.
⚠️ Never use LocalDateTime for instants (things that happened at an exact moment
worldwide). Use ZonedDateTime or DateTimeOffset for those.
dt.ToDateTimeOffset(zone) is DST-safe and consistent with InZone: gaps shift forward,
ambiguous times resolve to standard time by default. Pass explicit policies when you need
different behavior:
Parse/TryParse accept yyyy-MM-ddTHH:mm:ss, with .fff, with a space separator, and
yyyy-MM-ddTHH:mm, plus an invariant fallback.
⚠️ If the input string carries a timezone suffix (Z, +02:00, -05:00), the parser
strips and discards it: a LocalDateTime has no zone to store it in. If the offset
matters, parse a ZonedDateTime or DateTimeOffset instead.
When you need it: an exact instant where the zone is part of the data, such as flight departures,
meeting starts across offices, anything you’ll show in “local time” later. Internally stores the
UTC instant + the TimeZoneInfo.
Constructors are internal: you always go through a factory, which is what makes the type
DST-safe:
z.Offset // TimeSpan: UTC offset at this instant (+01:00 or +02:00 depending on DST)
z.IsDaylightSavingTime
z.LocalDateTime // DateTime: wall clock in the zone
z.Date // LocalDate z.Time // LocalTime
z.LocalDate // DateOnly z.LocalTime // TimeOnly
z.Year; z.Month; z.Day; z.Hour; z.Minute; z.Second; z.DayOfWeek; // all in the zone
Performance note: every local-side getter re-runs the UTC→zone conversion. In a loop that
reads several components, read z.LocalDateTime once into a variable.
The two kinds of arithmetic (the whole point of this type)
ZonedDateTime.Parse("2026-01-15T10:30:00Z"); // no bracket → zone is UTC
ZonedDateTime.TryParse(s, outvarresult);
Parsing rules worth knowing:
With a [Zone] bracket, the zone id must resolve and be ≤ 64 chars.
If the string has both an explicit offset and a [Zone], the offset must be valid for
that zone at that wall time; inconsistent values (wrong offset, or a time inside a DST gap)
are rejected rather than silently reinterpreted.
Without a bracket the value is parsed as a DateTimeOffset and normalized to the UTC
zone; the original offset is not kept as a zone.
When you need it: elapsed physical time, such as timeouts, SLAs, “how long did it take”. 24 hours
is always exactly 24 hours; DST doesn’t exist for Duration. Wraps TimeSpan.
Contrast with Period (calendar units) and with ZonedDateTime.AddDays (wall-clock).
Duration.Parse("-PT30S"); // leading minus for negative
Duration.TryParse(s, outvard);
Limits of the format (by design): days are the largest unit emitted/accepted, with no weeks, no
years/months (those are calendar concepts → Period); H/M/S must appear after the T;
fractional values are accepted (PT0.5H); custom format strings are not supported:
ToString(format, provider) always returns the ISO form.
When you need it: calendar-speak, as in “a 1-month subscription”, “payment due in 30 days”,
“2 years and 3 months”. Components are Years/Months/Days ints; the actual length depends
on the date you apply it to.
Period.FromYears(1); Period.FromMonths(6); Period.FromDays(30); Period.FromWeeks(2); // = 14 days
Period.Zero;
Period.Between(start, end); // calendar difference between two LocalDates
Between computes years, then months, then leftover days (borrowing from the month before the
end date when needed). It negates cleanly when start > end, but because month lengths differ,
Between(a, b) applied back to a lands on b while intermediate components may not be what
you’d guess near month ends; verify with round-trip tests if you do bookkeeping with it.
The parser accepts only P…Y…M…D (case-insensitive, at least one component): no weeks (P2W
is invalid; use FromWeeks), no time part (PT… belongs to Duration).
When you need it: reporting periods, booking spans, “the last 30 days”. A closed,
inclusive range [Start, End] of LocalDates that you can query, combine, and enumerate.
DateRange.LastDays(today, 30); // last 30 days ending at today (inclusive)
DateRange.NextDays(start, 7);
⚠️ Testability: the overloads of ThisWeek(), ThisMonth(), ThisQuarter(), ThisYear()without a date read DateTime.Today (server-local, not injectable) and are
marked [Obsolete]. LastDays(n)/NextDays(n) without a date have the same problem.
In application code always use the overloads that take an explicit today:
r.Days; // count inclusive of both endpoints (Jan 1..Jan 3 → 3)
r.IsSingleDay;
r.IsEmpty; // true only for DateRange.Empty
r.Duration; // Duration.FromDays(Days)
var (s, e) = r; // Deconstruct
DateRange.Empty (also the default of the struct) is empty: Days == 0, it
enumerates nothing, Contains/Overlaps/IsAdjacentTo are always false, and
Union with it returns the other range unchanged.
r.Overlaps(other); r.IsAdjacentTo(other); // touching, no gap, no overlap
r.Intersect(other); // → DateRange.Empty when no overlap
r.Union(other); // smallest range containing BOTH
r.Expand(3); // 3 days wider on each side
r.Shift(7); // move the whole range forward a week
r.Split(30); // lazy chunks of ≤30 days
⚠️ Union returns the bounding span: for disjoint ranges it silently includes the gap
between them ([Jan 1-5] ∪ [Jan 20-25] → [Jan 1-25]). If you need “only the covered days”,
keep the ranges separate.
When you need it: recurring schedules, such as “every day at 9”, “last day of the month”, “second
Tuesday”. Zero-dependency implementation; a sealed class (not a struct).
until: endOfYear, // inclusive; null → until maxOccurrences
zone: romeZone,
maxOccurrences: 1000); // default cap
boolhits= cron.Matches(someInstant, romeZone);
Results are returned normalized to UTC, but matching happens on the wall clock of the
given zone (default UTC).
GetNextOccurrence returns null if nothing matches within ~4 years of search.
DST-aware: wall times inside a spring-forward gap are skipped; during a fall-back overlap
the earliest instant strictly after from is chosen, so GetOccurrences never stalls or
double-fires.
There is no GetNextOccurrences(from, count); use GetOccurrences(...).Take(count).
using Pragmatic.Temporal.Extensions; gives the correct bridge from an instant to local types:
DateTimeOffsetinstant= clock.UtcNow;
instant.ToLocalDate("Europe/Rome"); // what date is it in Rome right now?
instant.ToLocalTime(romeZone); // what time is it in Rome?
instant.ToLocalDateTime("Asia/Tokyo"); // wall-clock datetime in Tokyo
Each converts the UTC instant into the target zone (string overloads resolve IANA or
Windows ids via TimeZoneResolver, throwing TimeZoneNotFoundException for unknown ids). This
is what you want instead of LocalDate.FromDateTimeOffset (which ignores the offset).
When you need it: showing a temporal value to a user in their culture (“21/03/2026” for
Italian, “3/21/2026” for US English) instead of the fixed ISO wire format.
What you write:LocalDate and LocalTime already work with the
Pragmatic.Internationalization formatting APIs: they convert implicitly to
DateOnly/TimeOnly. For LocalDateTime and ZonedDateTime, add the bridge package:
=>$"{formatter.FormatDateTime(start)}, reply by {formatter.FormatDate(deadline)}";
}
// Or with the ambient I18N culture context (mirrors I18n's DateExtensions):
varlabel= webinarStart.FormatDateTime(); // ZonedDateTime → wall time in ITS zone
What you get:LocalDateTime formats its wall time as-is (no timezone conversion);
ZonedDateTime formats the wall time in its own zone: the same instant shown for a
Rome-zoned and a UTC-zoned value renders differently, which is exactly what the type means
for display. All members delegate to the Internationalization APIs; no formatting logic is
duplicated. Full formatting reference: the Pragmatic.Internationalization module docs.