Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 38 additions & 27 deletions packages/orderable-bytes/src/chrono.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
//! Canonical, order-preserving fixed-length byte encodings for the
//! `chrono` types `NaiveDate` and `DateTime<Utc>`.
//!
//! Each submodule exposes a `to_orderable_bytes` function and an
//! `ENCODED_LEN` constant. The bytes returned have the property that
//! byte-wise lex comparison agrees with chronological ordering (and byte
//! equality with value equality), so any comparison-as-bytes scheme
//! (`ore-rs` BlockORE, OPE, an ordered hash) inherits those properties on
//! the resulting digest.
//! Each submodule exposes an `ENCODED_LEN` constant and an
//! [`crate::ToOrderableBytes`] impl on its target type. The bytes
//! returned have the property that byte-wise lex comparison agrees with
//! chronological ordering (and byte equality with value equality), so
//! any comparison-as-bytes scheme (`ore-rs` BlockORE, OPE, an ordered
//! hash) inherits those properties on the resulting digest.

/// Order-preserving byte encoding for [`::chrono::NaiveDate`].
pub mod naive_date {
use crate::ToOrderableBytes;
use ::chrono::{Datelike, NaiveDate};

/// Number of bytes in the canonical orderable-bytes form.
pub const ENCODED_LEN: usize = 4;

/// Build the canonical, order-preserving byte encoding of a `NaiveDate`.
///
/// `NaiveDate::num_days_from_ce()` returns an `i32` whose ordering
/// matches chronological order. Sign-flipping `i32 → u32` (XOR with
/// `1u32 << 31`) preserves order while making the value unsigned, then
/// big-endian byte serialisation gives a 4-byte sequence whose lex
/// order matches the natural date order.
pub fn to_orderable_bytes(d: &NaiveDate) -> [u8; ENCODED_LEN] {
let biased = (d.num_days_from_ce() as u32) ^ (1u32 << 31);
biased.to_be_bytes()
impl ToOrderableBytes for NaiveDate {
const ENCODED_LEN: usize = 4;
type Bytes = [u8; Self::ENCODED_LEN];

fn to_orderable_bytes(&self) -> [u8; Self::ENCODED_LEN] {
let biased = (self.num_days_from_ce() as u32) ^ (1u32 << 31);
biased.to_be_bytes()
}
}

#[cfg(test)]
Expand All @@ -38,7 +41,7 @@ pub mod naive_date {
#[test]
fn year_one_biases_to_known_u32() {
// Year 1 day 1 has num_days_from_ce = 1 ⇒ sign-flipped u32 = 0x8000_0001.
assert_eq!(to_orderable_bytes(&ymd(1, 1, 1)), [0x80, 0x00, 0x00, 0x01]);
assert_eq!(ymd(1, 1, 1).to_orderable_bytes(), [0x80, 0x00, 0x00, 0x01]);
}

#[test]
Expand All @@ -55,8 +58,8 @@ pub mod naive_date {
NaiveDate::MAX,
];
for window in ascending.windows(2) {
let a = to_orderable_bytes(&window[0]);
let b = to_orderable_bytes(&window[1]);
let a = window[0].to_orderable_bytes();
let b = window[1].to_orderable_bytes();
assert!(
a < b,
"to_orderable_bytes({}) < to_orderable_bytes({}) failed",
Expand All @@ -70,9 +73,12 @@ pub mod naive_date {

/// Order-preserving byte encoding for [`::chrono::DateTime<::chrono::Utc>`].
pub mod datetime_utc {
use crate::ToOrderableBytes;
use ::chrono::{DateTime, Utc};

/// Number of bytes in the canonical orderable-bytes form.
/// Number of bytes in the canonical orderable-bytes form. Mirrors
/// `<DateTime<Utc> as ToOrderableBytes>::ENCODED_LEN` for use in
/// const contexts that can't easily name the trait impl.
pub const ENCODED_LEN: usize = 12;

/// Build the canonical, order-preserving byte encoding of a
Expand All @@ -86,14 +92,19 @@ pub mod datetime_utc {
/// whole second. `timestamp_subsec_nanos` returns values in
/// `0..2_000_000_000` (the upper half encodes leap-second moments),
/// which fits in `u32` and preserves chronological order.
pub fn to_orderable_bytes(dt: &DateTime<Utc>) -> [u8; ENCODED_LEN] {
let secs = dt.timestamp();
let nanos = dt.timestamp_subsec_nanos();
let secs_biased = (secs as u64) ^ (1u64 << 63);
let mut out = [0u8; ENCODED_LEN];
out[..8].copy_from_slice(&secs_biased.to_be_bytes());
out[8..].copy_from_slice(&nanos.to_be_bytes());
out
impl ToOrderableBytes for DateTime<Utc> {
const ENCODED_LEN: usize = ENCODED_LEN;
type Bytes = [u8; ENCODED_LEN];

fn to_orderable_bytes(&self) -> [u8; ENCODED_LEN] {
let secs = self.timestamp();
let nanos = self.timestamp_subsec_nanos();
let secs_biased = (secs as u64) ^ (1u64 << 63);
let mut out = [0u8; ENCODED_LEN];
out[..8].copy_from_slice(&secs_biased.to_be_bytes());
out[8..].copy_from_slice(&nanos.to_be_bytes());
out
}
}

#[cfg(test)]
Expand All @@ -110,7 +121,7 @@ pub mod datetime_utc {
// 1970-01-01T00:00:00Z: timestamp = 0, subsec = 0. Sign-flip on
// `0_i64` gives `0x8000_0000_0000_0000`.
assert_eq!(
to_orderable_bytes(&dt(0, 0)),
dt(0, 0).to_orderable_bytes(),
[0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
);
}
Expand All @@ -132,8 +143,8 @@ pub mod datetime_utc {
DateTime::<Utc>::MAX_UTC,
];
for window in ascending.windows(2) {
let a = to_orderable_bytes(&window[0]);
let b = to_orderable_bytes(&window[1]);
let a = window[0].to_orderable_bytes();
let b = window[1].to_orderable_bytes();
assert!(
a < b,
"to_orderable_bytes({}) < to_orderable_bytes({}) failed",
Expand Down
Loading
Loading