connections 0.1.0

Galois connections as first-class values: lawful numeric casts, Q-format / IEEE / time ladders
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
//! Timezone-aware `OffsetDateTime` connections.
//!
//! Hosts:
//!
//! - [`ODTMNANO`] — `OffsetDateTime ↔ i128` unix nanoseconds
//!   (lossless across the full default OffsetDateTime range; i64
//!   seconds is too narrow).
//! - [`ODTMSECS`] — `OffsetDateTime ↔ i64` unix whole seconds with
//!   sub-second rounding (analog of [`TIMESECS`](super::TIMESECS)
//!   for the clock domain).
//!
//! `OffsetDateTime` equality and ordering in `time` 0.3 are
//! **instant**-based (the `Ord` impl re-bases the right-hand side
//! to the left-hand side's offset before comparing, which collapses
//! same-instant-different-offset values into a single equivalence
//! class). The conns therefore treat any two OffsetDateTime values
//! representing the same instant as identical, regardless of their
//! offset variant.
//!
//! A planned `UTCOSECS` connection (`UtcOffset ↔ i32` total seconds)
//! is **deferred**: `UtcOffset`'s `Ord` impl in `time` 0.3 orders by
//! a packed `as_u32()` byte representation (a hash-key implementation
//! detail), not by `whole_seconds`. The Galois machinery requires the
//! source-side `PartialOrd` to align with the rung's natural order,
//! which `UtcOffset` doesn't provide. Future sprint should introduce
//! a `UtcOffsetSecs(UtcOffset)` newtype with `PartialOrd` by
//! `whole_seconds`, then wire `UTCOSECS` over the wrapper.
//!
//! # Example
//!
//! ```rust
//! use connections::time::ODTMNANO;
//! use connections::extended::Extended;
//! use time::OffsetDateTime;
//!
//! assert_eq!(ODTMNANO.upper(0), Extended::Finite(OffsetDateTime::UNIX_EPOCH));
//! assert_eq!(
//!     ODTMNANO.ceil(Extended::Finite(OffsetDateTime::UNIX_EPOCH)),
//!     0,
//! );
//! ```

use crate::extended::Extended;
use time::{Date, OffsetDateTime, Time, UtcOffset};

// ── Range bounds, computed once at runtime ─────────────────────
//
// `OffsetDateTime`'s instant range is **wider** than its
// (Date::MIN, Date::MAX) UTC bounds. An OffsetDateTime is a
// (PrimitiveDateTime, UtcOffset) pair where the local PDT is in
// `[Date::MIN, Date::MAX]` × Time, but the instant = local_pdt −
// offset. The most-negative offset (-25:59:59) on Date::MAX local
// extends the instant past Date::MAX UTC; symmetric for the
// +25:59:59 offset on Date::MIN. So the actual instant range is
// approximately `[Date::MIN+25:59:59 UTC, Date::MAX-25:59:59 UTC]`.
//
// `time::OffsetDateTime`'s `Ord` re-bases the right-hand side to
// the LHS's offset before comparing — this is instant-based and
// exposes the wider range to law tests.

#[inline]
fn end_of_day_time() -> Time {
    Time::from_hms_nano(23, 59, 59, 999_999_999).expect("23:59:59.999_999_999 is a valid Time")
}

/// Minimum-instant OffsetDateTime: Date::MIN at midnight, with the
/// most-positive offset (`+25:59:59`) — local-pdt is the smallest,
/// offset is the largest, so `local − offset` is the smallest instant.
#[cfg(test)]
#[inline]
fn odtm_min_instant() -> OffsetDateTime {
    OffsetDateTime::new_in_offset(
        Date::MIN,
        Time::MIDNIGHT,
        UtcOffset::from_whole_seconds(93_599).expect("+25:59:59 is a valid offset"),
    )
}

/// Maximum-instant OffsetDateTime: Date::MAX at end-of-day, with
/// the most-negative offset (`-25:59:59`).
#[cfg(test)]
#[inline]
fn odtm_max_instant() -> OffsetDateTime {
    OffsetDateTime::new_in_offset(
        Date::MAX,
        end_of_day_time(),
        UtcOffset::from_whole_seconds(-93_599).expect("-25:59:59 is a valid offset"),
    )
}

/// Smallest unix-nanosecond rung value that
/// `OffsetDateTime::from_unix_timestamp_nanos` accepts (Date::MIN
/// midnight UTC). The instant range of `OffsetDateTime` is wider —
/// see `odtm_min_instant` — but the rung saturates here because
/// the inverse direction (rung → ODTM) only constructs UTC ODTMs.
#[inline]
fn utc_min_ns() -> i128 {
    OffsetDateTime::new_in_offset(Date::MIN, Time::MIDNIGHT, UtcOffset::UTC).unix_timestamp_nanos()
}

#[inline]
fn utc_max_ns() -> i128 {
    OffsetDateTime::new_in_offset(Date::MAX, end_of_day_time(), UtcOffset::UTC)
        .unix_timestamp_nanos()
}

#[inline]
fn utc_min_s() -> i64 {
    OffsetDateTime::new_in_offset(Date::MIN, Time::MIDNIGHT, UtcOffset::UTC).unix_timestamp()
}

#[inline]
fn utc_max_s() -> i64 {
    OffsetDateTime::new_in_offset(Date::MAX, end_of_day_time(), UtcOffset::UTC).unix_timestamp()
}

// ── ODTMNANO ─────────────────────────────────────────────────────

fn odtmnano_ceil(o: Extended<OffsetDateTime>) -> i128 {
    let min_ns = utc_min_ns();
    let max_ns = utc_max_ns();
    match o {
        Extended::NegInf => i128::MIN,
        Extended::Finite(odt) => {
            let n = odt.unix_timestamp_nanos();
            // ODTM's instant range exceeds the UTC range its rung
            // covers; saturate the rung to ±(UTC_bounds ± 1) when
            // the instant escapes via a non-UTC offset.
            if n < min_ns {
                min_ns
            } else if n > max_ns {
                max_ns + 1
            } else {
                n
            }
        }
        Extended::PosInf => max_ns + 1,
    }
}

fn odtmnano_inner(n: i128) -> Extended<OffsetDateTime> {
    let min_ns = utc_min_ns();
    let max_ns = utc_max_ns();
    if n < min_ns {
        Extended::NegInf
    } else if n > max_ns {
        Extended::PosInf
    } else {
        match OffsetDateTime::from_unix_timestamp_nanos(n) {
            Ok(odt) => Extended::Finite(odt),
            Err(_) => unreachable!("n in [utc_min_ns, utc_max_ns]"),
        }
    }
}

crate::conn_l! {
    /// `Extended<OffsetDateTime> → i128` — unix nanoseconds since
    /// `OffsetDateTime::UNIX_EPOCH`.
    ///
    /// One-sided left-Galois Conn (`Conn::new_l(ceil, inner)`):
    /// `inner: i128 → Extended<OffsetDateTime>` saturates out-of-range
    /// `i128` values onto `Extended::NegInf` / `Extended::PosInf`, which
    /// makes `inner` non-order-reflecting at the synthetic extremes. The
    /// constructor wires `floor = ceil` as a fn pointer so `floor(a) ≤
    /// ceil(a)` holds structurally over the full source domain.
    ///
    /// Lossless across the full default-features OffsetDateTime range
    /// (year ±9999 ≈ ±3.16 × 10²⁰ ns, comfortably inside `i128`'s
    /// ±1.7 × 10³⁸ envelope). On the `Finite` portion this is an exact
    /// bijection on the instant.
    ///
    /// Two OffsetDateTimes representing the same instant in different
    /// offsets compare equal under `time` 0.3's `PartialEq` and so map
    /// to the same `i128`. `inner` returns the UTC representative.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use connections::time::ODTMNANO;
    /// use connections::extended::Extended;
    /// use time::OffsetDateTime;
    ///
    /// assert_eq!(ODTMNANO.ceil(Extended::Finite(OffsetDateTime::UNIX_EPOCH)), 0);
    /// assert_eq!(ODTMNANO.upper(1_000_000_000), Extended::Finite(
    ///     OffsetDateTime::UNIX_EPOCH + time::Duration::seconds(1),
    /// ));
    ///
    /// // Out-of-range nanoseconds saturate.
    /// assert_eq!(ODTMNANO.upper(i128::MAX), Extended::PosInf);
    /// ```
    pub ODTMNANO : Extended<OffsetDateTime> => i128 {
        ceil:  odtmnano_ceil,
        inner: odtmnano_inner,
    }
}

// ── ODTMSECS ─────────────────────────────────────────────────────

fn odtmsecs_ceil(o: Extended<OffsetDateTime>) -> i64 {
    let min_s = utc_min_s();
    let max_s = utc_max_s();
    match o {
        Extended::NegInf => i64::MIN,
        Extended::Finite(odt) => {
            let s = odt.unix_timestamp();
            if s < min_s {
                // Instant below UTC range — smallest rung Finite
                // value is min_s.
                min_s
            } else if s >= max_s {
                // Instant at or beyond UTC max: ceil bumps past
                // max_s into the saturation marker.
                if s > max_s || odt.nanosecond() != 0 {
                    max_s + 1
                } else {
                    max_s
                }
            } else if odt.nanosecond() != 0 {
                // Sub-second past s — ceil rounds up by one.
                s + 1
            } else {
                s
            }
        }
        Extended::PosInf => max_s + 1,
    }
}

fn odtmsecs_inner(s: i64) -> Extended<OffsetDateTime> {
    let min_s = utc_min_s();
    let max_s = utc_max_s();
    if s < min_s {
        Extended::NegInf
    } else if s > max_s {
        Extended::PosInf
    } else {
        match OffsetDateTime::from_unix_timestamp(s) {
            Ok(odt) => Extended::Finite(odt),
            Err(_) => unreachable!("s in [utc_min_s, utc_max_s]"),
        }
    }
}

crate::conn_l! {
    /// `Extended<OffsetDateTime> → i64` — unix whole seconds since
    /// `OffsetDateTime::UNIX_EPOCH`, rounding sub-second fractions up to
    /// the next whole second.
    ///
    /// One-sided left-Galois Conn (`Conn::new_l(ceil, inner)`). Two
    /// reasons it can't ship as a full triple:
    ///
    /// - `inner` saturates out-of-range `i64` values to
    ///   `Extended::NegInf` / `Extended::PosInf`, making it non-order-
    ///   reflecting at the synthetic source extremes.
    /// - On Finite sub-second inputs, the natural `ceil` (round up) and
    ///   `floor` (truncate) differ; the constructor wires `floor = ceil`
    ///   as a fn pointer so the law holds structurally.
    ///
    /// **Behavioral note on rounding:** `ceil(Finite(odt))` =
    /// `odt.unix_timestamp() + 1` when `odt.nanosecond() != 0`, otherwise
    /// `odt.unix_timestamp()`. Because `floor = ceil` under `new_left`,
    /// callers reading `ODTMSECS.ceil(odt)` get the **rounded-up**
    /// answer, not the truncated one (`unix_timestamp()` is itself
    /// `floor`-shaped in the `time` crate). Callers needing the
    /// truncating direction can compute `odt.unix_timestamp()` directly.
    ///
    /// `i64` is wide enough for the full OffsetDateTime range (`±3.16 ×
    /// 10¹¹` s, comfortably inside `i64`'s `±9.22 × 10¹⁸`), so the only
    /// saturation happens on the outer `Extended` source arms.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use connections::time::ODTMSECS;
    /// use connections::extended::Extended;
    /// use time::{Duration, OffsetDateTime};
    ///
    /// // Exact second.
    /// assert_eq!(ODTMSECS.ceil(Extended::Finite(OffsetDateTime::UNIX_EPOCH)), 0);
    ///
    /// // Sub-second past epoch: ceil rounds up.
    /// let mid = OffsetDateTime::UNIX_EPOCH + Duration::nanoseconds(1);
    /// assert_eq!(ODTMSECS.ceil(Extended::Finite(mid)), 1);
    /// ```
    pub ODTMSECS : Extended<OffsetDateTime> => i64 {
        ceil:  odtmsecs_ceil,
        inner: odtmsecs_inner,
    }
}

#[cfg(test)]
mod odtm_tests {
    use super::*;
    #[allow(unused_imports)]
    use crate::conn::{ConnL, ConnR};
    use crate::prop::arb::{
        arb_extended_offset_dt, arb_offset_dt, arb_unix_nanos_in_range, arb_unix_secs_in_range,
    };
    use crate::prop::{conn as conn_laws, lattice as lattice_laws};
    use proptest::prelude::*;
    use time::Duration;

    // ── Preorder laws on `OffsetDateTime` ───────────────────────

    mod offset_dt_preorder {
        use super::*;

        proptest! {
            #[test]
            fn reflexive(x in arb_offset_dt()) {
                prop_assert!(lattice_laws::lattice_reflexive(&x));
            }

            #[test]
            fn transitive(x in arb_offset_dt(), y in arb_offset_dt(), z in arb_offset_dt()) {
                prop_assert!(lattice_laws::lattice_transitive(&x, &y, &z));
            }

            #[test]
            fn antisymmetric(x in arb_offset_dt(), y in arb_offset_dt()) {
                prop_assert!(lattice_laws::lattice_antisymmetric(&x, &y));
            }

            #[test]
            fn bot(x in arb_offset_dt()) {
                prop_assert!(lattice_laws::lattice_bot(&odtm_min_instant(), &x));
            }

            #[test]
            fn top(x in arb_offset_dt()) {
                prop_assert!(lattice_laws::lattice_top(&odtm_max_instant(), &x));
            }
        }
    }

    // ── ODTMNANO spot checks ────────────────────────────────────

    #[test]
    fn epoch_is_zero() {
        assert_eq!(
            ODTMNANO.ceil(Extended::Finite(OffsetDateTime::UNIX_EPOCH)),
            0
        );
        assert_eq!(
            ODTMNANO.upper(0),
            Extended::Finite(OffsetDateTime::UNIX_EPOCH)
        );
    }

    #[test]
    fn one_ns_past_epoch() {
        let mid = OffsetDateTime::UNIX_EPOCH + Duration::nanoseconds(1);
        assert_eq!(ODTMNANO.ceil(Extended::Finite(mid)), 1);
        assert_eq!(ODTMNANO.upper(1), Extended::Finite(mid));
    }

    #[test]
    fn one_ns_before_epoch() {
        let mid = OffsetDateTime::UNIX_EPOCH - Duration::nanoseconds(1);
        assert_eq!(ODTMNANO.ceil(Extended::Finite(mid)), -1);
        assert_eq!(ODTMNANO.upper(-1), Extended::Finite(mid));
    }

    #[test]
    fn y2038() {
        let y2038_ns: i128 = 2_147_483_648_000_000_000;
        let y2038 = OffsetDateTime::from_unix_timestamp_nanos(y2038_ns).unwrap();
        assert_eq!(ODTMNANO.ceil(Extended::Finite(y2038)), y2038_ns);
        assert_eq!(ODTMNANO.upper(y2038_ns), Extended::Finite(y2038));
    }

    #[test]
    fn utc_extremes_round_trip() {
        // UTC extremes are the round-trippable boundary — wider
        // OffsetDateTime instants (e.g. Date::MAX with negative
        // offset) saturate the rung.
        let min_utc = OffsetDateTime::new_in_offset(Date::MIN, Time::MIDNIGHT, UtcOffset::UTC);
        let max_utc = OffsetDateTime::new_in_offset(
            Date::MAX,
            Time::from_hms_nano(23, 59, 59, 999_999_999).unwrap(),
            UtcOffset::UTC,
        );
        let min_ns = utc_min_ns();
        let max_ns = utc_max_ns();

        assert_eq!(ODTMNANO.ceil(Extended::Finite(min_utc)), min_ns);
        assert_eq!(ODTMNANO.ceil(Extended::Finite(max_utc)), max_ns);
        assert_eq!(ODTMNANO.upper(min_ns), Extended::Finite(min_utc));
        assert_eq!(ODTMNANO.upper(max_ns), Extended::Finite(max_utc));
    }

    #[test]
    fn instant_extreme_saturates() {
        // odtm_max_instant() has instant past utc_max_ns; ceil bumps
        // it to max_ns + 1 (the PosInf marker on the rung).
        let max_inst = odtm_max_instant();
        assert_eq!(ODTMNANO.ceil(Extended::Finite(max_inst)), utc_max_ns() + 1);

        let min_inst = odtm_min_instant();
        assert_eq!(ODTMNANO.ceil(Extended::Finite(min_inst)), utc_min_ns());
    }

    #[test]
    fn odtmnano_saturation_extremes() {
        assert_eq!(ODTMNANO.upper(i128::MAX), Extended::PosInf);
        assert_eq!(ODTMNANO.upper(i128::MIN), Extended::NegInf);

        let max_ns = utc_max_ns();
        assert_eq!(ODTMNANO.ceil(Extended::PosInf), max_ns + 1);
        assert_eq!(ODTMNANO.ceil(Extended::NegInf), i128::MIN);
        // `new_left` wires `floor = ceil` structurally.
        assert_eq!(ODTMNANO.ceil(Extended::PosInf), max_ns + 1);
        assert_eq!(ODTMNANO.ceil(Extended::NegInf), i128::MIN);
    }

    // ── ODTMSECS spot checks ────────────────────────────────────

    #[test]
    fn odtmsecs_epoch() {
        assert_eq!(
            ODTMSECS.ceil(Extended::Finite(OffsetDateTime::UNIX_EPOCH)),
            0
        );
        assert_eq!(
            ODTMSECS.upper(0),
            Extended::Finite(OffsetDateTime::UNIX_EPOCH)
        );
    }

    #[test]
    fn odtmsecs_subsec_rounds_up() {
        let mid = OffsetDateTime::UNIX_EPOCH + Duration::nanoseconds(1);
        assert_eq!(ODTMSECS.ceil(Extended::Finite(mid)), 1);
    }

    #[test]
    fn odtmsecs_negative_subsec() {
        // 1969-12-31 23:59:59.5 UTC: instant = -0.5 s. ceil rounds
        // up to 0.
        let half_before = OffsetDateTime::UNIX_EPOCH - Duration::milliseconds(500);
        assert_eq!(ODTMSECS.ceil(Extended::Finite(half_before)), 0);
    }

    #[test]
    fn odtmsecs_saturation_extremes() {
        let max_s = utc_max_s();
        assert_eq!(ODTMSECS.ceil(Extended::PosInf), max_s + 1);
        assert_eq!(ODTMSECS.ceil(Extended::NegInf), i64::MIN);
        // `new_left` wires `floor = ceil` structurally.
        assert_eq!(ODTMSECS.ceil(Extended::PosInf), max_s + 1);
        assert_eq!(ODTMSECS.ceil(Extended::NegInf), i64::MIN);
    }

    // ── Galois law battery — ODTMNANO (one-sided L) ────────────

    proptest! {
        #[test]
        fn odtmnano_galois_l(o in arb_extended_offset_dt(), b in any::<i128>()) {
            prop_assert!(conn_laws::galois_l(&ODTMNANO, o, b));
        }

        #[test]
        fn odtmnano_closure_l(o in arb_extended_offset_dt()) {
            prop_assert!(conn_laws::closure_l(&ODTMNANO, o));
        }

        #[test]
        fn odtmnano_kernel_l(b in any::<i128>()) {
            prop_assert!(conn_laws::kernel_l(&ODTMNANO, b));
        }

        #[test]
        fn odtmnano_monotone_l(a in arb_extended_offset_dt(), b in arb_extended_offset_dt()) {
            prop_assert!(conn_laws::monotone_l(&ODTMNANO, a, b));
        }

        #[test]
        fn odtmnano_idempotent(o in arb_extended_offset_dt()) {
            prop_assert!(conn_laws::idempotent(&ODTMNANO, o));
        }

        #[test]
        fn odtmnano_roundtrip_ceil(b in arb_unix_nanos_in_range()) {
            prop_assert!(conn_laws::roundtrip_ceil(&ODTMNANO, b));
        }
    }

    // ── Galois law battery — ODTMSECS (one-sided L) ────────────

    proptest! {
        #[test]
        fn odtmsecs_galois_l(o in arb_extended_offset_dt(), b in any::<i64>()) {
            prop_assert!(conn_laws::galois_l(&ODTMSECS, o, b));
        }

        #[test]
        fn odtmsecs_closure_l(o in arb_extended_offset_dt()) {
            prop_assert!(conn_laws::closure_l(&ODTMSECS, o));
        }

        #[test]
        fn odtmsecs_kernel_l(b in any::<i64>()) {
            prop_assert!(conn_laws::kernel_l(&ODTMSECS, b));
        }

        #[test]
        fn odtmsecs_monotone_l(a in arb_extended_offset_dt(), b in arb_extended_offset_dt()) {
            prop_assert!(conn_laws::monotone_l(&ODTMSECS, a, b));
        }

        #[test]
        fn odtmsecs_idempotent(o in arb_extended_offset_dt()) {
            prop_assert!(conn_laws::idempotent(&ODTMSECS, o));
        }

        #[test]
        fn odtmsecs_roundtrip_ceil(b in arb_unix_secs_in_range()) {
            prop_assert!(conn_laws::roundtrip_ceil(&ODTMSECS, b));
        }
    }
}