Skip to main content

uni_btic/
encode.rs

1use crate::btic::{Btic, SIGN_FLIP};
2use crate::error::BticError;
3
4/// Encode a BTIC value into its 24-byte packed canonical form.
5///
6/// The packed format uses sign-bit-flipped big-endian encoding so that
7/// `memcmp` on the raw bytes produces the same order as `Btic::cmp`.
8pub fn encode(btic: &Btic) -> [u8; 24] {
9    let mut buf = [0u8; 24];
10    let lo_encoded = (btic.lo() as u64) ^ SIGN_FLIP;
11    let hi_encoded = (btic.hi() as u64) ^ SIGN_FLIP;
12    buf[0..8].copy_from_slice(&lo_encoded.to_be_bytes());
13    buf[8..16].copy_from_slice(&hi_encoded.to_be_bytes());
14    buf[16..24].copy_from_slice(&btic.meta().to_be_bytes());
15    buf
16}
17
18/// Decode a 24-byte packed canonical form into a BTIC value.
19///
20/// Validates all invariants after decoding.
21pub fn decode(bytes: &[u8; 24]) -> Result<Btic, BticError> {
22    // Reading a fixed [u8; 8] window out of a [u8; 24] is infallible — the
23    // compiler can prove the lengths line up — so `expect` here is unreachable.
24    fn word(slice: &[u8]) -> u64 {
25        let arr: [u8; 8] = slice
26            .try_into()
27            .expect("infallible: 8-byte slice from 24-byte array");
28        u64::from_be_bytes(arr)
29    }
30
31    let lo_encoded = word(&bytes[0..8]);
32    let hi_encoded = word(&bytes[8..16]);
33    let meta = word(&bytes[16..24]);
34
35    let lo = (lo_encoded ^ SIGN_FLIP) as i64;
36    let hi = (hi_encoded ^ SIGN_FLIP) as i64;
37
38    Btic::new(lo, hi, meta)
39}
40
41/// Decode from a byte slice, checking that the length is exactly 24.
42pub fn decode_slice(bytes: &[u8]) -> Result<Btic, BticError> {
43    if bytes.len() != 24 {
44        return Err(BticError::InvalidLength(bytes.len()));
45    }
46    let arr: &[u8; 24] = bytes
47        .try_into()
48        .expect("infallible: length validated above");
49    decode(arr)
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use crate::btic::NEG_INF;
56    use crate::certainty::Certainty;
57    use crate::granularity::Granularity;
58
59    #[test]
60    fn roundtrip_basic() {
61        let meta = Btic::build_meta(
62            Granularity::Year,
63            Granularity::Year,
64            Certainty::Definite,
65            Certainty::Definite,
66        );
67        let original = Btic::new(473_385_600_000, 504_921_600_000, meta).unwrap();
68        let packed = encode(&original);
69        let decoded = decode(&packed).unwrap();
70        assert_eq!(original, decoded);
71    }
72
73    #[test]
74    fn roundtrip_negative_lo() {
75        let meta = Btic::build_meta(
76            Granularity::Year,
77            Granularity::Year,
78            Certainty::Approximate,
79            Certainty::Approximate,
80        );
81        // Negative timestamp (before epoch)
82        let original = Btic::new(-77_914_137_600_000, -77_882_601_600_000, meta).unwrap();
83        let packed = encode(&original);
84        let decoded = decode(&packed).unwrap();
85        assert_eq!(original, decoded);
86    }
87
88    #[test]
89    fn roundtrip_sentinel_lo() {
90        let meta = Btic::build_meta(
91            Granularity::Millisecond,
92            Granularity::Month,
93            Certainty::Definite,
94            Certainty::Definite,
95        );
96        let original = Btic::new(NEG_INF, 481_161_600_000, meta).unwrap();
97        let packed = encode(&original);
98        let decoded = decode(&packed).unwrap();
99        assert_eq!(original, decoded);
100    }
101
102    #[test]
103    fn invalid_length_rejected() {
104        assert!(decode_slice(&[0u8; 23]).is_err());
105        assert!(decode_slice(&[0u8; 25]).is_err());
106    }
107
108    #[test]
109    fn memcmp_matches_ord() {
110        let meta = Btic::build_meta(
111            Granularity::Day,
112            Granularity::Day,
113            Certainty::Definite,
114            Certainty::Definite,
115        );
116        let a = Btic::new(100, 200, meta).unwrap();
117        let b = Btic::new(150, 200, meta).unwrap();
118        let packed_a = encode(&a);
119        let packed_b = encode(&b);
120
121        // memcmp ordering should match Ord
122        assert!(packed_a < packed_b);
123        assert!(a < b);
124    }
125}