Skip to content

Commit e1d70fa

Browse files
authored
Per-field chrono format wrappers: glz::date_format and glz::epoch_count (#2658)
* Add per-field chrono format wrappers: glz::date_format and glz::epoch_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 * Harden per-field chrono wrappers after review 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. * Fix MSVC overflow building far-future instants in date_format 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. * Switch per-field chrono wrappers to value-based call syntax 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. * Report date_format compile errors without throw (-fno-exceptions safe) 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. * docs: surface per-field chrono wrappers in the wrapper catalog 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.
1 parent 8391961 commit e1d70fa

9 files changed

Lines changed: 1069 additions & 10 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,4 @@ pr_description.md
6565
/exterior
6666
pr.md
6767
/tmp/
68+
tmp/

docs/chrono.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,94 @@ struct Event {
180180
};
181181
```
182182
183+
## Per-Field Format Customization
184+
185+
The clock type alone decides the default representation (ISO 8601 for `system_clock`, numeric count for `steady_clock`, and so on). When a single field needs a different shape, the per-field wrappers below decouple the *format* from the *type*. They are applied inside `glz::object(...)` within a `glz::meta<T>` specialization and require `#include "glaze/chrono.hpp"`.
186+
187+
### `glz::date_format` — custom strftime-subset pattern
188+
189+
`glz::date_format(&T::member, "pattern")` serializes a `system_clock` time point (or a `year_month_day`) using a `strftime`-style pattern instead of ISO 8601:
190+
191+
```cpp
192+
#include "glaze/chrono.hpp"
193+
194+
struct Event {
195+
std::chrono::system_clock::time_point start{};
196+
std::chrono::sys_days day{};
197+
};
198+
199+
template <>
200+
struct glz::meta<Event> {
201+
using T = Event;
202+
static constexpr auto value = glz::object(
203+
"start", glz::date_format(&T::start, "%Y-%m-%d %H:%M:%S"), // "2026-06-18 12:34:56"
204+
"day", glz::date_format(&T::day, "%Y/%m/%d")); // "2026/06/18"
205+
};
206+
```
207+
208+
The wrapper is bidirectional: the same pattern parses the value back on read.
209+
210+
```cpp
211+
Event e{};
212+
e.start = std::chrono::sys_days{std::chrono::year{2026} / 6 / 18} + std::chrono::hours{12} +
213+
std::chrono::minutes{34} + std::chrono::seconds{56};
214+
215+
std::string json = glz::write_json(e).value(); // {"start":"2026-06-18 12:34:56","day":"..."}
216+
217+
Event parsed{};
218+
auto ec = glz::read_json(parsed, json); // parsed.start == e.start
219+
```
220+
221+
The member pointer and pattern are passed as ordinary arguments (not template parameters), so fields that share a member type but differ in format do not each spin up a fresh set of template instantiations. The pattern is still a compile-time literal and is validated at compile time.
222+
223+
Supported conversion specifiers (locale-independent by design):
224+
225+
| Token | Meaning | Token | Meaning |
226+
|-------|----------------------|-------|------------------------|
227+
| `%Y` | year (4 digits) | `%H` | hour, 24-hour (2) |
228+
| `%m` | month (2 digits) | `%M` | minute (2 digits) |
229+
| `%d` | day (2 digits) | `%S` | second (2 digits) |
230+
| `%F` | `%Y-%m-%d` | `%T` | `%H:%M:%S` |
231+
| `%%` | literal `%` | | |
232+
233+
The pattern is validated at compile time:
234+
235+
- An unsupported token (e.g. `%A`, `%j`, `%z`) is a hard compile error.
236+
- The pattern must contain a full calendar date (`%Y %m %d`, or `%F`); a time-only pattern cannot reconstruct an absolute time point and is rejected.
237+
- A `year_month_day` field rejects time tokens (`%H %M %S %T`).
238+
239+
Notes and limitations (MVP scope):
240+
241+
- **Times are treated as UTC wall-clock.** There is no timezone token; the decomposed fields are UTC, matching the ISO 8601 writer.
242+
- **The four-digit-year range still applies.** As with the ISO 8601 writers, a year outside `[0000, 9999]` (or a non-`ok()` `year_month_day`) fails to serialize with `error_code::constraint_violated` rather than emitting wrap-around digits.
243+
- **`%S` writes integer seconds only.** Sub-second precision is truncated on write (the format you typed has nowhere to put it), so round-tripping a finer-than-seconds value through a `%S` pattern is lossy by design. Use the default ISO 8601 representation when you need sub-second fidelity. Explicit-width fraction tokens (`%3S`/`%6S`/`%9S`) are a planned extension.
244+
- **Text/JSON-family backends only.** `date_format` is a textual wrapper that routes through the JSON serializer, so it also works for the JSON-family text outputs (NDJSON, stencil/mustache). Serializing a field that uses it to a backend with its own native encoding (BEVE, CBOR, MsgPack, BSON, or TOML) is a compile error (an undefined `glz::to<Format, …>`) rather than a silent miscoding.
245+
246+
### `glz::epoch_count` — per-field Unix timestamp
247+
248+
`glz::epoch_count<Duration>(&T::member)` serializes a `system_clock` time point as a numeric Unix timestamp in units of `Duration`. It is the per-field counterpart to the `glz::epoch_time` storage wrapper, letting one field be an epoch count while others stay ISO 8601:
249+
250+
```cpp
251+
#include "glaze/chrono.hpp"
252+
253+
struct Reading {
254+
std::chrono::system_clock::time_point observed{}; // ISO 8601 (default)
255+
std::chrono::sys_time<std::chrono::milliseconds> logged{};
256+
};
257+
258+
template <>
259+
struct glz::meta<Reading> {
260+
using T = Reading;
261+
static constexpr auto value = glz::object(
262+
"observed", &T::observed, // "2026-06-18T12:34:56Z"
263+
"logged", glz::epoch_count<std::chrono::milliseconds>(&T::logged)); // 1781786096789
264+
};
265+
```
266+
267+
Unlike `glz::epoch_time<Duration>` (a storage type you declare your member as), `glz::epoch_count` wraps an ordinary `system_clock` time-point member in place, so you can keep the field's native type. Like `date_format`, it routes through the JSON serializer (so it also works for NDJSON and stencil output) and is a compile error under the native-encoding backends (BEVE, CBOR, MsgPack, BSON, TOML).
268+
269+
The count is read as a JSON integer: exponent form (`1e3``1000`) is accepted, while fractional values (`1.5`) and quoted numbers (`"1500"`) are rejected with `error_code::parse_error`.
270+
183271
## Complete Example
184272

185273
```cpp
@@ -264,6 +352,8 @@ TOML has native datetime types (not quoted strings). When using `glz::write_toml
264352
- `hh_mm_ss<Duration>` → TOML Local Time (`10:30:45.123`)
265353
- Durations and other time points → Numeric values
266354

355+
> The per-field `glz::date_format` / `glz::epoch_count` wrappers are JSON-text-only and do **not** compile under `glz::write_toml`. Native (unwrapped) chrono members serialize as TOML datetimes as shown above; only the per-field format wrappers are restricted.
356+
267357
See [TOML Documentation](./toml.md#datetime-support) for full details.
268358

269359
## CBOR Datetime Support

docs/wrappers.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,15 @@ glz::custom<&T::read, &T::write> // Calls custom read and write std::functions,
3434
glz::manage<&T::x, &T::read_x, &T::write_x> // Calls read_x() after reading x and calls write_x() before writing x
3535
glz::as_array<&T::member> // Treat a reflected/member-annotated type as a positional array for read and write
3636
glz::flatten_map<&T::member> // Flatten map-like containers into a JSON array [key..., value, ...]
37+
38+
// Per-field std::chrono wrappers (require #include "glaze/chrono.hpp"; JSON-text backends only)
39+
glz::date_format(&T::time_point, "%Y-%m-%d %H:%M:%S") // Format a system_clock time point or year_month_day with a strftime-subset pattern
40+
glz::epoch_count<std::chrono::milliseconds>(&T::time_point) // Write a system_clock time point as a numeric Unix timestamp in the given units
3741
```
3842
43+
> [!NOTE]
44+
> Unlike the wrappers above, `glz::date_format` and `glz::epoch_count` take the member pointer as a value argument rather than a non-type template parameter, and they live in `#include "glaze/chrono.hpp"`. See [std::chrono Support](chrono.md#per-field-format-customization) for the supported tokens, compile-time validation, and limitations.
45+
3946
## Associated glz::opts
4047
4148
`glz::opts` is the compile time options struct passed to most of Glaze functions to configure read/write behavior. Many wrappers are associated with compile time options that can be set via a custom options struct inheriting from `glz::opts`.

include/glaze/chrono.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@
77
// Includes all necessary headers for chrono serialization
88

99
#include "glaze/core/chrono.hpp"
10+
#include "glaze/json/chrono_format.hpp"
1011
#include "glaze/json/read.hpp"
1112
#include "glaze/json/write.hpp"

0 commit comments

Comments
 (0)