Per-field chrono format wrappers: glz::date_format and glz::epoch_count#2658
Merged
Conversation
…_count Decouple the wire format from the chrono type (issue #2640) with two JSON field wrappers usable inside glz::object(): - glz::date_format<&T::m, "%Y-%m-%d %H:%M:%S"> renders a system_clock time_point or year_month_day with a hand-rolled, compile-time-validated strftime subset (%Y %m %d %H %M %S %F %T %%). UTC wall-clock; %S is integer seconds (sub-second truncated by design). - glz::epoch_count<&T::m, Duration> renders a system_clock time_point as a numeric Unix count, the per-field counterpart to glz::epoch_time. Both follow the established wrapper precedents (float_format's NTTP format_str, quoted_t's full custom read+write) and are JSON-only: marked glaze_reflect=false so binary backends fail to compile rather than silently miscode. Format string parsing/formatting is hand-rolled (no std::chrono::parse/format) reusing the existing write_digits/parse_digits primitives, for consistent behavior across the compiler matrix. utc_time and binary backends are intentionally out of scope for this MVP. - Extract format_str NTTP into glaze/core/format_str.hpp (shared with float_format) - Add strftime-subset helpers to chrono_detail in glaze/core/chrono.hpp - New header glaze/json/chrono_format.hpp; wired via glaze/chrono.hpp - Tests in tests/chrono_test and docs in docs/chrono.md
Review-driven guards, tests, and docs for glz::date_format / glz::epoch_count. No behavioral regressions for valid inputs. - epoch_count: validate the Duration unit (is_duration) alongside the member type via a shared validate_epoch_count() guard, so a non-duration unit gives a friendly message instead of a cascade out of `typename Duration::rep`. - date_format: reject an invalid/default year_month_day on write (constraint_violated) so the writer never emits a string its own reader rejects, and an out-of-range month/day cannot slip past write_digits<2>. - date_format: skip the ensure_space reservation on the write_unchecked fast path (matches to<JSON, num_t>); correct the buffer-size comments (%F, not %T, is the widest token at 5 bytes per source char). - docs: correct the backend-scope wording (text/JSON-family only; TOML also fails to compile; NDJSON/stencil work) and document the epoch_count integer input contract. - tests: compile-time validator assertions plus runtime coverage for year boundaries, pre-1970 instants, the %% literal, sub-second read-back, the epoch_count cross-precision duration_cast, and the invalid-ymd write guard.
MSVC's std::chrono::hours and minutes use a 32-bit rep, so reconstructing a
far-future instant as `sys_days{ymd} + hours + minutes + seconds` overflows
the intermediate minutes count (days * 24 * 60 exceeds INT_MAX, ~4.2e9 at year
9999) and silently wraps to a bogus instant. libc++/libstdc++ use a 64-bit rep,
so the year-boundary test passed there but failed on the msvc/clang-cl jobs,
emitting 1833-11-15T19:43:59 for 9999-12-31T23:59:59.
Anchor the day at seconds precision (sys_seconds) before folding in the
time-of-day so the whole chain is computed in 64 bits, in both the date_format
reader and the test's input construction.
Closed
date_format and epoch_count now take the member pointer as a value argument: glz::date_format(&T::start, "%Y-%m-%d %H:%M:%S") glz::epoch_count<std::chrono::milliseconds>(&T::logged) Previously the member pointer and (for date_format) the format string were non-type template parameters. Carrying the format as a runtime std::string_view keys the view type on the member type alone, so fields that differ only in their format string no longer each instantiate a fresh date_format_t / to / from / closure. On a synthetic struct of N same-typed members with distinct formats this removed ~40% of the wrapper's template instantiations and made the per-distinct- format compile cost ~flat (it tracks a single shared format). date_format's strftime walk was already runtime, so codegen is unchanged. The strftime pattern stays a compile-time literal: a consteval guard in the date_format() factory still rejects unsupported tokens, a missing calendar date, and time tokens on a year_month_day field. float_format keeps its NTTP form because it relies on std::format's compile-time format checking. The reader keeps the seconds-anchored reconstruction (sys_seconds) so far-future years stay exact under MSVC's 32-bit chrono hours/minutes rep.
The consteval date_format() validator used throw to turn an invalid format string into a compile error. throw is ill-formed under -fno-exceptions even inside an immediate function, so the chrono_test build (which compiles with -fno-exceptions -fno-rtti) failed across the GCC/Clang matrix. Replace the throws with calls to non-constexpr marker functions on the error branches: when a branch is constant-evaluated the call makes the immediate invocation non-constant, producing a hard compile error whose diagnostic names the marker. Same compile-time rejection of unsupported tokens, a missing date, and time tokens on a year_month_day field, now without relying on exceptions.
date_format and epoch_count were documented only in chrono.md and were absent from docs/wrappers.md, the canonical wrapper list where the sibling float_format lives, so a user browsing wrappers would not find them. Add both there (with the glaze/chrono.hpp include note and a link to the chrono docs). Also round out the chrono.md section: add a read/round-trip example for date_format (previously only the write side was shown) and document the write-side constraint_violated error for out-of-range years / non-ok dates.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses #2640. Decouples the format of a chrono field from its type, so a single field can opt into a custom representation while others keep the type-driven default. (
std::chrono::utc_timesupport from #2640 is tracked separately in #2657.)What
Two per-field wrappers, applied inside
glz::object(...)in aglz::meta<T>specialization:glz::date_format(&T::member, "pattern")— serializes asystem_clocktime point (or ayear_month_day) with a locale-independentstrftime-subset pattern instead of ISO 8601.glz::epoch_count<Duration>(&T::member)— serializes asystem_clocktime point as a numeric Unix timestamp inDurationunits (the per-field counterpart to theglz::epoch_timestorage wrapper).The member pointer is a value argument (and for
date_formatso is the pattern), not a non-type template parameter — see Implementation notes for why.Supported tokens (compile-time validated):
%Y %m %d %H %M %S %F %T %%. Unsupported tokens, a missing calendar date, or time tokens on ayear_month_dayfield are hard compile errors.Scope
glz::to<Format, …>), not a silent miscoding.%Swrites integer seconds; sub-second precision is truncated on write (a%Spattern has nowhere to put a fraction). Use the default ISO 8601 representation for sub-second fidelity.Guarantees
[0000, 9999]on both read and write, matching the ISO 8601 path.date_formatrejects an invalid/defaultyear_month_dayon write (constraint_violated), so the writer never emits a string its own reader would reject and an out-of-range month/day cannot slip pastwrite_digits<2>.epoch_countvalidates both its member type and itsDurationunit at compile time, so misuse produces a clear diagnostic rather than a deep template cascade.Implementation notes
date_format's pattern) are passed as values, so the view type is keyed on the member type alone and the format lives as a runtimestd::string_view. Fields that differ only in their format string no longer each instantiate a freshdate_format_t/to/from/ closure — on a synthetic struct of N same-typed members with distinct formats this removes ~40% of the wrapper's template instantiations and makes the per-distinct-format compile cost flat. The pattern stays a compile-time literal: aconstevalguard in thedate_format(...)factory rejects unsupported tokens, a missing date, or time tokens on ayear_month_day. (glz::float_formatkeeps the NTTP form because it relies onstd::format's compile-time format checking;date_format's strftime walk is already runtime, so it has no such dependency.)core/chrono.hppthat reuses the existingparse_digits/write_digitsprimitives, so the[0000, 9999]year and component-range guarantees match the ISO 8601 path.date_formatreader anchors its time-point reconstruction at seconds precision (sys_seconds) before folding in the time-of-day. MSVC'sstd::chrono::hours/minutesuse a 32-bit representation, so the naturalsys_days + hours + minutesidiom overflows the intermediate minutes count for far-future years; the seconds anchor keeps the arithmetic in 64 bits and the reconstruction exact across libstdc++, libc++, and MSVC.float_format's compile-time format-string helper was factored out ofjson/float_format.hppintocore/format_str.hpp(no behavioral change).Follow-ups (tracked separately)
std::chrono::utc_time/utc_clocksupport (the remaining part of Chrono Improvements #2640).std::optional.