!===========================================
! Author: Elmo Tempel (elmo.tempel@ut.ee)
!===========================================
!
!> Independent, self-contained module providing the element-level Parquet date/time value types.
!!
!! Provides three public types, each holding ONE element (not a whole column -- unlike
!! `parquet_string_column` in the `parquet_strings` module):
!!
!! * `parquet_date` -- a calendar date, stored as days since 1970-01-01 (identical to the
!!   physical value of Parquet's DATE / Arrow's date32 type).
!! * `parquet_time` -- a time of day, stored as nanoseconds since midnight (holds any Parquet
!!   TIME unit -- seconds/millis/micros/nanos -- exactly).
!! * `parquet_timestamp` -- an instant, stored losslessly as seconds since 1970-01-01T00:00:00
!!   plus a nanosecond-of-second part (holds any Parquet TIMESTAMP unit, including legacy
!!   INT96 files, exactly).
!!
!! All three are plain value types with an internal null state; a default-initialized element
!! is null ("no value yet"). Element access follows a two-tier rule: semantic accessors
!! (`get`, `year`, `to_string`, `to_mjd`, `to_unix`, comparison operators, ...) abort on a
!! null element (guard with `is_null`), interop accessors (`raw`/`set_raw`/`get_raw`) never
!! abort (they return 0 for a null element -- validity travels separately via `is_null`), and
!! type-to-type conversions (`get_date`/`get_time`, `set(date, time)`) propagate null instead
!! of aborting. Most procedures are elemental, so they apply directly to whole arrays
!! (`mask = ts%is_null()`, `call dates%set(years, months, days)`).
!!
!! Calendar math uses the proleptic Gregorian calendar (Howard Hinnant's exact-integer
!! `days_from_civil`/`civil_from_days` algorithms). Timezone interpretation is deliberately
!! out of scope: a `parquet_timestamp` always holds the stored epoch offset verbatim, and a
!! column's timezone metadata is a column-level property of the read/write layer, not of the
!! element. This module depends only on intrinsic modules (`iso_fortran_env`,
!! `ieee_arithmetic`); it has no dependency on any other module in this library (the
!! read/write integration layer depends on it, never the reverse).
!!
!! **Why the setters take `intent(inout)` rather than `intent(out)`, and the obligation that
!! creates.** A POLYMORPHIC `intent(out)` dummy is not free: the compiler default-initialises the
!! element through the runtime on entry, and a type-bound procedure's passed-object dummy has to be
!! polymorphic, so an elemental setter pays that per element. Measured on a 4M-row whole-column
!! read: the construction loop cost 12.3 ms with `intent(out)` and 3.4 ms with `intent(inout)`
!! (3.6x), taking the whole date-column read from 23.5 ms to 14.5 ms. The same change on the
!! timestamp setters took its loop from 17.2 ms to 5.7 ms.
!!
!! The cost of that is an obligation moved from the compiler to this source: `intent(out)` reset
!! EVERY component for free, whereas under `intent(inout)` a component a setter does not assign
!! keeps whatever the element held before. So **every `intent(inout)` setter here must assign every
!! component of its type**, and adding a component to one of these types means revisiting all of
!! them. `tools/check_source_conventions.py`'s `check_temporal_setters_assign_all` enforces exactly
!! that, deriving both the component list and the setter list from this file so a new component or
!! a new setter is covered without editing the check.
!!
!! The converse is equally load-bearing and is enforced too: a setter with a caught-failure path
!! that RETURNS without assigning `self` -- `%parse` with a `success` argument, and
!! `ts_set_date_time`'s null propagation -- must KEEP `intent(out)`, because that is precisely what
!! makes a failed or null-propagating call yield a null element rather than a stale one.
module parquet_temporal
    use, intrinsic :: iso_fortran_env, only : int32, int64, real64
    use, intrinsic :: ieee_arithmetic, only : ieee_is_nan
    !
    implicit none
    private
    !
    public :: parquet_date
    public :: parquet_time
    public :: parquet_timestamp
    public :: parquet_unit_seconds
    public :: parquet_unit_millis
    public :: parquet_unit_micros
    public :: parquet_unit_nanos
    public :: parquet_ns_per_sec
    public :: parquet_ns_per_day
    public :: parquet_ns_to_sec
    public :: parquet_ns_to_day
    !
    !> Error-message prefix for every `error stop` raised by this module.
    character(len=*), parameter :: EP = "parquet_temporal: "
    !
    !> Time-unit selectors for `set_unix`/`to_unix` (and reused by the read/write layer). Note:
    !! parquet_unit_seconds is meaningful only for set_unix/to_unix (Unix-time interop) -- it can
    !! never be a MAML-declared TIME/TIMESTAMP column's own stored unit, since Parquet's physical
    !! format has no seconds-resolution TIME/TIMESTAMP encoding at all (only MILLIS/MICROS/NANOS);
    !! a MAML `time[s]`/`timestamp[s]` token is rejected at parse time for exactly this reason.
    integer, parameter :: parquet_unit_seconds = 1 !! whole seconds since the epoch.
    integer, parameter :: parquet_unit_millis = 2  !! milliseconds since the epoch.
    integer, parameter :: parquet_unit_micros = 3  !! microseconds since the epoch.
    integer, parameter :: parquet_unit_nanos = 4   !! nanoseconds since the epoch.
    !
    !> Nanoseconds per second / seconds per day / nanoseconds per day.
    integer(int64), parameter :: NS_PER_SECOND = 1000000000_int64
    integer(int64), parameter :: SECONDS_PER_DAY = 86400_int64
    integer(int64), parameter :: NS_PER_DAY = SECONDS_PER_DAY*NS_PER_SECOND
    !
    !> Public unit-conversion convenience constants for the raw nanosecond values the
    !! difference/offset operators traffic in. The `_per_X` pair are exact integer(int64)
    !! divisors (for a whole-unit result, e.g. `ns_value/parquet_ns_per_day` for a whole day
    !! count); the `_to_X` pair are real(real64) multiplicative factors (for a fractional
    !! result, e.g. a fractional day count) -- see feature_temporal.md's "Design: unit-conversion
    !! convenience constants" for the naming rationale.
    integer(int64), parameter :: parquet_ns_per_sec = NS_PER_SECOND !! exact; ns in one second.
    integer(int64), parameter :: parquet_ns_per_day = NS_PER_DAY   !! exact; ns in one day.
    real(real64), parameter :: parquet_ns_to_sec = 1.0e-9_real64   !! convenience; ns -> fractional seconds.
    real(real64), parameter :: parquet_ns_to_day = 1.0_real64/86400.0e9_real64 !! convenience; ns -> fractional days.
    !
    !> Days from 1858-11-17 (MJD epoch) to 1970-01-01 (Unix epoch): MJD = unix days + 40587.
    integer(int64), parameter :: MJD_UNIX_EPOCH = 40587_int64
    !> Offset from Modified Julian Date to Julian Date: JD = MJD + 2400000.5.
    real(real64), parameter :: JD_MJD_OFFSET = 2400000.5_real64
    !
    !> The minimum int64 value, built with ibset to avoid the out-of-symmetric-range literal
    !! `-huge-1` (same idiom as the int8 case in CLAUDE.md's "Build and compiler notes").
    !!
    !! **Never combine it with a runtime value in an expression -- not even through a local copy.**
    !! nagfor 7.2 mis-evaluates such an expression, silently: `v < INT64_MIN - delta` answers
    !! `.true.` for `v = 19920, delta = -4` (it must be `.false.`), and `INT64_MIN/scale` comes
    !! back with the WRONG SIGN, `+9223372036` instead of `-9223372036` for `scale = 1e9`. `mod()`
    !! divides the same way. Printing the expression shows the RIGHT value, so only a comparison
    !! or a stored result reveals it. Both failure directions have shipped here: a guard that fired
    !! on ordinary date arithmetic (`a + 4` aborting), and a guard that never fired, letting an
    !! out-of-range `to_unix` wrap silently instead of aborting.
    !!
    !! **Copying it into a local first does NOT work, and believing otherwise cost a second round
    !! of this bug.** The copy was measured correct -- in a probe that assigned it in a program
    !! body. In a module procedure, which is what this file actually contains, the optimiser
    !! propagates the constant straight back: correct at `-O0`, wrong at `-O2`, `-O3` and `-O4`, so
    !! `fpm test` passed and `fpm test --profile release` aborted on `date + 4`. **A workaround
    !! verified in a simplified reproducer is not verified**; reproduce it in the shape the code
    !! really has. Use `offset_floor` for the subtraction, and form any other bound from `huge`
    !! alone, as `ts_to_unix` does for the division.
    !!
    !! Safe, measured at `-O0`/`-O2`/`-O3`/`-O4`: equality (`n == INT64_MIN`), a bare constant
    !! comparison, plain assignment, a copy compared bare (`ts_diff_ns`'s `lo`), and an expression
    !! whose every operand is constant and therefore folded (`INT64_MIN + TS_DIFF_NS_BOUND_SECONDS`,
    !! also `ts_diff_ns`). gfortran, ifx and flang are unaffected throughout, so only a nagfor run
    !! -- and only an OPTIMISED one -- reports a regression here.
    integer(int64), parameter :: INT64_MIN = ibset(0_int64, 63)
    !
    !> `parquet_date%days` bounds (the int32 range), as int64 for overflow-safe checks.
    integer(int64), parameter :: DATE_DAYS_MAX = int(huge(0_int32), int64)
    integer(int64), parameter :: DATE_DAYS_MIN = -DATE_DAYS_MAX - 1_int64
    !> Year magnitude accepted by the string parsers, just above the largest year an int64
    !! timestamp can reach (+-292,277,026,596), so the era math inside days_from_civil stays
    !! within int64 while no representable value is rejected; the exact per-value range check
    !! happens against PARSE_DAYS_BOUND (timestamps) / DATE_DAYS_MAX (dates) afterwards.
    integer(int64), parameter :: PARSE_YEAR_BOUND = 292277030000_int64
    !> Day-count magnitude a parsed timestamp may reach: the largest |days| whose `days*86400`
    !! still fits int64 (floor(huge(int64)/86400)).
    integer(int64), parameter :: PARSE_DAYS_BOUND = 106751991167300_int64
    !> `abs(MJD)` bound for parquet_timestamp%set_mjd/set_jd, keeping `seconds` within int64
    !! (the exact limit is huge(int64)/86400 ~ 1.0675e14; slightly conservative on purpose).
    real(real64), parameter :: MJD_ABS_BOUND = 1.0e14_real64
    !> Elapsed-seconds magnitude bound for ts_diff_ns's result: huge(int64)/NS_PER_SECOND,
    !! minus 1 second of headroom so the largest nanosecond-of-second component (up to
    !! 999999999) can still be added/subtracted afterwards without itself overflowing int64.
    !! ~292.3 years (huge(int64)/1e9 seconds), matching pandas.Timestamp's own 1677-2262 range.
    integer(int64), parameter :: TS_DIFF_NS_BOUND_SECONDS = huge(0_int64)/NS_PER_SECOND - 1_int64
    !
    !> A calendar date (proleptic Gregorian), stored as days since 1970-01-01 -- identical to
    !! the physical value of a Parquet DATE / Arrow date32 column. A default-initialized
    !! element is null. Range: about +-5.8 million years.
    type :: parquet_date
        private
        integer(int32) :: days = 0_int32 !! days since 1970-01-01 (the Parquet DATE value).
        logical :: valid = .false.       !! .false. = null element (the default state).
    contains
        procedure :: set => date_set              !! Sets from a validated (year, month, day).
        procedure :: get => date_get              !! Returns year, month, day (aborts on null).
        procedure :: year => date_year            !! Calendar year (aborts on null).
        procedure :: month => date_month          !! Calendar month 1..12 (aborts on null).
        procedure :: day => date_day              !! Day of month 1..31 (aborts on null).
        procedure :: is_null => date_is_null      !! Whether the element is null (never aborts).
        procedure :: set_null => date_set_null    !! Marks the element null.
        procedure :: set_raw => date_set_raw      !! Sets the raw day count (interop; marks valid).
        procedure :: raw => date_raw              !! Raw day count; 0 for a null element (interop).
        procedure, private :: date_set_mjd_i32    !! int32 specific of set_mjd.
        procedure, private :: date_set_mjd_i64    !! int64 specific of set_mjd.
        generic :: set_mjd => date_set_mjd_i32, date_set_mjd_i64 !! Sets from an integer Modified Julian Date.
        procedure :: to_mjd => date_to_mjd        !! Integer Modified Julian Date (aborts on null).
        procedure :: to_string => date_to_string  !! ISO-8601 "YYYY-MM-DD" (aborts on null).
        procedure :: parse => date_parse          !! Sets from an ISO-8601 date string.
        procedure, private :: date_eq             !! == specific.
        procedure, private :: date_ne             !! /= specific.
        procedure, private :: date_lt             !! <  specific.
        procedure, private :: date_le             !! <= specific.
        procedure, private :: date_gt             !! >  specific.
        procedure, private :: date_ge             !! >= specific.
        generic :: operator(==) => date_eq        !! Equality (aborts on a null operand).
        generic :: operator(/=) => date_ne        !! Inequality (aborts on a null operand).
        generic :: operator(<) => date_lt         !! Ordering (aborts on a null operand).
        generic :: operator(<=) => date_le        !! Ordering (aborts on a null operand).
        generic :: operator(>) => date_gt         !! Ordering (aborts on a null operand).
        generic :: operator(>=) => date_ge        !! Ordering (aborts on a null operand).
        procedure, private :: date_diff           !! (date, date) specific of operator(-): whole-day difference.
        procedure, private :: date_sub_days_i32   !! (date, integer(int32)) specific of operator(-).
        procedure, private :: date_sub_days_i64   !! (date, integer(int64)) specific of operator(-).
        procedure, private :: date_add_days_i32   !! (date, integer(int32)) specific of operator(+).
        procedure, private :: date_add_days_i64   !! (date, integer(int64)) specific of operator(+).
        generic :: operator(-) => date_diff, date_sub_days_i32, date_sub_days_i64
        !! Difference or day offset (aborts on a null operand / out-of-range result).
        generic :: operator(+) => date_add_days_i32, date_add_days_i64
        !! Day offset (aborts on a null operand / out-of-range result).
    end type parquet_date
    !
    !> A time of day, stored as nanoseconds since midnight, [0, 86400e9 - 1]. Holds any
    !! Parquet TIME unit (seconds/millis/micros/nanos) exactly. A default-initialized element
    !! is null.
    type :: parquet_time
        private
        integer(int64) :: nanoseconds = 0_int64 !! nanoseconds since midnight, [0, 86400e9 - 1].
        logical :: valid = .false.              !! .false. = null element (the default state).
    contains
        procedure :: set => time_set              !! Sets from validated (hour, minute, second[, nanosecond]).
        procedure :: get => time_get              !! Returns hour, minute, second[, nanosecond] (aborts on null).
        procedure :: hour => time_hour            !! Hour 0..23 (aborts on null).
        procedure :: minute => time_minute        !! Minute 0..59 (aborts on null).
        procedure :: second => time_second        !! Second 0..59 (aborts on null).
        procedure :: nanosecond => time_nanosecond !! Sub-second part 0..999999999 (aborts on null).
        procedure :: is_null => time_is_null      !! Whether the element is null (never aborts).
        procedure :: set_null => time_set_null    !! Marks the element null.
        procedure :: set_raw => time_set_raw      !! Sets raw ns-since-midnight (interop; marks valid).
        procedure :: raw => time_raw              !! Raw ns-since-midnight; 0 for a null element (interop).
        procedure :: to_string => time_to_string  !! ISO-8601 "HH:MM:SS[.fraction]" (aborts on null).
        procedure :: parse => time_parse          !! Sets from an ISO-8601 time string.
        procedure, private :: time_eq             !! == specific.
        procedure, private :: time_ne             !! /= specific.
        procedure, private :: time_lt             !! <  specific.
        procedure, private :: time_le             !! <= specific.
        procedure, private :: time_gt             !! >  specific.
        procedure, private :: time_ge             !! >= specific.
        generic :: operator(==) => time_eq        !! Equality (aborts on a null operand).
        generic :: operator(/=) => time_ne        !! Inequality (aborts on a null operand).
        generic :: operator(<) => time_lt         !! Ordering (aborts on a null operand).
        generic :: operator(<=) => time_le        !! Ordering (aborts on a null operand).
        generic :: operator(>) => time_gt         !! Ordering (aborts on a null operand).
        generic :: operator(>=) => time_ge        !! Ordering (aborts on a null operand).
        procedure, private :: time_diff           !! (time, time) specific of operator(-): ns difference.
        procedure, private :: time_sub_ns_i32     !! (time, integer(int32)) specific of operator(-).
        procedure, private :: time_sub_ns_i64     !! (time, integer(int64)) specific of operator(-).
        procedure, private :: time_add_ns_i32     !! (time, integer(int32)) specific of operator(+).
        procedure, private :: time_add_ns_i64     !! (time, integer(int64)) specific of operator(+).
        generic :: operator(-) => time_diff, time_sub_ns_i32, time_sub_ns_i64
        !! Difference or ns offset (wraps; aborts on a null operand or a >24h offset magnitude).
        generic :: operator(+) => time_add_ns_i32, time_add_ns_i64
        !! Ns offset (wraps; aborts on a null operand or a >24h offset magnitude).
    end type parquet_time
    !
    !> An instant, stored losslessly as whole seconds since 1970-01-01T00:00:00 plus a
    !! normalized nanosecond-of-second part (always 0..999999999, also for pre-epoch
    !! instants). Holds any Parquet TIMESTAMP unit exactly over the full int64 range of the
    !! stored value. Timezone-agnostic: holds the stored epoch offset verbatim. A
    !! default-initialized element is null.
    type :: parquet_timestamp
        private
        integer(int64) :: seconds = 0_int64     !! whole seconds since 1970-01-01T00:00:00.
        integer(int32) :: nanoseconds = 0_int32 !! nanosecond-of-second part, always 0..999999999.
        logical :: valid = .false.              !! .false. = null element (the default state).
    contains
        procedure, private :: ts_set_civil        !! (year, month, day, hour, minute, second[, ns]) specific.
        procedure, private :: ts_set_date_time    !! (parquet_date, parquet_time) specific; null propagates.
        generic :: set => ts_set_civil, ts_set_date_time !! Sets from civil fields or a date + time pair.
        procedure :: get => ts_get                !! Returns all civil fields (aborts on null).
        procedure :: get_date => ts_get_date      !! The date part; null propagates (never aborts).
        procedure :: get_time => ts_get_time      !! The time-of-day part; null propagates (never aborts).
        procedure :: is_null => ts_is_null        !! Whether the element is null (never aborts).
        procedure :: set_null => ts_set_null      !! Marks the element null.
        procedure :: set_raw => ts_set_raw        !! Sets the raw (seconds, nanoseconds) pair (interop).
        procedure :: get_raw => ts_get_raw        !! Raw (seconds, nanoseconds); zeros when null (interop).
        procedure, private :: ts_set_unix_i32     !! int32 specific of set_unix.
        procedure, private :: ts_set_unix_i64     !! int64 specific of set_unix.
        generic :: set_unix => ts_set_unix_i32, ts_set_unix_i64 !! Sets from a Unix-time value in a given unit.
        procedure :: to_unix => ts_to_unix        !! Unix-time value in a given unit (aborts on null/loss).
        procedure :: set_mjd => ts_set_mjd        !! Sets from a real64 Modified Julian Date.
        procedure :: to_mjd => ts_to_mjd          !! real64 Modified Julian Date (aborts on null).
        procedure :: set_jd => ts_set_jd          !! Sets from a real64 Julian Date.
        procedure :: to_jd => ts_to_jd            !! real64 Julian Date (aborts on null).
        procedure :: to_string => ts_to_string    !! ISO-8601 "YYYY-MM-DDTHH:MM:SS[.fraction]" (aborts on null).
        procedure :: parse => ts_parse            !! Sets from an ISO-8601 date-time string.
        procedure, private :: ts_eq               !! == specific.
        procedure, private :: ts_ne               !! /= specific.
        procedure, private :: ts_lt               !! <  specific.
        procedure, private :: ts_le               !! <= specific.
        procedure, private :: ts_gt               !! >  specific.
        procedure, private :: ts_ge               !! >= specific.
        generic :: operator(==) => ts_eq          !! Equality (aborts on a null operand).
        generic :: operator(/=) => ts_ne          !! Inequality (aborts on a null operand).
        generic :: operator(<) => ts_lt           !! Ordering (aborts on a null operand).
        generic :: operator(<=) => ts_le          !! Ordering (aborts on a null operand).
        generic :: operator(>) => ts_gt           !! Ordering (aborts on a null operand).
        generic :: operator(>=) => ts_ge          !! Ordering (aborts on a null operand).
        procedure, private :: ts_diff_ns          !! (ts, ts) specific of operator(-): ns difference.
        procedure :: diff_seconds => ts_diff_seconds !! Real64-seconds difference (ts, ts); never aborts on magnitude.
        procedure, private :: ts_sub_ns_i32       !! (ts, integer(int32)) specific of operator(-).
        procedure, private :: ts_sub_ns_i64       !! (ts, integer(int64)) specific of operator(-).
        procedure, private :: ts_add_ns_i32       !! (ts, integer(int32)) specific of operator(+).
        procedure, private :: ts_add_ns_i64       !! (ts, integer(int64)) specific of operator(+).
        generic :: operator(-) => ts_diff_ns, ts_sub_ns_i32, ts_sub_ns_i64
        !! Difference or ns offset (aborts on a null operand or int64 overflow).
        generic :: operator(+) => ts_add_ns_i32, ts_add_ns_i64 !! Ns offset (aborts on a null operand or int64 overflow).
    end type parquet_timestamp
    !
    !> Constructs a valid parquet_date from (year, month, day); aborts on an invalid civil date
    !! (the structure constructor itself is unavailable outside this module -- components are
    !! private -- so this generic takes its place).
    interface parquet_date
        module procedure date_new !! elemental (year, month, day) constructor.
    end interface parquet_date
    !
    !> Constructs a valid parquet_time from (hour, minute, second[, nanosecond]); aborts on
    !! invalid fields.
    interface parquet_time
        module procedure time_new !! elemental (hour, minute, second[, nanosecond]) constructor.
    end interface parquet_time
    !
    !> Constructs a valid parquet_timestamp, either from full civil fields
    !! (year, month, day, hour, minute, second[, nanosecond]) -- aborting on invalid fields --
    !! or from a (parquet_date, parquet_time) pair, where a null input propagates to a null
    !! result.
    interface parquet_timestamp
        module procedure ts_new_civil     !! elemental civil-fields constructor.
        module procedure ts_new_date_time !! elemental (date, time) constructor; null propagates.
    end interface parquet_timestamp
    !
contains
    !
    ! ==================================================================================
    ! Internal helpers (private module procedures)
    ! ==================================================================================
    !
    !> Floor division (quotient rounded toward negative infinity; `b` must be positive).
    elemental integer(int64) function floor_div(a, b)
        integer(int64), intent(in) :: a !! dividend.
        integer(int64), intent(in) :: b !! divisor (positive).
        floor_div = a/b
        if (mod(a, b) /= 0_int64 .and. a < 0_int64) floor_div = floor_div - 1_int64
    end function floor_div
    !
    !> Returns whether year `y` is a leap year in the proleptic Gregorian calendar.
    elemental logical function is_leap_year(y)
        integer(int64), intent(in) :: y !! calendar year.
        is_leap_year = (mod(y, 4_int64) == 0_int64 .and. mod(y, 100_int64) /= 0_int64) &
            .or. mod(y, 400_int64) == 0_int64
    end function is_leap_year
    !
    !> Returns the number of days in month `m` of year `y` (proleptic Gregorian).
    elemental integer function days_in_month(y, m)
        integer(int64), intent(in) :: y !! calendar year.
        integer, intent(in) :: m        !! month, 1..12.
        integer, parameter :: DIM(12) = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
        days_in_month = DIM(m)
        if (m == 2 .and. is_leap_year(y)) days_in_month = 29
    end function days_in_month
    !
    !> Returns days since 1970-01-01 for civil date (y, m, d) -- Howard Hinnant's
    !! days_from_civil, proleptic Gregorian, exact integer math. `y` must be within
    !! PARSE_YEAR_BOUND (callers guarantee this) so the internal era math cannot overflow.
    elemental integer(int64) function days_from_civil(y, m, d)
        integer(int64), intent(in) :: y !! calendar year.
        integer, intent(in) :: m        !! month, 1..12.
        integer, intent(in) :: d        !! day of month, 1..31.
        integer(int64) :: yy, era, yoe, doy, doe
        yy = y
        if (m <= 2) yy = yy - 1_int64
        era = floor_div(yy, 400_int64)
        yoe = yy - era*400_int64
        doy = (153_int64*int(merge(m - 3, m + 9, m > 2), int64) + 2_int64)/5_int64 + int(d - 1, int64)
        doe = yoe*365_int64 + yoe/4_int64 - yoe/100_int64 + doy
        days_from_civil = era*146097_int64 + doe - 719468_int64
    end function days_from_civil
    !
    !> Returns the civil date (y, m, d) for days-since-1970-01-01 `z_in` -- Howard Hinnant's
    !! civil_from_days, the exact inverse of days_from_civil over the full int64 day range
    !! used here.
    pure subroutine civil_from_days(z_in, y, m, d)
        integer(int64), intent(in) :: z_in !! days since 1970-01-01.
        integer(int64), intent(out) :: y   !! calendar year.
        integer, intent(out) :: m          !! month, 1..12.
        integer, intent(out) :: d          !! day of month, 1..31.
        integer(int64) :: z, era, doe, yoe, doy, mp
        z = z_in + 719468_int64
        era = floor_div(z, 146097_int64)
        doe = z - era*146097_int64
        yoe = (doe - doe/1460_int64 + doe/36524_int64 - doe/146096_int64)/365_int64
        y = yoe + era*400_int64
        doy = doe - (365_int64*yoe + yoe/4_int64 - yoe/100_int64)
        mp = (5_int64*doy + 2_int64)/153_int64
        d = int(doy - (153_int64*mp + 2_int64)/5_int64 + 1_int64)
        m = int(mp + merge(3_int64, -9_int64, mp < 10_int64))
        if (m <= 2) y = y + 1_int64
    end subroutine civil_from_days
    !
    !> The smallest int64 that can have `delta` added to it without underflowing, for a `delta`
    !! that is zero or negative: mathematically `INT64_MIN - delta`, and the ONLY way this module
    !! is allowed to compute it.
    !!
    !! It is formed from `huge` alone, and that is the entire point -- **an expression naming the
    !! most-negative constant is miscompiled by nagfor at `-O2` and above** (see INT64_MIN's own
    !! declaration for the measurements). Copying the constant into a local first does NOT help:
    !! the optimiser propagates it straight back, which is how a guard that had been "fixed" that
    !! way still aborted on `date + 4` under `--profile release`.
    !!
    !! Nothing here can overflow. `delta <= 0` makes `delta + 1 <= 1`, so `huge + (delta + 1)` is
    !! at most `huge`; and `delta >= INT64_MIN` makes it at least `huge + INT64_MIN + 1 == 0`. The
    !! result is therefore in `[INT64_MIN + 1, 0]` and always representable. Check the two ends by
    !! hand when reading this: `delta = -1` gives `-huge`, and `delta = INT64_MIN` gives `0`, which
    !! are exactly `INT64_MIN + 1` and `INT64_MIN - INT64_MIN`.
    elemental integer(int64) function offset_floor(delta)
        integer(int64), intent(in) :: delta !! the offset about to be added; must be <= 0.
        offset_floor = -(huge(0_int64) + (delta + 1_int64))
    end function offset_floor
    !
    !> Returns the per-second count of `unit` (1 for seconds ... 1e9 for nanos); aborts on an
    !! unrecognized unit selector.
    elemental integer(int64) function unit_scale(unit)
        integer, intent(in) :: unit !! one of the parquet_unit_* constants.
        select case (unit)
        case (parquet_unit_seconds)
            unit_scale = 1_int64
        case (parquet_unit_millis)
            unit_scale = 1000_int64
        case (parquet_unit_micros)
            unit_scale = 1000000_int64
        case (parquet_unit_nanos)
            unit_scale = NS_PER_SECOND
        case default
            error stop EP//"invalid time unit (use parquet_unit_seconds/millis/micros/nanos)"
        end select
    end function unit_scale
    !
    !> Parses a fixed run of decimal digits into an int64; `ok` is .false. for an empty run,
    !! any non-digit character, or a run too long for int64 (> 18 digits).
    pure subroutine str_to_int(str, val, ok)
        character(len=*), intent(in) :: str !! the digit run (no sign, no blanks).
        integer(int64), intent(out) :: val  !! parsed value.
        logical, intent(out) :: ok          !! .true. when the run is a valid number.
        integer :: i
        val = 0_int64
        ok = len(str) > 0 .and. len(str) <= 18
        if (.not. ok) return
        do i = 1, len(str)
            if (str(i:i) < '0' .or. str(i:i) > '9') then
                ok = .false.
                return
            end if
            val = val*10_int64 + int(iachar(str(i:i)) - 48, int64)
        end do
    end subroutine str_to_int
    !
    !> Parses an ISO-8601 date "[-]YYYY-MM-DD" (year >= 4 digits, month/day exactly 2) between
    !! bounds of `str` and validates it as a civil date; `ok` reports success. `y` is bounded
    !! by PARSE_YEAR_BOUND so downstream day math cannot overflow.
    pure subroutine parse_date_fields(str, y, m, d, ok)
        character(len=*), intent(in) :: str !! candidate date string (surrounding blanks allowed).
        integer(int64), intent(out) :: y    !! parsed calendar year.
        integer, intent(out) :: m           !! parsed month.
        integer, intent(out) :: d           !! parsed day of month.
        logical, intent(out) :: ok          !! .true. when str is a valid civil date.
        integer :: a, b
        integer(int64) :: tmp
        logical :: neg, fok
        y = 0_int64
        m = 0
        d = 0
        ok = .false.
        b = len_trim(str)
        a = 1
        do while (a <= b)
            if (str(a:a) /= ' ') exit
            a = a + 1
        end do
        neg = .false.
        if (a <= b) then
            if (str(a:a) == '-') then
                neg = .true.
                a = a + 1
            end if
        end if
        ! fixed layout from the right: <year>-MM-DD with a >= 4-digit year field
        if (b - a + 1 < 10) return
        if (str(b-2:b-2) /= '-' .or. str(b-5:b-5) /= '-') return
        call str_to_int(str(a:b-6), tmp, fok)
        if (.not. fok) return
        if (tmp > PARSE_YEAR_BOUND) return
        y = merge(-tmp, tmp, neg)
        call str_to_int(str(b-4:b-3), tmp, fok)
        if (.not. fok) return
        m = int(tmp)
        call str_to_int(str(b-1:b), tmp, fok)
        if (.not. fok) return
        d = int(tmp)
        if (m < 1 .or. m > 12) return
        if (d < 1 .or. d > days_in_month(y, m)) return
        ok = .true.
    end subroutine parse_date_fields
    !
    !> Parses an ISO-8601 time "HH:MM:SS[.fraction]" (two-digit fields; 1..9 fraction digits)
    !! between bounds of `str`, validating field ranges; `ok` reports success. The fraction is
    !! right-padded to nanoseconds.
    pure subroutine parse_time_fields(str, h, mi, s, ns, ok)
        character(len=*), intent(in) :: str !! candidate time string (surrounding blanks allowed).
        integer, intent(out) :: h           !! parsed hour.
        integer, intent(out) :: mi          !! parsed minute.
        integer, intent(out) :: s           !! parsed second.
        integer(int64), intent(out) :: ns   !! parsed sub-second part in nanoseconds.
        logical, intent(out) :: ok          !! .true. when str is a valid time of day.
        integer :: a, b, k, nfrac
        integer(int64) :: tmp
        logical :: fok
        h = 0
        mi = 0
        s = 0
        ns = 0_int64
        ok = .false.
        b = len_trim(str)
        a = 1
        do while (a <= b)
            if (str(a:a) /= ' ') exit
            a = a + 1
        end do
        if (b - a + 1 < 8) return
        if (str(a+2:a+2) /= ':' .or. str(a+5:a+5) /= ':') return
        call str_to_int(str(a:a+1), tmp, fok)
        if (.not. fok) return
        h = int(tmp)
        call str_to_int(str(a+3:a+4), tmp, fok)
        if (.not. fok) return
        mi = int(tmp)
        call str_to_int(str(a+6:a+7), tmp, fok)
        if (.not. fok) return
        s = int(tmp)
        if (b >= a + 8) then
            if (str(a+8:a+8) /= '.') return
            nfrac = b - (a + 9) + 1
            if (nfrac < 1 .or. nfrac > 9) return
            call str_to_int(str(a+9:b), tmp, fok)
            if (.not. fok) return
            do k = nfrac + 1, 9
                tmp = tmp*10_int64
            end do
            ns = tmp
        end if
        if (h < 0 .or. h > 23) return
        if (mi < 0 .or. mi > 59) return
        if (s < 0 .or. s > 59) return
        ok = .true.
    end subroutine parse_time_fields
    !
    !> Writes the ISO fraction suffix for sub-second part `ns` into `buf(pos+1:)` -- nothing
    !! for 0, else '.' plus the shortest of 3/6/9 digits that represents `ns` exactly --
    !! advancing `pos` past what was written.
    pure subroutine format_fraction(ns, buf, pos)
        integer(int64), intent(in) :: ns             !! sub-second part, 0..999999999.
        character(len=*), intent(inout) :: buf       !! output buffer.
        integer, intent(inout) :: pos                !! last used position in buf; updated.
        integer(int64) :: v
        integer :: ndig, k
        if (ns == 0_int64) return
        if (mod(ns, 1000000_int64) == 0_int64) then
            ndig = 3
            v = ns/1000000_int64
        else if (mod(ns, 1000_int64) == 0_int64) then
            ndig = 6
            v = ns/1000_int64
        else
            ndig = 9
            v = ns
        end if
        buf(pos+1:pos+1) = '.'
        do k = ndig, 1, -1
            buf(pos+1+k:pos+1+k) = achar(48 + int(mod(v, 10_int64)))
            v = v/10_int64
        end do
        pos = pos + 1 + ndig
    end subroutine format_fraction
    !
    !> Writes the ISO date "[-]YYYY-MM-DD" for (y, m, d) into `buf`, 4-digit-padding years
    !! 0..9999 and using as many digits as needed otherwise; `pos` returns the used length.
    pure subroutine format_date_fields(y, m, d, buf, pos)
        integer(int64), intent(in) :: y        !! calendar year.
        integer, intent(in) :: m               !! month.
        integer, intent(in) :: d               !! day of month.
        character(len=*), intent(inout) :: buf !! output buffer.
        integer, intent(out) :: pos            !! last used position in buf.
        character(len=16) :: digits
        integer(int64) :: v
        integer :: nd, k
        pos = 0
        v = y
        if (v < 0_int64) then
            buf(1:1) = '-'
            pos = 1
            v = -v
        end if
        nd = 0
        do
            nd = nd + 1
            digits(nd:nd) = achar(48 + int(mod(v, 10_int64)))
            v = v/10_int64
            if (v == 0_int64) exit
        end do
        do k = nd + 1, 4 ! ISO-style zero padding to at least 4 year digits
            digits(k:k) = '0'
        end do
        nd = max(nd, 4)
        do k = nd, 1, -1
            pos = pos + 1
            buf(pos:pos) = digits(k:k)
        end do
        buf(pos+1:pos+1) = '-'
        buf(pos+2:pos+2) = achar(48 + m/10)
        buf(pos+3:pos+3) = achar(48 + mod(m, 10))
        buf(pos+4:pos+4) = '-'
        buf(pos+5:pos+5) = achar(48 + d/10)
        buf(pos+6:pos+6) = achar(48 + mod(d, 10))
        pos = pos + 6
    end subroutine format_date_fields
    !
    !> Writes the ISO time "HH:MM:SS" for a nanoseconds-of-day value into `buf(pos+1:)`
    !! (without fraction -- see format_fraction), advancing `pos`.
    pure subroutine format_time_fields(nsod, buf, pos)
        integer(int64), intent(in) :: nsod     !! nanoseconds since midnight.
        character(len=*), intent(inout) :: buf !! output buffer.
        integer, intent(inout) :: pos          !! last used position in buf; updated.
        integer :: h, mi, s
        integer(int64) :: sod
        sod = nsod/NS_PER_SECOND
        h = int(sod/3600_int64)
        mi = int(mod(sod/60_int64, 60_int64))
        s = int(mod(sod, 60_int64))
        buf(pos+1:pos+1) = achar(48 + h/10)
        buf(pos+2:pos+2) = achar(48 + mod(h, 10))
        buf(pos+3:pos+3) = ':'
        buf(pos+4:pos+4) = achar(48 + mi/10)
        buf(pos+5:pos+5) = achar(48 + mod(mi, 10))
        buf(pos+6:pos+6) = ':'
        buf(pos+7:pos+7) = achar(48 + s/10)
        buf(pos+8:pos+8) = achar(48 + mod(s, 10))
        pos = pos + 8
    end subroutine format_time_fields
    !
    ! ==================================================================================
    ! parquet_date
    ! ==================================================================================
    !
    !> Sets the element from civil fields (proleptic Gregorian), validating month, day, and the
    !! representable range; marks it valid.
    impure elemental subroutine date_set(self, year, month, day)
        class(parquet_date), intent(inout) :: self !! receives the date (marked valid).
        integer(int32), intent(in) :: year       !! calendar year.
        integer(int32), intent(in) :: month      !! month, 1..12.
        integer(int32), intent(in) :: day        !! day of month, 1..days_in_month.
        integer(int64) :: d64
        if (month < 1 .or. month > 12) then
            error stop EP//"invalid month in parquet_date%set (must be 1..12)"
        end if
        if (day < 1 .or. day > days_in_month(int(year, int64), int(month))) then
            error stop EP//"invalid day of month in parquet_date%set"
        end if
        d64 = days_from_civil(int(year, int64), int(month), int(day))
        if (d64 > DATE_DAYS_MAX .or. d64 < DATE_DAYS_MIN) then
            error stop EP//"date out of range in parquet_date%set (beyond +-5.8 million years)"
        end if
        self%days = int(d64, int32)
        self%valid = .true.
    end subroutine date_set
    !
    !> Returns the civil fields of the element; aborts on a null element.
    elemental subroutine date_get(self, year, month, day)
        class(parquet_date), intent(in) :: self !! the element.
        integer(int32), intent(out) :: year     !! calendar year.
        integer(int32), intent(out) :: month    !! month, 1..12.
        integer(int32), intent(out) :: day      !! day of month.
        integer(int64) :: y
        integer :: m, d
        if (.not. self%valid) then
            error stop EP//"null parquet_date element accessed in get (guard with is_null)"
        end if
        call civil_from_days(int(self%days, int64), y, m, d)
        year = int(y, int32)
        month = int(m, int32)
        day = int(d, int32)
    end subroutine date_get
    !
    !> Returns the calendar year; aborts on a null element.
    elemental integer(int32) function date_year(self) result(res)
        class(parquet_date), intent(in) :: self !! the element.
        integer(int64) :: y
        integer :: m, d
        if (.not. self%valid) then
            error stop EP//"null parquet_date element accessed in year (guard with is_null)"
        end if
        call civil_from_days(int(self%days, int64), y, m, d)
        res = int(y, int32)
    end function date_year
    !
    !> Returns the calendar month (1..12); aborts on a null element.
    elemental integer(int32) function date_month(self) result(res)
        class(parquet_date), intent(in) :: self !! the element.
        integer(int64) :: y
        integer :: m, d
        if (.not. self%valid) then
            error stop EP//"null parquet_date element accessed in month (guard with is_null)"
        end if
        call civil_from_days(int(self%days, int64), y, m, d)
        res = int(m, int32)
    end function date_month
    !
    !> Returns the day of month (1..31); aborts on a null element.
    elemental integer(int32) function date_day(self) result(res)
        class(parquet_date), intent(in) :: self !! the element.
        integer(int64) :: y
        integer :: m, d
        if (.not. self%valid) then
            error stop EP//"null parquet_date element accessed in day (guard with is_null)"
        end if
        call civil_from_days(int(self%days, int64), y, m, d)
        res = int(d, int32)
    end function date_day
    !
    !> Returns whether the element is null (the primary null guard; never aborts).
    elemental logical function date_is_null(self) result(res)
        class(parquet_date), intent(in) :: self !! the element.
        res = .not. self%valid
    end function date_is_null
    !
    !> Marks the element null.
    impure elemental subroutine date_set_null(self)
        class(parquet_date), intent(inout) :: self !! the element (reset to the null state).
        self%days = 0_int32
        self%valid = .false.
    end subroutine date_set_null
    !
    !> Sets the raw day count directly (interop/advanced accessor; marks the element valid).
    impure elemental subroutine date_set_raw(self, days)
        class(parquet_date), intent(inout) :: self !! receives the value (marked valid).
        integer(int32), intent(in) :: days       !! days since 1970-01-01 (the Parquet DATE value).
        self%days = days
        self%valid = .true.
    end subroutine date_set_raw
    !
    !> Returns the raw day count, or 0 for a null element (interop/advanced accessor; never
    !! aborts -- validity travels separately via is_null).
    elemental integer(int32) function date_raw(self) result(res)
        class(parquet_date), intent(in) :: self !! the element.
        res = 0_int32
        if (self%valid) res = self%days
    end function date_raw
    !
    !> int32 specific of set_mjd; see the set_mjd generic.
    impure elemental subroutine date_set_mjd_i32(self, mjd)
        class(parquet_date), intent(inout) :: self !! receives the date (marked valid).
        integer(int32), intent(in) :: mjd        !! integer Modified Julian Date.
        call date_set_mjd_i64(self, int(mjd, int64))
    end subroutine date_set_mjd_i32
    !
    !> int64 specific of set_mjd: sets the element from an integer Modified Julian Date
    !! (MJD 0 = 1858-11-17); aborts if out of the representable range.
    impure elemental subroutine date_set_mjd_i64(self, mjd)
        class(parquet_date), intent(inout) :: self !! receives the date (marked valid).
        integer(int64), intent(in) :: mjd        !! integer Modified Julian Date.
        integer(int64) :: d64
        d64 = mjd - MJD_UNIX_EPOCH
        if (d64 > DATE_DAYS_MAX .or. d64 < DATE_DAYS_MIN) then
            error stop EP//"MJD out of range in parquet_date%set_mjd"
        end if
        self%days = int(d64, int32)
        self%valid = .true.
    end subroutine date_set_mjd_i64
    !
    !> Returns the integer Modified Julian Date (MJD 0 = 1858-11-17); aborts on a null element.
    elemental integer(int64) function date_to_mjd(self) result(res)
        class(parquet_date), intent(in) :: self !! the element.
        if (.not. self%valid) then
            error stop EP//"null parquet_date element accessed in to_mjd (guard with is_null)"
        end if
        res = int(self%days, int64) + MJD_UNIX_EPOCH
    end function date_to_mjd
    !
    !> Writes the element as ISO-8601 "YYYY-MM-DD" (years 0..9999 zero-padded to 4 digits;
    !! a leading '-' and more digits otherwise); aborts on a null element. A subroutine (not a
    !! function) so this never returns `character(len=:), allocatable` as a function result --
    !! see "Build and compiler notes" in CLAUDE.md for why.
    subroutine date_to_string(self, str)
        class(parquet_date), intent(in) :: self           !! the element.
        character(len=:), allocatable, intent(out) :: str !! receives the formatted date.
        character(len=32) :: buf
        integer(int64) :: y
        integer :: m, d, pos
        if (.not. self%valid) then
            error stop EP//"null parquet_date element accessed in to_string (guard with is_null)"
        end if
        call civil_from_days(int(self%days, int64), y, m, d)
        call format_date_fields(y, m, d, buf, pos)
        str = buf(1:pos)
    end subroutine date_to_string
    !
    !> Sets the element from an ISO-8601 date string "[-]YYYY-MM-DD". On failure (not parseable
    !! as a valid civil date): error stop by default; if `success` is present, no abort happens
    !! -- `success` is set .false. and the element is left null (a defined state) instead.
    !! (This procedure's header line -- and five others in this file: date_new, time_parse,
    !! time_new, ts_parse, ts_new_civil -- never register as "hit" in gcov, even though every
    !! other line of each one's body does, including their own `error stop` lines, which only
    !! execute on the actual abort path this file's error-scenario tests specifically trigger.
    !! That proves each procedure genuinely runs; the header line itself just isn't attributed
    !! a hit by gcov for these six specifically (no common dummy-argument shape distinguishes
    !! them from this file's other, correctly-attributed procedure headers -- e.g. ts_set_civil,
    !! same `impure elemental` + optional-argument shape, IS attributed normally). Treated as
    !! the same class of line-attribution artifact as the documented end module/end submodule
    !! exclusion in CLAUDE.md, not a real gap -- see the coverage note in feature_temporal.md.)
    impure elemental subroutine date_parse(self, str, success) ! GCOVR_EXCL_LINE
        class(parquet_date), intent(out) :: self  !! receives the parsed date (or null on caught failure).
        character(len=*), intent(in) :: str       !! ISO-8601 date string.
        logical, intent(out), optional :: success !! .true. on success; absent => abort on failure.
        integer(int64) :: y, d64
        integer :: m, d
        logical :: ok
        call parse_date_fields(str, y, m, d, ok)
        if (ok) then
            d64 = days_from_civil(y, m, d)
            ok = d64 <= DATE_DAYS_MAX .and. d64 >= DATE_DAYS_MIN
        end if
        if (.not. ok) then
            if (present(success)) then
                success = .false.
                return
            end if
            error stop EP//"not a valid ISO-8601 date (expected YYYY-MM-DD) in parquet_date%parse"
        end if
        self%days = int(d64, int32)
        self%valid = .true.
        if (present(success)) success = .true.
    end subroutine date_parse
    !
    !> Equality specific; see the operator(==) generic. Aborts on a null operand.
    elemental logical function date_eq(a, b) result(res)
        class(parquet_date), intent(in) :: a !! left operand.
        class(parquet_date), intent(in) :: b !! right operand.
        if (.not. (a%valid .and. b%valid)) then
            error stop EP//"comparison with a null parquet_date element (guard with is_null)"
        end if
        res = a%days == b%days
    end function date_eq
    !
    !> Inequality specific; see the operator(/=) generic. Aborts on a null operand.
    elemental logical function date_ne(a, b) result(res)
        class(parquet_date), intent(in) :: a !! left operand.
        class(parquet_date), intent(in) :: b !! right operand.
        res = .not. date_eq(a, b)
    end function date_ne
    !
    !> Less-than specific; see the operator(<) generic. Aborts on a null operand.
    elemental logical function date_lt(a, b) result(res)
        class(parquet_date), intent(in) :: a !! left operand.
        class(parquet_date), intent(in) :: b !! right operand.
        if (.not. (a%valid .and. b%valid)) then
            error stop EP//"comparison with a null parquet_date element (guard with is_null)"
        end if
        res = a%days < b%days
    end function date_lt
    !
    !> Less-or-equal specific; see the operator(<=) generic. Aborts on a null operand.
    elemental logical function date_le(a, b) result(res)
        class(parquet_date), intent(in) :: a !! left operand.
        class(parquet_date), intent(in) :: b !! right operand.
        res = .not. date_lt(b, a)
    end function date_le
    !
    !> Greater-than specific; see the operator(>) generic. Aborts on a null operand.
    elemental logical function date_gt(a, b) result(res)
        class(parquet_date), intent(in) :: a !! left operand.
        class(parquet_date), intent(in) :: b !! right operand.
        res = date_lt(b, a)
    end function date_gt
    !
    !> Greater-or-equal specific; see the operator(>=) generic. Aborts on a null operand.
    elemental logical function date_ge(a, b) result(res)
        class(parquet_date), intent(in) :: a !! left operand.
        class(parquet_date), intent(in) :: b !! right operand.
        res = .not. date_lt(a, b)
    end function date_ge
    !
    !> Constructor specific for the parquet_date generic: builds a valid date element from
    !! (year, month, day); aborts on an invalid civil date.
    impure elemental function date_new(year, month, day) result(res) ! GCOVR_EXCL_LINE
        integer(int32), intent(in) :: year  !! calendar year.
        integer(int32), intent(in) :: month !! month, 1..12.
        integer(int32), intent(in) :: day   !! day of month.
        type(parquet_date) :: res           !! the constructed element (valid).
        call date_set(res, year, month, day)
    end function date_new
    !
    !> Difference specific; see the operator(-) generic. Whole days, exact; aborts on a null
    !! operand.
    elemental integer(int64) function date_diff(a, b) result(res)
        class(parquet_date), intent(in) :: a !! left (later) operand.
        class(parquet_date), intent(in) :: b !! right (earlier) operand.
        if (.not. (a%valid .and. b%valid)) then
            error stop EP//"difference with a null parquet_date element (guard with is_null)"
        end if
        res = int(a%days, int64) - int(b%days, int64)
    end function date_diff
    !
    !> Shared worker for the day-offset operator(+)/operator(-) specifics: shifts self by
    !! `delta` whole days, aborting on a null operand or an out-of-range result (reusing
    !! date_set's own +-5.8 million year bound).
    impure elemental function date_offset_days_impl(self, delta) result(res)
        class(parquet_date), intent(in) :: self !! the element to shift.
        integer(int64), intent(in) :: delta     !! signed day offset (+ forward, - backward).
        type(parquet_date) :: res               !! self shifted by delta days.
        integer(int64) :: new_days
        if (.not. self%valid) then
            error stop EP//"null parquet_date element accessed in operator(+)/operator(-) (guard with is_null)"
        end if
        if (delta >= 0_int64) then
            if (int(self%days, int64) > huge(0_int64) - delta) then
                error stop EP//"date out of range in parquet_date operator(+)/operator(-) (beyond +-5.8 million years)"
            end if
        else
            if (int(self%days, int64) < offset_floor(delta)) then
                error stop EP//"date out of range in parquet_date operator(+)/operator(-) (beyond +-5.8 million years)"
            end if
        end if
        new_days = int(self%days, int64) + delta
        if (new_days > DATE_DAYS_MAX .or. new_days < DATE_DAYS_MIN) then
            error stop EP//"date out of range in parquet_date operator(+)/operator(-) (beyond +-5.8 million years)"
        end if
        res%days = int(new_days, int32)
        res%valid = .true.
    end function date_offset_days_impl
    !
    !> int32 specific of operator(-); see date_offset_days_impl.
    impure elemental function date_sub_days_i32(self, n) result(res)
        class(parquet_date), intent(in) :: self !! the element to shift.
        integer(int32), intent(in) :: n         !! days to subtract.
        type(parquet_date) :: res               !! self shifted back by n days.
        res = date_offset_days_impl(self, -int(n, int64))
    end function date_sub_days_i32
    !
    !> int64 specific of operator(-); see date_offset_days_impl.
    impure elemental function date_sub_days_i64(self, n) result(res)
        class(parquet_date), intent(in) :: self !! the element to shift.
        integer(int64), intent(in) :: n         !! days to subtract.
        type(parquet_date) :: res               !! self shifted back by n days.
        if (n == INT64_MIN) then
            error stop EP//"date out of range in parquet_date operator(-) (beyond +-5.8 million years)"
        end if
        res = date_offset_days_impl(self, -n)
    end function date_sub_days_i64
    !
    !> int32 specific of operator(+); see date_offset_days_impl.
    impure elemental function date_add_days_i32(self, n) result(res)
        class(parquet_date), intent(in) :: self !! the element to shift.
        integer(int32), intent(in) :: n         !! days to add.
        type(parquet_date) :: res               !! self shifted forward by n days.
        res = date_offset_days_impl(self, int(n, int64))
    end function date_add_days_i32
    !
    !> int64 specific of operator(+); see date_offset_days_impl.
    impure elemental function date_add_days_i64(self, n) result(res)
        class(parquet_date), intent(in) :: self !! the element to shift.
        integer(int64), intent(in) :: n         !! days to add.
        type(parquet_date) :: res               !! self shifted forward by n days.
        res = date_offset_days_impl(self, n)
    end function date_add_days_i64
    !
    ! ==================================================================================
    ! parquet_time
    ! ==================================================================================
    !
    !> Sets the element from validated time-of-day fields; marks it valid. Leap seconds
    !! (second == 60) are not representable (Parquet/Arrow TIME does not support them).
    impure elemental subroutine time_set(self, hour, minute, second, nanosecond)
        class(parquet_time), intent(inout) :: self         !! receives the time (marked valid).
        integer(int32), intent(in) :: hour               !! hour, 0..23.
        integer(int32), intent(in) :: minute             !! minute, 0..59.
        integer(int32), intent(in) :: second             !! second, 0..59.
        integer(int32), intent(in), optional :: nanosecond !! sub-second part, 0..999999999 (default 0).
        integer(int64) :: ns
        if (hour < 0 .or. hour > 23) error stop EP//"invalid hour in parquet_time%set (must be 0..23)"
        if (minute < 0 .or. minute > 59) error stop EP//"invalid minute in parquet_time%set (must be 0..59)"
        if (second < 0 .or. second > 59) error stop EP//"invalid second in parquet_time%set (must be 0..59)"
        ns = 0_int64
        if (present(nanosecond)) then
            if (nanosecond < 0 .or. int(nanosecond, int64) >= NS_PER_SECOND) then
                error stop EP//"invalid nanosecond in parquet_time%set (must be 0..999999999)"
            end if
            ns = int(nanosecond, int64)
        end if
        self%nanoseconds = (int(hour, int64)*3600_int64 + int(minute, int64)*60_int64 &
            + int(second, int64))*NS_PER_SECOND + ns
        self%valid = .true.
    end subroutine time_set
    !
    !> Returns the time-of-day fields of the element; aborts on a null element.
    elemental subroutine time_get(self, hour, minute, second, nanosecond)
        class(parquet_time), intent(in) :: self           !! the element.
        integer(int32), intent(out) :: hour               !! hour, 0..23.
        integer(int32), intent(out) :: minute             !! minute, 0..59.
        integer(int32), intent(out) :: second             !! second, 0..59.
        integer(int32), intent(out), optional :: nanosecond !! sub-second part, 0..999999999.
        integer(int64) :: sod
        if (.not. self%valid) then
            error stop EP//"null parquet_time element accessed in get (guard with is_null)"
        end if
        sod = self%nanoseconds/NS_PER_SECOND
        hour = int(sod/3600_int64, int32)
        minute = int(mod(sod/60_int64, 60_int64), int32)
        second = int(mod(sod, 60_int64), int32)
        if (present(nanosecond)) nanosecond = int(mod(self%nanoseconds, NS_PER_SECOND), int32)
    end subroutine time_get
    !
    !> Returns the hour (0..23); aborts on a null element.
    elemental integer(int32) function time_hour(self) result(res)
        class(parquet_time), intent(in) :: self !! the element.
        if (.not. self%valid) then
            error stop EP//"null parquet_time element accessed in hour (guard with is_null)"
        end if
        res = int(self%nanoseconds/(3600_int64*NS_PER_SECOND), int32)
    end function time_hour
    !
    !> Returns the minute (0..59); aborts on a null element.
    elemental integer(int32) function time_minute(self) result(res)
        class(parquet_time), intent(in) :: self !! the element.
        if (.not. self%valid) then
            error stop EP//"null parquet_time element accessed in minute (guard with is_null)"
        end if
        res = int(mod(self%nanoseconds/(60_int64*NS_PER_SECOND), 60_int64), int32)
    end function time_minute
    !
    !> Returns the second (0..59); aborts on a null element.
    elemental integer(int32) function time_second(self) result(res)
        class(parquet_time), intent(in) :: self !! the element.
        if (.not. self%valid) then
            error stop EP//"null parquet_time element accessed in second (guard with is_null)"
        end if
        res = int(mod(self%nanoseconds/NS_PER_SECOND, 60_int64), int32)
    end function time_second
    !
    !> Returns the sub-second part in nanoseconds (0..999999999); aborts on a null element.
    elemental integer(int32) function time_nanosecond(self) result(res)
        class(parquet_time), intent(in) :: self !! the element.
        if (.not. self%valid) then
            error stop EP//"null parquet_time element accessed in nanosecond (guard with is_null)"
        end if
        res = int(mod(self%nanoseconds, NS_PER_SECOND), int32)
    end function time_nanosecond
    !
    !> Returns whether the element is null (the primary null guard; never aborts).
    elemental logical function time_is_null(self) result(res)
        class(parquet_time), intent(in) :: self !! the element.
        res = .not. self%valid
    end function time_is_null
    !
    !> Marks the element null.
    impure elemental subroutine time_set_null(self)
        class(parquet_time), intent(inout) :: self !! the element (reset to the null state).
        self%nanoseconds = 0_int64
        self%valid = .false.
    end subroutine time_set_null
    !
    !> Sets the raw nanoseconds-since-midnight directly (interop/advanced accessor; marks the
    !! element valid); aborts on a value outside a day.
    impure elemental subroutine time_set_raw(self, nanoseconds)
        class(parquet_time), intent(inout) :: self  !! receives the value (marked valid).
        integer(int64), intent(in) :: nanoseconds !! nanoseconds since midnight, [0, 86400e9 - 1].
        if (nanoseconds < 0_int64 .or. nanoseconds >= NS_PER_DAY) then
            error stop EP//"nanoseconds-of-day out of range in parquet_time%set_raw"
        end if
        self%nanoseconds = nanoseconds
        self%valid = .true.
    end subroutine time_set_raw
    !
    !> Returns the raw nanoseconds-since-midnight, or 0 for a null element (interop/advanced
    !! accessor; never aborts -- validity travels separately via is_null).
    elemental integer(int64) function time_raw(self) result(res)
        class(parquet_time), intent(in) :: self !! the element.
        res = 0_int64
        if (self%valid) res = self%nanoseconds
    end function time_raw
    !
    !> Writes the element as ISO-8601 "HH:MM:SS[.fraction]" (fraction omitted when zero, else
    !! the shortest of 3/6/9 digits that is exact); aborts on a null element. A subroutine for
    !! the same reason as parquet_date%to_string.
    subroutine time_to_string(self, str)
        class(parquet_time), intent(in) :: self           !! the element.
        character(len=:), allocatable, intent(out) :: str !! receives the formatted time.
        character(len=24) :: buf
        integer :: pos
        if (.not. self%valid) then
            error stop EP//"null parquet_time element accessed in to_string (guard with is_null)"
        end if
        pos = 0
        call format_time_fields(self%nanoseconds, buf, pos)
        call format_fraction(mod(self%nanoseconds, NS_PER_SECOND), buf, pos)
        str = buf(1:pos)
    end subroutine time_to_string
    !
    !> Sets the element from an ISO-8601 time string "HH:MM:SS[.fraction]" (1..9 fraction
    !! digits). Failure handling matches parquet_date%parse: abort by default, or report via
    !! the optional `success` (leaving the element null).
    impure elemental subroutine time_parse(self, str, success) ! GCOVR_EXCL_LINE
        class(parquet_time), intent(out) :: self  !! receives the parsed time (or null on caught failure).
        character(len=*), intent(in) :: str       !! ISO-8601 time string.
        logical, intent(out), optional :: success !! .true. on success; absent => abort on failure.
        integer :: h, mi, s
        integer(int64) :: ns
        logical :: ok
        call parse_time_fields(str, h, mi, s, ns, ok)
        if (.not. ok) then
            if (present(success)) then
                success = .false.
                return
            end if
            error stop EP//"not a valid ISO-8601 time (expected HH:MM:SS[.fraction]) in parquet_time%parse"
        end if
        self%nanoseconds = (int(h, int64)*3600_int64 + int(mi, int64)*60_int64 &
            + int(s, int64))*NS_PER_SECOND + ns
        self%valid = .true.
        if (present(success)) success = .true.
    end subroutine time_parse
    !
    !> Equality specific; see the operator(==) generic. Aborts on a null operand.
    elemental logical function time_eq(a, b) result(res)
        class(parquet_time), intent(in) :: a !! left operand.
        class(parquet_time), intent(in) :: b !! right operand.
        if (.not. (a%valid .and. b%valid)) then
            error stop EP//"comparison with a null parquet_time element (guard with is_null)"
        end if
        res = a%nanoseconds == b%nanoseconds
    end function time_eq
    !
    !> Inequality specific; see the operator(/=) generic. Aborts on a null operand.
    elemental logical function time_ne(a, b) result(res)
        class(parquet_time), intent(in) :: a !! left operand.
        class(parquet_time), intent(in) :: b !! right operand.
        res = .not. time_eq(a, b)
    end function time_ne
    !
    !> Less-than specific; see the operator(<) generic. Aborts on a null operand.
    elemental logical function time_lt(a, b) result(res)
        class(parquet_time), intent(in) :: a !! left operand.
        class(parquet_time), intent(in) :: b !! right operand.
        if (.not. (a%valid .and. b%valid)) then
            error stop EP//"comparison with a null parquet_time element (guard with is_null)"
        end if
        res = a%nanoseconds < b%nanoseconds
    end function time_lt
    !
    !> Less-or-equal specific; see the operator(<=) generic. Aborts on a null operand.
    elemental logical function time_le(a, b) result(res)
        class(parquet_time), intent(in) :: a !! left operand.
        class(parquet_time), intent(in) :: b !! right operand.
        res = .not. time_lt(b, a)
    end function time_le
    !
    !> Greater-than specific; see the operator(>) generic. Aborts on a null operand.
    elemental logical function time_gt(a, b) result(res)
        class(parquet_time), intent(in) :: a !! left operand.
        class(parquet_time), intent(in) :: b !! right operand.
        res = time_lt(b, a)
    end function time_gt
    !
    !> Greater-or-equal specific; see the operator(>=) generic. Aborts on a null operand.
    elemental logical function time_ge(a, b) result(res)
        class(parquet_time), intent(in) :: a !! left operand.
        class(parquet_time), intent(in) :: b !! right operand.
        res = .not. time_lt(a, b)
    end function time_ge
    !
    !> Constructor specific for the parquet_time generic: builds a valid time element from
    !! (hour, minute, second[, nanosecond]); aborts on invalid fields.
    impure elemental function time_new(hour, minute, second, nanosecond) result(res) ! GCOVR_EXCL_LINE
        integer(int32), intent(in) :: hour                 !! hour, 0..23.
        integer(int32), intent(in) :: minute               !! minute, 0..59.
        integer(int32), intent(in) :: second               !! second, 0..59.
        integer(int32), intent(in), optional :: nanosecond !! sub-second part, 0..999999999 (default 0).
        type(parquet_time) :: res                          !! the constructed element (valid).
        call time_set(res, hour, minute, second, nanosecond)
    end function time_new
    !
    !> Difference specific; see the operator(-) generic. Nanoseconds, exact (bounded to +-1 day
    !! of ns since both operands lie in [0, 86400e9 - 1]); aborts on a null operand.
    elemental integer(int64) function time_diff(a, b) result(res)
        class(parquet_time), intent(in) :: a !! left (later) operand.
        class(parquet_time), intent(in) :: b !! right (earlier) operand.
        if (.not. (a%valid .and. b%valid)) then
            error stop EP//"difference with a null parquet_time element (guard with is_null)"
        end if
        res = a%nanoseconds - b%nanoseconds
    end function time_diff
    !
    !> Shared worker for the ns-offset operator(+)/operator(-) specifics: shifts self by
    !! `offset` nanoseconds and always wraps into a valid time-of-day, [0, 86400e9 - 1] --
    !! `offset` itself must already be validated to fit in [-86400e9, 86400e9] by the caller
    !! (the 24h magnitude guard), so this worker never aborts once entered. Uses floor_div-style
    !! floored modulo, not the intrinsic MOD, so a negative offset wraps correctly (see
    !! CLAUDE.md's "Implementation notes" for feature_temporal.md).
    impure elemental function time_offset_ns_impl(self, offset) result(res)
        class(parquet_time), intent(in) :: self !! the element to shift.
        integer(int64), intent(in) :: offset    !! signed ns offset, already within [-86400e9, 86400e9].
        type(parquet_time) :: res               !! self shifted by offset ns, wrapped to a valid time-of-day.
        integer(int64) :: raw
        if (.not. self%valid) then
            error stop EP//"null parquet_time element accessed in operator(+)/operator(-) (guard with is_null)"
        end if
        raw = self%nanoseconds + offset
        res%nanoseconds = raw - floor_div(raw, NS_PER_DAY)*NS_PER_DAY
        res%valid = .true.
    end function time_offset_ns_impl
    !
    !> int32 specific of operator(-); see time_offset_ns_impl. The 24h magnitude guard is moot
    !! for this kind (int32's own range never reaches 86400e9 ns), kept only for symmetry with
    !! the int64 specific.
    impure elemental function time_sub_ns_i32(self, n) result(res)
        class(parquet_time), intent(in) :: self !! the element to shift.
        integer(int32), intent(in) :: n         !! ns to subtract.
        type(parquet_time) :: res               !! self shifted back by n ns, wrapped.
        res = time_offset_ns_impl(self, -int(n, int64))
    end function time_sub_ns_i32
    !
    !> int64 specific of operator(-); see time_offset_ns_impl. Aborts if abs(n) exceeds 24h of
    !! nanoseconds (checked on the raw, not-yet-negated input, so negating it afterwards can
    !! never overflow).
    impure elemental function time_sub_ns_i64(self, n) result(res)
        class(parquet_time), intent(in) :: self !! the element to shift.
        integer(int64), intent(in) :: n         !! ns to subtract.
        type(parquet_time) :: res               !! self shifted back by n ns, wrapped.
        if (n > NS_PER_DAY .or. n < -NS_PER_DAY) then
            error stop EP//"offset magnitude exceeds 24 hours in parquet_time operator(-)"
        end if
        res = time_offset_ns_impl(self, -n)
    end function time_sub_ns_i64
    !
    !> int32 specific of operator(+); see time_offset_ns_impl. The 24h magnitude guard is moot
    !! for this kind (int32's own range never reaches 86400e9 ns), kept only for symmetry with
    !! the int64 specific.
    impure elemental function time_add_ns_i32(self, n) result(res)
        class(parquet_time), intent(in) :: self !! the element to shift.
        integer(int32), intent(in) :: n         !! ns to add.
        type(parquet_time) :: res               !! self shifted forward by n ns, wrapped.
        res = time_offset_ns_impl(self, int(n, int64))
    end function time_add_ns_i32
    !
    !> int64 specific of operator(+); see time_offset_ns_impl. Aborts if abs(n) exceeds 24h of
    !! nanoseconds.
    impure elemental function time_add_ns_i64(self, n) result(res)
        class(parquet_time), intent(in) :: self !! the element to shift.
        integer(int64), intent(in) :: n         !! ns to add.
        type(parquet_time) :: res               !! self shifted forward by n ns, wrapped.
        if (n > NS_PER_DAY .or. n < -NS_PER_DAY) then
            error stop EP//"offset magnitude exceeds 24 hours in parquet_time operator(+)"
        end if
        res = time_offset_ns_impl(self, n)
    end function time_add_ns_i64
    !
    ! ==================================================================================
    ! parquet_timestamp
    ! ==================================================================================
    !
    !> Civil-fields specific of the set generic: sets the element from
    !! (year, month, day, hour, minute, second[, nanosecond]), validated; marks it valid.
    impure elemental subroutine ts_set_civil(self, year, month, day, hour, minute, second, nanosecond)
        class(parquet_timestamp), intent(inout) :: self      !! receives the instant (marked valid).
        integer(int32), intent(in) :: year                 !! calendar year.
        integer(int32), intent(in) :: month                !! month, 1..12.
        integer(int32), intent(in) :: day                  !! day of month.
        integer(int32), intent(in) :: hour                 !! hour, 0..23.
        integer(int32), intent(in) :: minute               !! minute, 0..59.
        integer(int32), intent(in) :: second               !! second, 0..59.
        integer(int32), intent(in), optional :: nanosecond !! sub-second part, 0..999999999 (default 0).
        integer(int64) :: d64, ns
        if (month < 1 .or. month > 12) then
            error stop EP//"invalid month in parquet_timestamp%set (must be 1..12)"
        end if
        if (day < 1 .or. day > days_in_month(int(year, int64), int(month))) then
            error stop EP//"invalid day of month in parquet_timestamp%set"
        end if
        if (hour < 0 .or. hour > 23) error stop EP//"invalid hour in parquet_timestamp%set (must be 0..23)"
        if (minute < 0 .or. minute > 59) error stop EP//"invalid minute in parquet_timestamp%set (must be 0..59)"
        if (second < 0 .or. second > 59) error stop EP//"invalid second in parquet_timestamp%set (must be 0..59)"
        ns = 0_int64
        if (present(nanosecond)) then
            if (nanosecond < 0 .or. int(nanosecond, int64) >= NS_PER_SECOND) then
                error stop EP//"invalid nanosecond in parquet_timestamp%set (must be 0..999999999)"
            end if
            ns = int(nanosecond, int64)
        end if
        d64 = days_from_civil(int(year, int64), int(month), int(day))
        self%seconds = d64*SECONDS_PER_DAY + int(hour, int64)*3600_int64 &
            + int(minute, int64)*60_int64 + int(second, int64)
        self%nanoseconds = int(ns, int32)
        self%valid = .true.
    end subroutine ts_set_civil
    !
    !> Date-plus-time specific of the set generic: combines a parquet_date and a parquet_time
    !! into an instant. A null input propagates: the result is null (never aborts).
    impure elemental subroutine ts_set_date_time(self, date, time)
        class(parquet_timestamp), intent(out) :: self !! receives the instant (or null if an input is null).
        type(parquet_date), intent(in) :: date        !! the calendar-date part.
        type(parquet_time), intent(in) :: time        !! the time-of-day part.
        if (.not. (date%valid .and. time%valid)) return
        self%seconds = int(date%days, int64)*SECONDS_PER_DAY + time%nanoseconds/NS_PER_SECOND
        self%nanoseconds = int(mod(time%nanoseconds, NS_PER_SECOND), int32)
        self%valid = .true.
    end subroutine ts_set_date_time
    !
    !> Returns all civil fields of the element; aborts on a null element, and on a year outside
    !! the integer(int32) range (practically unreachable for real data).
    elemental subroutine ts_get(self, year, month, day, hour, minute, second, nanosecond)
        class(parquet_timestamp), intent(in) :: self        !! the element.
        integer(int32), intent(out) :: year                 !! calendar year.
        integer(int32), intent(out) :: month                !! month, 1..12.
        integer(int32), intent(out) :: day                  !! day of month.
        integer(int32), intent(out) :: hour                 !! hour, 0..23.
        integer(int32), intent(out) :: minute               !! minute, 0..59.
        integer(int32), intent(out) :: second               !! second, 0..59.
        integer(int32), intent(out), optional :: nanosecond !! sub-second part, 0..999999999.
        integer(int64) :: d64, sod, y
        integer :: m, d
        if (.not. self%valid) then
            error stop EP//"null parquet_timestamp element accessed in get (guard with is_null)"
        end if
        d64 = floor_div(self%seconds, SECONDS_PER_DAY)
        sod = self%seconds - d64*SECONDS_PER_DAY
        call civil_from_days(d64, y, m, d)
        if (y > int(huge(0_int32), int64) .or. y < -int(huge(0_int32), int64) - 1_int64) then
            error stop EP//"year out of integer(int32) range in parquet_timestamp%get"
        end if
        year = int(y, int32)
        month = int(m, int32)
        day = int(d, int32)
        hour = int(sod/3600_int64, int32)
        minute = int(mod(sod/60_int64, 60_int64), int32)
        second = int(mod(sod, 60_int64), int32)
        if (present(nanosecond)) nanosecond = self%nanoseconds
    end subroutine ts_get
    !
    !> Returns the calendar-date part as a parquet_date. A null element propagates to a null
    !! result (never aborts on null); aborts only if the date falls outside parquet_date's
    !! representable range (practically unreachable for real data).
    elemental function ts_get_date(self) result(res)
        class(parquet_timestamp), intent(in) :: self !! the element.
        type(parquet_date) :: res                    !! the date part (null if the element is null).
        integer(int64) :: d64
        if (.not. self%valid) return
        d64 = floor_div(self%seconds, SECONDS_PER_DAY)
        if (d64 > DATE_DAYS_MAX .or. d64 < DATE_DAYS_MIN) then
            error stop EP//"date part out of parquet_date range in parquet_timestamp%get_date"
        end if
        res%days = int(d64, int32)
        res%valid = .true.
    end function ts_get_date
    !
    !> Returns the time-of-day part as a parquet_time. A null element propagates to a null
    !! result (never aborts).
    elemental function ts_get_time(self) result(res)
        class(parquet_timestamp), intent(in) :: self !! the element.
        type(parquet_time) :: res                    !! the time-of-day part (null if the element is null).
        integer(int64) :: d64, sod
        if (.not. self%valid) return
        d64 = floor_div(self%seconds, SECONDS_PER_DAY)
        sod = self%seconds - d64*SECONDS_PER_DAY
        res%nanoseconds = sod*NS_PER_SECOND + int(self%nanoseconds, int64)
        res%valid = .true.
    end function ts_get_time
    !
    !> Returns whether the element is null (the primary null guard; never aborts).
    elemental logical function ts_is_null(self) result(res)
        class(parquet_timestamp), intent(in) :: self !! the element.
        res = .not. self%valid
    end function ts_is_null
    !
    !> Marks the element null.
    impure elemental subroutine ts_set_null(self)
        class(parquet_timestamp), intent(inout) :: self !! the element (reset to the null state).
        self%seconds = 0_int64
        self%nanoseconds = 0_int32
        self%valid = .false.
    end subroutine ts_set_null
    !
    !> Sets the raw (seconds, nanoseconds) pair directly (interop/advanced accessor; marks the
    !! element valid); aborts on a nanosecond part outside the normalized 0..999999999 range.
    impure elemental subroutine ts_set_raw(self, seconds, nanoseconds)
        class(parquet_timestamp), intent(inout) :: self !! receives the value (marked valid).
        integer(int64), intent(in) :: seconds         !! whole seconds since 1970-01-01T00:00:00.
        integer(int32), intent(in) :: nanoseconds     !! nanosecond-of-second part, 0..999999999.
        if (nanoseconds < 0 .or. int(nanoseconds, int64) >= NS_PER_SECOND) then
            error stop EP//"nanosecond part out of range in parquet_timestamp%set_raw (must be 0..999999999)"
        end if
        self%seconds = seconds
        self%nanoseconds = nanoseconds
        self%valid = .true.
    end subroutine ts_set_raw
    !
    !> Returns the raw (seconds, nanoseconds) pair, or zeros for a null element
    !! (interop/advanced accessor; never aborts -- validity travels separately via is_null).
    elemental subroutine ts_get_raw(self, seconds, nanoseconds)
        class(parquet_timestamp), intent(in) :: self !! the element.
        integer(int64), intent(out) :: seconds       !! whole seconds since 1970-01-01T00:00:00 (0 when null).
        integer(int32), intent(out) :: nanoseconds   !! nanosecond-of-second part (0 when null).
        seconds = 0_int64
        nanoseconds = 0_int32
        if (self%valid) then
            seconds = self%seconds
            nanoseconds = self%nanoseconds
        end if
    end subroutine ts_get_raw
    !
    !> int32 specific of set_unix; see ts_set_unix_i64.
    impure elemental subroutine ts_set_unix_i32(self, value, unit)
        class(parquet_timestamp), intent(inout) :: self !! receives the value (marked valid).
        integer(int32), intent(in) :: value           !! Unix time in `unit`.
        integer, intent(in) :: unit                   !! one of the parquet_unit_* constants.
        call ts_set_unix_i64(self, int(value, int64), unit)
    end subroutine ts_set_unix_i32
    !
    !> int64 specific of set_unix: sets the element from a Unix-time value -- `value` counts
    !! time since 1970-01-01T00:00:00 in `unit`. The unit is a conversion parameter only; it
    !! is not stored (the internal representation is always the canonical seconds+nanoseconds
    !! pair). Always exact; marks the element valid.
    impure elemental subroutine ts_set_unix_i64(self, value, unit)
        class(parquet_timestamp), intent(inout) :: self !! receives the value (marked valid).
        integer(int64), intent(in) :: value           !! Unix time in `unit`.
        integer, intent(in) :: unit                   !! one of the parquet_unit_* constants.
        integer(int64) :: scale, q, r
        scale = unit_scale(unit)
        q = floor_div(value, scale)
        r = value - q*scale
        self%seconds = q
        self%nanoseconds = int(r*(NS_PER_SECOND/scale), int32)
        self%valid = .true.
    end subroutine ts_set_unix_i64
    !
    !> Returns the element as a single Unix-time integer in `unit` -- a pure query, never
    !! modifying the element. Aborts on a null element, on int64 overflow in the requested
    !! unit, and on precision loss (the value carries sub-`unit` precision) unless
    !! exact=.false., which floors toward negative infinity instead (floor, not truncation,
    !! so ordering is preserved across the epoch).
    elemental integer(int64) function ts_to_unix(self, unit, exact) result(res)
        class(parquet_timestamp), intent(in) :: self !! the element (unchanged).
        integer, intent(in) :: unit                  !! one of the parquet_unit_* constants.
        logical, intent(in), optional :: exact       !! default .true.; .false. => floor instead of abort.
        integer(int64) :: scale, ns_per_unit, q, lo_limit
        logical :: need_exact
        if (.not. self%valid) then
            error stop EP//"null parquet_timestamp element accessed in to_unix (guard with is_null)"
        end if
        need_exact = .true.
        if (present(exact)) need_exact = exact
        scale = unit_scale(unit)
        ns_per_unit = NS_PER_SECOND/scale
        if (need_exact .and. mod(int(self%nanoseconds, int64), ns_per_unit) /= 0_int64) then
            error stop EP//"precision loss in parquet_timestamp%to_unix (value has sub-unit precision;"// &
                " pass exact=.false. to floor)"
        end if
        q = int(self%nanoseconds, int64)/ns_per_unit ! 0 <= q < scale
        if (scale > 1_int64) then
            if (self%seconds >= 0_int64) then
                if (self%seconds > (huge(0_int64) - q)/scale) then
                    error stop EP//"overflow in parquet_timestamp%to_unix (value does not fit int64 in this unit)"
                end if
            else
                ! `INT64_MIN/scale`, computed without naming the constant -- see INT64_MIN's own
                ! declaration for why any expression mentioning it is unusable here. `2**63` is
                ! `huge + 1`, so the magnitude is `huge/scale`, plus one exactly when the
                ! remainder is one short of a whole divisor; negating it reproduces Fortran's
                ! truncation toward zero. `scale > 1` is guaranteed by the branch above, which is
                ! what keeps the `+ 1` from overflowing (at `scale == 1` it would be `huge + 1`).
                lo_limit = huge(0_int64)/scale
                if (mod(huge(0_int64), scale) + 1_int64 == scale) lo_limit = lo_limit + 1_int64
                if (self%seconds < -lo_limit) then
                    error stop EP//"overflow in parquet_timestamp%to_unix (value does not fit int64 in this unit)"
                end if
            end if
        end if
        res = self%seconds*scale + q
    end function ts_to_unix
    !
    !> Sets the element from a real64 Modified Julian Date (MJD 0 = 1858-11-17T00:00:00),
    !! rounding to the nearest nanosecond; marks it valid. Aborts on NaN or a magnitude beyond
    !! the representable range. real64 only by design -- real32 would silently lose precision
    !! (consecutive real32 values around a present-day MJD are ~340 s apart) and is deliberately
    !! not accepted.
    impure elemental subroutine ts_set_mjd(self, mjd)
        class(parquet_timestamp), intent(inout) :: self !! receives the instant (marked valid).
        real(real64), intent(in) :: mjd               !! Modified Julian Date (fractional days).
        real(real64) :: dd, frac
        integer(int64) :: d64, ns_of_day
        if (ieee_is_nan(mjd)) error stop EP//"NaN passed to parquet_timestamp%set_mjd"
        if (mjd >= MJD_ABS_BOUND .or. mjd <= -MJD_ABS_BOUND) then
            error stop EP//"MJD out of range in parquet_timestamp%set_mjd"
        end if
        dd = aint(mjd)
        frac = mjd - dd
        d64 = int(dd, int64)
        if (frac < 0.0_real64) then ! aint truncates toward zero; normalize to a [0,1) day fraction
            d64 = d64 - 1_int64
            frac = frac + 1.0_real64
        end if
        ns_of_day = nint(frac*real(NS_PER_DAY, real64), int64)
        if (ns_of_day >= NS_PER_DAY) then ! rounding rolled over to the next day
            d64 = d64 + 1_int64
            ns_of_day = 0_int64
        end if
        self%seconds = (d64 - MJD_UNIX_EPOCH)*SECONDS_PER_DAY + ns_of_day/NS_PER_SECOND
        self%nanoseconds = int(mod(ns_of_day, NS_PER_SECOND), int32)
        self%valid = .true.
    end subroutine ts_set_mjd
    !
    !> Returns the element as a real64 Modified Julian Date; aborts on a null element.
    !! Resolution is limited by real64: ~1 microsecond in the current era -- for exact
    !! nanosecond round-trips use the civil fields or set_unix/to_unix instead.
    elemental real(real64) function ts_to_mjd(self) result(res)
        class(parquet_timestamp), intent(in) :: self !! the element.
        integer(int64) :: d64, sod
        if (.not. self%valid) then
            error stop EP//"null parquet_timestamp element accessed in to_mjd (guard with is_null)"
        end if
        d64 = floor_div(self%seconds, SECONDS_PER_DAY)
        sod = self%seconds - d64*SECONDS_PER_DAY
        res = real(d64 + MJD_UNIX_EPOCH, real64) &
            + (real(sod, real64) + real(self%nanoseconds, real64)*1.0e-9_real64)/real(SECONDS_PER_DAY, real64)
    end function ts_to_mjd
    !
    !> Sets the element from a real64 Julian Date (JD = MJD + 2400000.5); same rounding, range
    !! and real64-only rationale as set_mjd. Note the coarser real64 resolution at JD
    !! magnitudes (~50 microseconds in the current era).
    impure elemental subroutine ts_set_jd(self, jd)
        class(parquet_timestamp), intent(inout) :: self !! receives the instant (marked valid).
        real(real64), intent(in) :: jd                !! Julian Date (fractional days).
        if (ieee_is_nan(jd)) error stop EP//"NaN passed to parquet_timestamp%set_jd"
        call ts_set_mjd(self, jd - JD_MJD_OFFSET)
    end subroutine ts_set_jd
    !
    !> Returns the element as a real64 Julian Date (JD = MJD + 2400000.5); aborts on a null
    !! element. Resolution ~50 microseconds in the current era (real64 at JD magnitudes) --
    !! prefer to_mjd, the civil fields, or to_unix when that matters.
    elemental real(real64) function ts_to_jd(self) result(res)
        class(parquet_timestamp), intent(in) :: self !! the element.
        res = ts_to_mjd(self) + JD_MJD_OFFSET
    end function ts_to_jd
    !
    !> Writes the element as ISO-8601 "YYYY-MM-DDTHH:MM:SS[.fraction]" (year formatting as in
    !! parquet_date%to_string; fraction as in parquet_time%to_string); aborts on a null
    !! element. A subroutine for the same reason as parquet_date%to_string.
    subroutine ts_to_string(self, str)
        class(parquet_timestamp), intent(in) :: self      !! the element.
        character(len=:), allocatable, intent(out) :: str !! receives the formatted date-time.
        character(len=48) :: buf
        integer(int64) :: d64, sod, y
        integer :: m, d, pos
        if (.not. self%valid) then
            error stop EP//"null parquet_timestamp element accessed in to_string (guard with is_null)"
        end if
        d64 = floor_div(self%seconds, SECONDS_PER_DAY)
        sod = self%seconds - d64*SECONDS_PER_DAY
        call civil_from_days(d64, y, m, d)
        call format_date_fields(y, m, d, buf, pos)
        buf(pos+1:pos+1) = 'T'
        pos = pos + 1
        call format_time_fields(sod*NS_PER_SECOND, buf, pos)
        call format_fraction(int(self%nanoseconds, int64), buf, pos)
        str = buf(1:pos)
    end subroutine ts_to_string
    !
    !> Sets the element from an ISO-8601 date-time string
    !! "[-]YYYY-MM-DD{T or space}HH:MM:SS[.fraction][Z]" (an optional trailing 'Z' is accepted
    !! and ignored -- values are stored as epoch offsets regardless). Failure handling matches
    !! parquet_date%parse: abort by default, or report via the optional `success` (leaving the
    !! element null).
    impure elemental subroutine ts_parse(self, str, success) ! GCOVR_EXCL_LINE
        class(parquet_timestamp), intent(out) :: self !! receives the parsed instant (or null on caught failure).
        character(len=*), intent(in) :: str           !! ISO-8601 date-time string.
        logical, intent(out), optional :: success     !! .true. on success; absent => abort on failure.
        integer :: a, b, sep, m, d, h, mi, s
        integer(int64) :: y, ns, d64, sec, sod
        logical :: ok
        ok = .false.
        d64 = 0_int64
        sec = 0_int64
        sod = 0_int64
        b = len_trim(str)
        a = 1
        do while (a <= b)
            if (str(a:a) /= ' ') exit
            a = a + 1
        end do
        if (a <= b) then
            if (str(b:b) == 'Z') b = b - 1
        end if
        sep = 0
        do while (a + sep <= b) ! find the date/time separator ('T' or a single space)
            if (str(a+sep:a+sep) == 'T' .or. str(a+sep:a+sep) == ' ') exit
            sep = sep + 1
        end do
        sep = a + sep
        if (sep > a .and. sep < b) then
            call parse_date_fields(str(a:sep-1), y, m, d, ok)
            if (ok) call parse_time_fields(str(sep+1:b), h, mi, s, ns, ok)
        end if
        if (ok) then ! exact range check: the resulting epoch seconds must fit int64
            d64 = days_from_civil(y, m, d)
            ok = d64 <= PARSE_DAYS_BOUND .and. d64 >= -PARSE_DAYS_BOUND
        end if
        if (ok) then
            sec = d64*SECONDS_PER_DAY
            sod = int(h, int64)*3600_int64 + int(mi, int64)*60_int64 + int(s, int64)
            ok = sec <= huge(0_int64) - sod
        end if
        if (.not. ok) then
            if (present(success)) then
                success = .false.
                return
            end if
            error stop EP//"not a valid ISO-8601 date-time (expected YYYY-MM-DDTHH:MM:SS[.fraction]),"// &
                " or out of the representable range, in parquet_timestamp%parse"
        end if
        self%seconds = sec + sod
        self%nanoseconds = int(ns, int32)
        self%valid = .true.
        if (present(success)) success = .true.
    end subroutine ts_parse
    !
    !> Equality specific; see the operator(==) generic. Aborts on a null operand.
    elemental logical function ts_eq(a, b) result(res)
        class(parquet_timestamp), intent(in) :: a !! left operand.
        class(parquet_timestamp), intent(in) :: b !! right operand.
        if (.not. (a%valid .and. b%valid)) then
            error stop EP//"comparison with a null parquet_timestamp element (guard with is_null)"
        end if
        res = a%seconds == b%seconds .and. a%nanoseconds == b%nanoseconds
    end function ts_eq
    !
    !> Inequality specific; see the operator(/=) generic. Aborts on a null operand.
    elemental logical function ts_ne(a, b) result(res)
        class(parquet_timestamp), intent(in) :: a !! left operand.
        class(parquet_timestamp), intent(in) :: b !! right operand.
        res = .not. ts_eq(a, b)
    end function ts_ne
    !
    !> Less-than specific; see the operator(<) generic. Aborts on a null operand.
    elemental logical function ts_lt(a, b) result(res)
        class(parquet_timestamp), intent(in) :: a !! left operand.
        class(parquet_timestamp), intent(in) :: b !! right operand.
        if (.not. (a%valid .and. b%valid)) then
            error stop EP//"comparison with a null parquet_timestamp element (guard with is_null)"
        end if
        res = a%seconds < b%seconds .or. (a%seconds == b%seconds .and. a%nanoseconds < b%nanoseconds)
    end function ts_lt
    !
    !> Less-or-equal specific; see the operator(<=) generic. Aborts on a null operand.
    elemental logical function ts_le(a, b) result(res)
        class(parquet_timestamp), intent(in) :: a !! left operand.
        class(parquet_timestamp), intent(in) :: b !! right operand.
        res = .not. ts_lt(b, a)
    end function ts_le
    !
    !> Greater-than specific; see the operator(>) generic. Aborts on a null operand.
    elemental logical function ts_gt(a, b) result(res)
        class(parquet_timestamp), intent(in) :: a !! left operand.
        class(parquet_timestamp), intent(in) :: b !! right operand.
        res = ts_lt(b, a)
    end function ts_gt
    !
    !> Greater-or-equal specific; see the operator(>=) generic. Aborts on a null operand.
    elemental logical function ts_ge(a, b) result(res)
        class(parquet_timestamp), intent(in) :: a !! left operand.
        class(parquet_timestamp), intent(in) :: b !! right operand.
        res = .not. ts_lt(a, b)
    end function ts_ge
    !
    !> Civil-fields constructor specific for the parquet_timestamp generic; aborts on invalid
    !! fields.
    impure elemental function ts_new_civil(year, month, day, hour, minute, second, nanosecond) result(res) ! GCOVR_EXCL_LINE
        integer(int32), intent(in) :: year                 !! calendar year.
        integer(int32), intent(in) :: month                !! month, 1..12.
        integer(int32), intent(in) :: day                  !! day of month.
        integer(int32), intent(in) :: hour                 !! hour, 0..23.
        integer(int32), intent(in) :: minute               !! minute, 0..59.
        integer(int32), intent(in) :: second               !! second, 0..59.
        integer(int32), intent(in), optional :: nanosecond !! sub-second part, 0..999999999 (default 0).
        type(parquet_timestamp) :: res                     !! the constructed element (valid).
        call ts_set_civil(res, year, month, day, hour, minute, second, nanosecond)
    end function ts_new_civil
    !
    !> Date-plus-time constructor specific for the parquet_timestamp generic; a null input
    !! propagates to a null result (never aborts).
    impure elemental function ts_new_date_time(date, time) result(res)
        type(parquet_date), intent(in) :: date !! the calendar-date part.
        type(parquet_time), intent(in) :: time !! the time-of-day part.
        type(parquet_timestamp) :: res         !! the constructed element (null if an input is null).
        call ts_set_date_time(res, date, time)
    end function ts_new_date_time
    !
    !> Difference specific; see the operator(-) generic. Nanoseconds, exact; aborts on a null
    !! operand and when the elapsed time exceeds ~292.3 years (TS_DIFF_NS_BOUND_SECONDS) -- the
    !! bound is checked on the seconds component alone, without ever forming an intermediate
    !! value that could itself overflow int64, so the "too far apart to represent" case is
    !! detected before any arithmetic that could silently wrap.
    elemental integer(int64) function ts_diff_ns(a, b) result(res)
        class(parquet_timestamp), intent(in) :: a !! left (later) operand.
        class(parquet_timestamp), intent(in) :: b !! right (earlier) operand.
        integer(int64) :: hi, lo, sec_diff
        if (.not. (a%valid .and. b%valid)) then
            error stop EP//"difference with a null parquet_timestamp element (guard with is_null)"
        end if
        if (b%seconds > huge(0_int64) - TS_DIFF_NS_BOUND_SECONDS) then
            hi = huge(0_int64)
        else
            hi = b%seconds + TS_DIFF_NS_BOUND_SECONDS
        end if
        if (b%seconds < INT64_MIN + TS_DIFF_NS_BOUND_SECONDS) then
            lo = INT64_MIN
        else
            lo = b%seconds - TS_DIFF_NS_BOUND_SECONDS
        end if
        if (a%seconds > hi .or. a%seconds < lo) then
            error stop EP//"parquet_timestamp difference exceeds the representable nanosecond range"// &
                " (elapsed time beyond ~292.3 years)"
        end if
        sec_diff = a%seconds - b%seconds
        res = sec_diff*NS_PER_SECOND + (int(a%nanoseconds, int64) - int(b%nanoseconds, int64))
    end function ts_diff_ns
    !
    !> Type-bound diff_seconds: real64-seconds difference between two instants. Unlike
    !! operator(-) (ts_diff_ns), this never aborts on magnitude -- only precision is lost at
    !! extreme elapsed times, matching real64's own resolution. Aborts on a null operand.
    !! Each operand's seconds component is converted to real64 *before* subtracting (rather
    !! than subtracting as int64 first) -- two instants near opposite ends of the int64 range
    !! would overflow an int64 subtraction, whereas real64 subtraction only loses precision,
    !! matching this function's own "never aborts on magnitude" contract.
    elemental real(real64) function ts_diff_seconds(a, b) result(res)
        class(parquet_timestamp), intent(in) :: a !! left (later) operand.
        class(parquet_timestamp), intent(in) :: b !! right (earlier) operand.
        if (.not. (a%valid .and. b%valid)) then
            error stop EP//"difference with a null parquet_timestamp element (guard with is_null)"
        end if
        res = (real(a%seconds, real64) - real(b%seconds, real64)) &
            + (real(a%nanoseconds, real64) - real(b%nanoseconds, real64))*parquet_ns_to_sec
    end function ts_diff_seconds
    !
    !> Shared worker for the ns-offset operator(+)/operator(-) specifics: shifts self by
    !! `n` nanoseconds, carrying the normalized nanosecond-of-second component back into
    !! [0, 999999999] via floor_div (see CLAUDE.md's "Implementation notes" for
    !! feature_temporal.md). Aborts on a null operand or if the shifted result overflows int64
    !! seconds -- unlike parquet_date/parquet_time, there is no smaller domain-specific range to
    !! enforce, only int64 itself.
    impure elemental function ts_offset_ns_impl(self, n) result(res)
        class(parquet_timestamp), intent(in) :: self !! the instant to shift.
        integer(int64), intent(in) :: n              !! signed ns offset (+ forward, - backward).
        type(parquet_timestamp) :: res                !! self shifted by n ns.
        integer(int64) :: ns_self, total_ns, carry_sec
        if (.not. self%valid) then
            error stop EP//"null parquet_timestamp element accessed in operator(+)/operator(-) (guard with is_null)"
        end if
        ns_self = int(self%nanoseconds, int64)
        if (n >= 0_int64) then
            if (ns_self > huge(0_int64) - n) then
                error stop EP//"parquet_timestamp offset arithmetic overflows int64"
            end if
        end if
        total_ns = ns_self + n
        carry_sec = floor_div(total_ns, NS_PER_SECOND)
        if (carry_sec >= 0_int64) then
            if (self%seconds > huge(0_int64) - carry_sec) then
                error stop EP//"parquet_timestamp offset arithmetic overflows int64 seconds"
            end if
        else
            if (self%seconds < offset_floor(carry_sec)) then
                error stop EP//"parquet_timestamp offset arithmetic overflows int64 seconds"
            end if
        end if
        res%seconds = self%seconds + carry_sec
        res%nanoseconds = int(total_ns - carry_sec*NS_PER_SECOND, int32)
        res%valid = .true.
    end function ts_offset_ns_impl
    !
    !> int32 specific of operator(-); see ts_offset_ns_impl.
    impure elemental function ts_sub_ns_i32(self, n) result(res)
        class(parquet_timestamp), intent(in) :: self !! the instant to shift.
        integer(int32), intent(in) :: n              !! ns to subtract.
        type(parquet_timestamp) :: res                !! self shifted back by n ns.
        res = ts_offset_ns_impl(self, -int(n, int64))
    end function ts_sub_ns_i32
    !
    !> int64 specific of operator(-); see ts_offset_ns_impl.
    impure elemental function ts_sub_ns_i64(self, n) result(res)
        class(parquet_timestamp), intent(in) :: self !! the instant to shift.
        integer(int64), intent(in) :: n              !! ns to subtract.
        type(parquet_timestamp) :: res                !! self shifted back by n ns.
        if (n == INT64_MIN) then
            error stop EP//"parquet_timestamp offset arithmetic overflows int64"
        end if
        res = ts_offset_ns_impl(self, -n)
    end function ts_sub_ns_i64
    !
    !> int32 specific of operator(+); see ts_offset_ns_impl.
    impure elemental function ts_add_ns_i32(self, n) result(res)
        class(parquet_timestamp), intent(in) :: self !! the instant to shift.
        integer(int32), intent(in) :: n              !! ns to add.
        type(parquet_timestamp) :: res                !! self shifted forward by n ns.
        res = ts_offset_ns_impl(self, int(n, int64))
    end function ts_add_ns_i32
    !
    !> int64 specific of operator(+); see ts_offset_ns_impl.
    impure elemental function ts_add_ns_i64(self, n) result(res)
        class(parquet_timestamp), intent(in) :: self !! the instant to shift.
        integer(int64), intent(in) :: n              !! ns to add.
        type(parquet_timestamp) :: res                !! self shifted forward by n ns.
        res = ts_offset_ns_impl(self, n)
    end function ts_add_ns_i64
    !
end module parquet_temporal ! GCOVR_EXCL_LINE
