morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
datetime.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5/// @file util/datetime.hpp
6/// @brief UTC timestamps for morph actions.
7///
8/// Two types, mirroring the `Rational`/`Quantity` split:
9///
10/// - `morph::time::DateTime` — the *value*: a UTC instant with millisecond
11/// precision over `std::chrono::sys_time` (adapted from LASTRADA
12/// `Toolbox/Chrono.hpp`). Cheap to copy, ordered, duration arithmetic.
13/// - `morph::time::Timestamp` — the *field*: an optionally-empty `DateTime`
14/// with the same one-kind-of-empty semantics as `Quantity` (a form draft
15/// starts blank; required-ness is derived by `morph::forms`).
16///
17/// @par Wire format
18/// A `DateTime` travels as the ISO-8601 string
19/// `"YYYY-MM-DDTHH:MM:SS[.mmm]Z"` (UTC only; the fractional part and the
20/// trailing `Z` are optional on input, always written on output). Parsing is
21/// a strict, hand-rolled routine — no locale, no `std::chrono::parse`
22/// dependency — so behaviour is identical across libstdc++, libc++ (WASM),
23/// and MSVC. Unlike the `Rational` codec, which clamps hostile input into a
24/// valid value, a malformed timestamp has no meaningful clamp: the codec
25/// rejects it as a JSON **read error**.
26///
27/// @par Schema
28/// A `Timestamp` field renders as `{"type": ["string","null"],
29/// "format": "date-time"}` — the standard JSON-Schema vocabulary form
30/// renderers key on (the demo clients render a date-time input for it).
31
32#include <charconv>
33#include <chrono>
34#include <compare>
35#include <cstdint>
36#include <format>
37#include <functional>
38#include <glaze/glaze.hpp>
39#include <morph/attributes.hpp>
41#include <mutex>
42#include <optional>
43#include <string>
44#include <string_view>
45#include <utility>
46
47namespace morph::time {
48
49struct DateTime;
50
51namespace detail {
52
53/// @brief Process-wide override for `DateTime::now()`, guarded by a mutex.
54///
55/// Mirrors `morph::journal::detail::defaultActionLogState()` — the same
56/// "mutex-guarded function-local static" shape used for the action-log and
57/// logger override seams elsewhere in the codebase. An empty `std::function`
58/// (the default) means "no override installed"; `DateTime::now()` falls back
59/// to `std::chrono::system_clock::now()` in that case. Forward-declared here
60/// (before `DateTime` is a complete type -- a function signature only needs
61/// `DateTime` to be declared, not defined) and defined out-of-line just after
62/// the `DateTime` struct, before `DateTime::now()`'s first caller needs it.
63[[nodiscard]] std::pair<std::mutex, std::function<DateTime()>>& nowOverrideState();
64
65} // namespace detail
66
67/// @brief A UTC instant with millisecond precision.
68///
69/// The default state is the Unix epoch — `DateTime` itself has no empty
70/// state; use `Timestamp` for fields that may be blank.
71struct DateTime {
72 /// @brief The underlying UTC instant (Unix time, leap seconds ignored).
73 std::chrono::sys_time<std::chrono::milliseconds> value;
74
75 /// @brief Constructs the Unix epoch (1970-01-01T00:00:00.000Z).
76 constexpr DateTime() noexcept = default;
77
78 /// @brief Wraps an existing UTC instant.
79 /// @param instant The UTC instant to wrap.
80 constexpr DateTime(std::chrono::sys_time<std::chrono::milliseconds> instant) noexcept : value{instant} {}
81
82 /// @brief Composes a UTC instant from calendar/clock components.
83 /// @param year Calendar year.
84 /// @param month Month-of-year in [1, 12].
85 /// @param day Day-of-month in [1, 31].
86 /// @param hour Hours in [0, 23].
87 /// @param minute Minutes in [0, 59].
88 /// @param second Seconds in [0, 59].
89 /// @param millisecond Milliseconds in [0, 999].
90 constexpr DateTime(std::chrono::year year, std::chrono::month month, std::chrono::day day, std::chrono::hours hour,
91 std::chrono::minutes minute, std::chrono::seconds second,
92 std::chrono::milliseconds millisecond = std::chrono::milliseconds{0}) noexcept
93 : value{static_cast<std::chrono::sys_days>(std::chrono::year_month_day{year, month, day}) + hour + minute +
94 second + millisecond} {}
95
96 /// @brief The current UTC instant, truncated to milliseconds.
97 ///
98 /// Consults the process-wide clock override installed via `setNowOverride`/
99 /// `ScopedNowOverride`, if any; otherwise calls
100 /// `std::chrono::system_clock::now()` directly. This is the seam that lets
101 /// registry-constructed (remotely instantiated) models get a deterministic
102 /// "now" in tests without any constructor parameter: production code calls
103 /// `DateTime::now()` exactly as before, and a test installs an override for
104 /// its scope. See `ScopedNowOverride`.
105 ///
106 /// @warning This function is `noexcept`. If an installed override callable
107 /// throws, that exception escapes a `noexcept` function and `std::terminate()`
108 /// is called — an override must not throw. The override also runs while the
109 /// internal mutex is held, so it must not itself call `DateTime::now()` /
110 /// `Timestamp::now()`, `setNowOverride`, or construct a `ScopedNowOverride`:
111 /// the mutex is non-recursive and any of those would self-deadlock (the same
112 /// constraint `morph::log`'s sink callback documents for the same reason).
113 /// @return The current UTC date-time, or the overridden instant if one is installed.
114 [[nodiscard]] static DateTime now() noexcept {
115 auto& [mtx, overrideFn] = detail::nowOverrideState();
116 {
117 std::scoped_lock const lock{mtx};
118 if (overrideFn) {
119 return overrideFn();
120 }
121 }
122 return DateTime{std::chrono::time_point_cast<std::chrono::milliseconds>(std::chrono::system_clock::now())};
123 }
124
125 /// @brief Renders as ISO-8601 UTC: `YYYY-MM-DDTHH:MM:SS.mmmZ`.
126 /// @return The formatted instant.
127 [[nodiscard]] std::string toIso8601() const {
128 auto const wholeDays = std::chrono::floor<std::chrono::days>(value);
129 auto const calendar = std::chrono::year_month_day{wholeDays};
130 auto const clock = std::chrono::hh_mm_ss{value - wholeDays};
131 return std::format("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z", static_cast<int>(calendar.year()),
132 static_cast<unsigned>(calendar.month()), static_cast<unsigned>(calendar.day()),
133 clock.hours().count(), clock.minutes().count(), clock.seconds().count(),
134 clock.subseconds().count());
135 }
136
137 /// @brief Strictly parses ISO-8601 UTC: `YYYY-MM-DDTHH:MM:SS`, optionally
138 /// followed by `.m`/`.mm`/`.mmm` and an optional trailing `Z`.
139 ///
140 /// The calendar date must exist (2026-02-30 is rejected) and clock
141 /// components must be in range; no time zones other than `Z`, no
142 /// lowercase separators, no trailing input. The fixed-width date and clock
143 /// fields (month, day, hour, minute, second, fraction) reject a leading
144 /// sign; only the year field may carry a `-` (negative years).
145 /// @param text Candidate ISO-8601 string.
146 /// @return The parsed instant, or `std::nullopt` when @p text is malformed.
147 // NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic, cppcoreguidelines-pro-bounds-avoid-unchecked-container-access)
148 [[nodiscard]] static std::optional<DateTime> fromIso8601(std::string_view text) noexcept {
149 // Callers only pass offsets within the length-checked prefix below.
150 // `allowSign` gates the leading '-'/'+' that `std::from_chars` would
151 // otherwise accept: the whole-string year is the only field where a
152 // sign is legitimate (negative years). For the fixed-width date/clock
153 // fields a leading sign is a sign-injection attack — "T-5:30:15" would
154 // read hour = -5 and silently shift to a different valid instant — so
155 // those callers reject it and the value is forced non-negative.
156 auto const number = [&text](std::size_t offset, std::size_t count,
157 bool allowSign) noexcept -> std::optional<int> {
158 if (!allowSign && count > 0 && offset < text.size() && (text[offset] == '-' || text[offset] == '+')) {
159 return std::nullopt;
160 }
161 int parsed = 0;
162 auto const* first = text.data() + offset;
163 auto const [end, errc] = std::from_chars(first, first + count, parsed);
164 if (errc != std::errc{} || end != first + count) {
165 return std::nullopt;
166 }
167 return parsed;
168 };
169 auto const separator = [&text](std::size_t offset, char expected) noexcept {
170 return offset < text.size() && text[offset] == expected;
171 };
172
173 if (text.size() < 19 || !separator(4, '-') || !separator(7, '-') || !separator(10, 'T') ||
174 !separator(13, ':') || !separator(16, ':')) {
175 return std::nullopt;
176 }
177 auto const year = number(0, 4, /*allowSign=*/true);
178 auto const month = number(5, 2, /*allowSign=*/false);
179 auto const day = number(8, 2, /*allowSign=*/false);
180 auto const hour = number(11, 2, /*allowSign=*/false);
181 auto const minute = number(14, 2, /*allowSign=*/false);
182 auto const second = number(17, 2, /*allowSign=*/false);
183 if (!year || !month || !day || !hour || !minute || !second) {
184 return std::nullopt;
185 }
186
187 std::size_t cursor = 19;
188 int milliseconds = 0;
189 if (separator(cursor, '.')) {
190 ++cursor;
191 std::size_t digits = 0;
192 int fraction = 0;
193 while (digits < 3 && cursor < text.size() && text[cursor] >= '0' && text[cursor] <= '9') {
194 fraction = (fraction * 10) + (text[cursor] - '0');
195 ++cursor;
196 ++digits;
197 }
198 if (digits == 0) {
199 return std::nullopt;
200 }
201 for (std::size_t pad = digits; pad < 3; ++pad) {
202 fraction *= 10;
203 }
204 milliseconds = fraction;
205 }
206 if (separator(cursor, 'Z')) {
207 ++cursor;
208 }
209 if (cursor != text.size()) {
210 return std::nullopt;
211 }
212
213 auto const calendar =
214 std::chrono::year_month_day{std::chrono::year{*year}, std::chrono::month{static_cast<unsigned>(*month)},
215 std::chrono::day{static_cast<unsigned>(*day)}};
216 if (!calendar.ok() || *hour > 23 || *minute > 59 || *second > 59) {
217 return std::nullopt;
218 }
219 return DateTime{std::chrono::year{*year},
220 std::chrono::month{static_cast<unsigned>(*month)},
221 std::chrono::day{static_cast<unsigned>(*day)},
222 std::chrono::hours{*hour},
223 std::chrono::minutes{*minute},
224 std::chrono::seconds{*second},
225 std::chrono::milliseconds{milliseconds}};
226 }
227 // NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic, cppcoreguidelines-pro-bounds-avoid-unchecked-container-access)
228
229 /// @brief Ordering on the underlying instant.
230 /// @param other Instant to compare against.
231 /// @return The relative ordering of the two instants.
232 [[nodiscard]] constexpr std::strong_ordering operator<=>(const DateTime& other) const noexcept = default;
233
234 /// @brief Shifts this instant by an arbitrary chrono duration.
235 /// @param delta Duration to add (may be negative).
236 /// @return `*this`.
237 template <typename Rep, typename Period>
238 constexpr DateTime& operator+=(std::chrono::duration<Rep, Period> delta) noexcept {
239 value += std::chrono::duration_cast<std::chrono::milliseconds>(delta);
240 return *this;
241 }
242
243 /// @brief Shifts this instant backwards by an arbitrary chrono duration.
244 /// @param delta Duration to subtract (may be negative).
245 /// @return `*this`.
246 template <typename Rep, typename Period>
247 constexpr DateTime& operator-=(std::chrono::duration<Rep, Period> delta) noexcept {
248 value -= std::chrono::duration_cast<std::chrono::milliseconds>(delta);
249 return *this;
250 }
251
252 /// @brief An instant @p delta after @p lhs.
253 /// @param lhs Starting instant.
254 /// @param delta Duration offset.
255 /// @return The shifted instant.
256 template <typename Rep, typename Period>
257 [[nodiscard]] friend constexpr DateTime operator+(DateTime lhs,
258 std::chrono::duration<Rep, Period> delta) noexcept {
259 lhs += delta;
260 return lhs;
261 }
262
263 /// @brief An instant @p delta before @p lhs.
264 /// @param lhs Starting instant.
265 /// @param delta Duration offset.
266 /// @return The shifted instant.
267 template <typename Rep, typename Period>
268 [[nodiscard]] friend constexpr DateTime operator-(DateTime lhs,
269 std::chrono::duration<Rep, Period> delta) noexcept {
270 lhs -= delta;
271 return lhs;
272 }
273
274 /// @brief The signed difference between two instants.
275 /// @param lhs Minuend.
276 /// @param rhs Subtrahend.
277 /// @return `lhs - rhs` as a chrono duration.
278 [[nodiscard]] friend constexpr auto operator-(const DateTime& lhs, const DateTime& rhs) noexcept {
279 return lhs.value - rhs.value;
280 }
281};
282
283namespace detail {
284
285/// @brief Function-local static backing `DateTime::now()`'s override seam.
286///
287/// Defined out-of-line (after `DateTime` is a complete type) since the state
288/// is keyed on `std::function<DateTime()>`.
289/// @return Reference to the process-wide `(mutex, override callable)` pair;
290/// the callable is empty when no override is installed.
291inline std::pair<std::mutex, std::function<DateTime()>>& nowOverrideState() {
292 static std::pair<std::mutex, std::function<DateTime()>> state;
293 return state;
294}
295
296} // namespace detail
297
298/// @brief Installs @p clock as the process-wide override for `DateTime::now()`
299/// (and therefore `Timestamp::now()`, which delegates to it).
300///
301/// Every call to `DateTime::now()` — including inside a registry-constructed
302/// (remotely instantiated) model that has no constructor parameter to receive
303/// an injected clock — consults this override before falling back to
304/// `std::chrono::system_clock::now()`. This is the seam
305/// `docs/spec/util/datetime.md` describes for deterministic
306/// testing of time-dependent model behavior: a test fixes "now" once via
307/// `ScopedNowOverride` and every `DateTime::now()`/`Timestamp::now()` call for
308/// its lifetime — direct or from inside a model under test — observes the
309/// fixed instant. Thread-safe.
310///
311/// @warning @p clock must not throw (`DateTime::now()` is `noexcept` and calls
312/// it directly — an exception escaping @p clock terminates the process) and
313/// must not call `DateTime::now()`/`Timestamp::now()`, `setNowOverride`, or
314/// construct a `ScopedNowOverride` itself: `now()` holds a non-recursive mutex
315/// while invoking @p clock, so any of those self-deadlocks.
316/// @param clock Callable returning the instant `DateTime::now()` should report,
317/// or an empty `std::function` to clear the override (restore
318/// real wall-clock time).
319inline void setNowOverride(std::function<DateTime()> clock) {
320 auto& [mtx, slot] = detail::nowOverrideState();
321 std::scoped_lock const lock{mtx};
322 slot = std::move(clock);
323}
324
325/// @brief RAII helper that fixes `DateTime::now()` to a constant instant (or a
326/// custom callable) for its lifetime and restores the previous
327/// override on destruction.
328///
329/// Mirrors `morph::journal::ScopedActionLog` and `morph::log::ScopedLoggerOverride`
330/// — the same scoped-install-then-restore pattern used for the codebase's other
331/// process-wide override seams, so one test's fixed "now" never leaks into the
332/// next.
333///
334/// @code
335/// {
336/// morph::time::ScopedNowOverride guard{someFixedInstant};
337/// // ... DateTime::now()/Timestamp::now(), anywhere, return someFixedInstant ...
338/// } // previous override (or real wall-clock time) restored here
339/// @endcode
341public:
342 /// @brief Installs a clock that always returns @p fixedInstant.
343 /// @param fixedInstant The constant instant `DateTime::now()` should report for this scope.
344 explicit ScopedNowOverride(DateTime fixedInstant) : ScopedNowOverride{[fixedInstant] { return fixedInstant; }} {}
345
346 /// @brief Installs an arbitrary clock callable, saving whatever override was there before.
347 ///
348 /// @warning See `setNowOverride`'s warning: @p clock must not throw and must
349 /// not call `DateTime::now()`/`Timestamp::now()` or install another override.
350 /// @param clock Callable returning the instant `DateTime::now()` should report for this scope.
351 explicit ScopedNowOverride(std::function<DateTime()> clock) {
352 auto& [mtx, slot] = detail::nowOverrideState();
353 std::scoped_lock const lock{mtx};
354 _previous = std::move(slot);
355 slot = std::move(clock);
356 }
357
358 /// @brief Restores the saved override (or "no override", if none was installed before).
359 ~ScopedNowOverride() { setNowOverride(std::move(_previous)); }
360
361 ScopedNowOverride(const ScopedNowOverride&) = delete;
362 ScopedNowOverride& operator=(const ScopedNowOverride&) = delete;
364 ScopedNowOverride& operator=(ScopedNowOverride&&) = delete;
365
366private:
367 std::function<DateTime()> _previous;
368};
369
370/// @brief An optionally-empty timestamp field.
371///
372/// The blank state ("not entered / not recorded") lives inside, exactly like
373/// `morph::units::Quantity`: action structs never wrap a `Timestamp` in
374/// `std::optional`, and a non-optional `Timestamp` member is *required* by
375/// the `morph::forms` rules.
376struct Timestamp {
377 /// @brief The payload; `std::nullopt` means "not entered".
378 std::optional<DateTime> value;
379
380 /// @brief Constructs the empty state.
381 constexpr Timestamp() noexcept = default;
382
383 /// @brief Engages with @p instant.
384 /// @param instant The instant to hold.
385 constexpr Timestamp(DateTime instant) noexcept : value{instant} {}
386
387 /// @brief Adopts an optional payload as-is.
388 /// @param payload Engaged or empty payload.
389 constexpr Timestamp(std::optional<DateTime> payload) noexcept : value{payload} {}
390
391 /// @brief An engaged timestamp holding the current UTC instant.
392 /// @return `Timestamp{DateTime::now()}`.
393 [[nodiscard]] static Timestamp now() noexcept { return Timestamp{DateTime::now()}; }
394
395 /// @brief Whether an instant has been entered.
396 /// @return `true` if the payload is engaged.
397 [[nodiscard]] constexpr bool hasValue() const noexcept { return value.has_value(); }
398
399 /// @brief Unchecked access to the engaged instant (UB when empty, exactly
400 /// like `std::optional` — the unchecked contract is the point).
401 /// @return The engaged instant — a reference into this `Timestamp`, valid
402 /// only for as long as it is.
403 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
404 [[nodiscard]] constexpr const DateTime& operator*() const noexcept MORPH_LIFETIMEBOUND { return *value; }
405
406 /// @brief Ordering/equality on the payload; empty sorts before engaged.
407 /// @param other Timestamp to compare against.
408 /// @return The ordering of the two payloads.
409 [[nodiscard]] constexpr auto operator<=>(const Timestamp& other) const noexcept = default;
410};
411
412/// @brief The signed duration between two engaged timestamps.
413/// @param lhs Minuend.
414/// @param rhs Subtrahend.
415/// @return `lhs - rhs` as a chrono duration, or `std::nullopt` if either is empty.
416[[nodiscard]] constexpr std::optional<std::chrono::milliseconds> operator-(const Timestamp& lhs,
417 const Timestamp& rhs) noexcept {
418 if (!lhs.value || !rhs.value) {
419 return std::nullopt;
420 }
421 return *lhs.value - *rhs.value;
422}
423
424} // namespace morph::time
425
426// ---------------------------------------------------------------------------
427// Glaze wire codec: ISO-8601 string, strict — malformed input is a read error.
428// ---------------------------------------------------------------------------
429
430namespace glz {
431
432/// @brief Reads a `DateTime` from a JSON ISO-8601 string; malformed content
433/// sets a syntax error on the context (it is not clampable input).
434template <>
435struct from<JSON, morph::time::DateTime> {
436 template <auto Opts>
437 static void op(auto&& value, is_context auto&& ctx, auto&& iter, auto end) {
438 std::string str{};
439 parse<JSON>::op<Opts>(str, ctx, iter, end);
440 if (bool(ctx.error)) [[unlikely]] {
441 return;
442 }
443 if (auto parsed = morph::time::DateTime::fromIso8601(str); parsed.has_value()) {
444 value = *parsed;
445 return;
446 }
447 ctx.error = error_code::syntax_error;
448 }
449};
450
451/// @brief Writes a `DateTime` as its ISO-8601 JSON string.
452template <>
453struct to<JSON, morph::time::DateTime> {
454 template <auto Opts>
455 static void op(auto&& value, is_context auto&& ctx, auto&&... args) {
456 serialize<JSON>::op<Opts>(value.toIso8601(), ctx, args...);
457 }
458};
459
460/// @brief On the wire a Timestamp is its nullable ISO-8601 string.
461template <>
462struct meta<morph::time::Timestamp> {
463 static constexpr auto value = &morph::time::Timestamp::value;
464 static constexpr std::string_view name = "Timestamp";
465};
466
467} // namespace glz
468
469namespace glz::detail {
470
471/// @brief Schema for `DateTime`: a string carrying the standard JSON-Schema
472/// `date-time` format annotation (renderers key on it).
473template <>
474struct to_json_schema<morph::time::DateTime> {
475 template <auto Opts>
476 static void op(auto& outSchema, auto& defs) {
477 to_json_schema<std::string>::template op<Opts>(outSchema, defs);
478 outSchema.format = defined_formats::datetime;
479 }
480};
481
482} // namespace glz::detail
483
484/// @brief Stable shape tag for `DateTime`.
485///
486/// Its codec is the hand-written `glz::from`/`glz::to` pair above (an ISO-8601
487/// string), so `payloadShape` has no reflected members to decompose and would
488/// otherwise render the field as the bare opaque tag — indistinguishable from
489/// a `Rational` or a `Timestamp` field, which is the retype
490/// `journal::replay()`'s mismatch gate could not see. See
491/// `morph/core/payload_shape_tag.hpp`.
492template <>
494 /// @brief This type's stable shape name.
495 /// @return `"datetime"`.
496 static constexpr std::string_view name() noexcept { return "datetime"; }
497};
498
499/// @brief Stable shape tag for `Timestamp`.
500///
501/// Distinct from `DateTime`'s: the two are the pair most easily swapped for
502/// one another, and on the wire both are a single scalar, so nothing but this
503/// name separates them in a fingerprint. See
504/// `morph/core/payload_shape_tag.hpp`.
505template <>
507 /// @brief This type's stable shape name.
508 /// @return `"timestamp"`.
509 static constexpr std::string_view name() noexcept { return "timestamp"; }
510};
511
512// ---------------------------------------------------------------------------
513// std::format support.
514// ---------------------------------------------------------------------------
515
516/// @brief std::format support for `morph::time::DateTime` (empty spec only):
517/// renders the ISO-8601 UTC form.
518template <>
519struct std::formatter<morph::time::DateTime> {
520 // NOLINTBEGIN(readability-convert-member-functions-to-static) — std::formatter requires non-static methods
521 constexpr auto parse(std::format_parse_context& ctx) {
522 // The parse context always contains the terminating '}' (the format
523 // machinery's contract), so the range is never empty here.
524 if (*ctx.begin() != '}') {
525 throw std::format_error{"morph::time::DateTime accepts only the empty format spec '{}'"};
526 }
527 return ctx.begin();
528 }
529
530 auto format(const morph::time::DateTime& value, std::format_context& ctx) const {
531 return std::format_to(ctx.out(), "{}", value.toIso8601());
532 }
533};
534// NOLINTEND(readability-convert-member-functions-to-static)
Handle< std::string > format(AnyHandle const &pattern, std::vector< AnyHandle > args)
format: a template with {0}, {1}, … placeholders.
Definition author.hpp:970
RAII helper that fixes DateTime::now() to a constant instant (or a custom callable) for its lifetime ...
Definition datetime.hpp:340
~ScopedNowOverride()
Restores the saved override (or "no override", if none was installed before).
Definition datetime.hpp:359
ScopedNowOverride(DateTime fixedInstant)
Installs a clock that always returns fixedInstant.
Definition datetime.hpp:344
ScopedNowOverride(std::function< DateTime()> clock)
Installs an arbitrary clock callable, saving whatever override was there before.
Definition datetime.hpp:351
std::pair< std::mutex, std::function< DateTime()> > & nowOverrideState()
Process-wide override for DateTime::now(), guarded by a mutex.
Definition datetime.hpp:291
constexpr std::optional< std::chrono::milliseconds > operator-(const Timestamp &lhs, const Timestamp &rhs) noexcept
The signed duration between two engaged timestamps.
Definition datetime.hpp:416
void setNowOverride(std::function< DateTime()> clock)
Installs clock as the process-wide override for DateTime::now() (and therefore Timestamp::now(),...
Definition datetime.hpp:319
@ DateTime
An instant, entered in the display zone, sent as ISO-8601 UTC.
Opt-in, compiler-independent shape tag for a type with a custom Glaze codec.
static constexpr std::string_view name() noexcept
This type's stable shape name.
Definition datetime.hpp:496
static constexpr std::string_view name() noexcept
This type's stable shape name.
Definition datetime.hpp:509
Declares a stable shape tag for a type T whose JSON shape payloadShape cannot decompose.
A UTC instant with millisecond precision.
Definition datetime.hpp:71
static DateTime now() noexcept
The current UTC instant, truncated to milliseconds.
Definition datetime.hpp:114
constexpr DateTime() noexcept=default
Constructs the Unix epoch (1970-01-01T00:00:00.000Z).
friend constexpr DateTime operator+(DateTime lhs, std::chrono::duration< Rep, Period > delta) noexcept
An instant delta after lhs.
Definition datetime.hpp:257
friend constexpr auto operator-(const DateTime &lhs, const DateTime &rhs) noexcept
The signed difference between two instants.
Definition datetime.hpp:278
std::string toIso8601() const
Renders as ISO-8601 UTC: YYYY-MM-DDTHH:MM:SS.mmmZ.
Definition datetime.hpp:127
constexpr DateTime & operator-=(std::chrono::duration< Rep, Period > delta) noexcept
Shifts this instant backwards by an arbitrary chrono duration.
Definition datetime.hpp:247
friend constexpr DateTime operator-(DateTime lhs, std::chrono::duration< Rep, Period > delta) noexcept
An instant delta before lhs.
Definition datetime.hpp:268
constexpr std::strong_ordering operator<=>(const DateTime &other) const noexcept=default
Ordering on the underlying instant.
static std::optional< DateTime > fromIso8601(std::string_view text) noexcept
Strictly parses ISO-8601 UTC: YYYY-MM-DDTHH:MM:SS, optionally followed by .m/.mm/....
Definition datetime.hpp:148
std::chrono::sys_time< std::chrono::milliseconds > value
The underlying UTC instant (Unix time, leap seconds ignored).
Definition datetime.hpp:73
constexpr DateTime(std::chrono::year year, std::chrono::month month, std::chrono::day day, std::chrono::hours hour, std::chrono::minutes minute, std::chrono::seconds second, std::chrono::milliseconds millisecond=std::chrono::milliseconds{0}) noexcept
Composes a UTC instant from calendar/clock components.
Definition datetime.hpp:90
constexpr DateTime & operator+=(std::chrono::duration< Rep, Period > delta) noexcept
Shifts this instant by an arbitrary chrono duration.
Definition datetime.hpp:238
An optionally-empty timestamp field.
Definition datetime.hpp:376
std::optional< DateTime > value
The payload; std::nullopt means "not entered".
Definition datetime.hpp:378
static Timestamp now() noexcept
An engaged timestamp holding the current UTC instant.
Definition datetime.hpp:393
constexpr const DateTime & operator*() const noexcept
Unchecked access to the engaged instant (UB when empty, exactly like std::optional — the unchecked co...
Definition datetime.hpp:404
constexpr bool hasValue() const noexcept
Whether an instant has been entered.
Definition datetime.hpp:397
constexpr Timestamp(std::optional< DateTime > payload) noexcept
Adopts an optional payload as-is.
Definition datetime.hpp:389
constexpr Timestamp() noexcept=default
Constructs the empty state.
constexpr auto operator<=>(const Timestamp &other) const noexcept=default
Ordering/equality on the payload; empty sorts before engaged.