use crate::coding::{checked_count, checked_decode_count};
use crate::error::{EncodingError, Result};
#[derive(Debug, Clone, Copy)]
pub struct PatasEncoder;
#[derive(Debug, Clone, Copy)]
pub struct PatasDecoder;
impl PatasEncoder {
pub fn encode(values: &[f64]) -> Result<Vec<u8>> {
if values.is_empty() {
return Err(EncodingError::EmptyInput {
context: "patas encoder",
});
}
let count = checked_count(values.len())?;
let mut buf = Vec::with_capacity(12 + values.len() * 9);
buf.extend_from_slice(&count.to_le_bytes());
let mut prev_bits = values[0].to_bits();
buf.extend_from_slice(&prev_bits.to_le_bytes());
for &val in &values[1..] {
let bits = val.to_bits();
let xor = prev_bits ^ bits;
if xor == 0 {
buf.push(0);
} else {
let tz_bytes = (xor.trailing_zeros() / 8) as u8; let total_bytes = 8 - (xor.leading_zeros() / 8) as u8; let sig_bytes = total_bytes.saturating_sub(tz_bytes);
let flag = (tz_bytes << 4) | sig_bytes;
buf.push(flag);
let shifted = xor >> (u32::from(tz_bytes) * 8);
let sig_slice = shifted.to_le_bytes();
buf.extend_from_slice(&sig_slice[..sig_bytes as usize]);
}
prev_bits = bits;
}
Ok(buf)
}
}
impl PatasDecoder {
pub fn decode(payload: &[u8]) -> Result<Vec<f64>> {
if payload.len() < 12 {
return Err(EncodingError::CorruptData {
detail: "patas payload too short for header".to_string(),
});
}
let count = checked_decode_count(
u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]) as usize,
"Patas",
)?;
if count == 0 {
return Err(EncodingError::CorruptData {
detail: "patas count is zero".to_string(),
});
}
let mut values = Vec::with_capacity(count);
let mut pos = 4;
if pos + 8 > payload.len() {
return Err(EncodingError::CorruptData {
detail: "patas payload truncated at first value".to_string(),
});
}
let mut prev_bits = u64::from_le_bytes(payload[pos..pos + 8].try_into().expect("8 bytes"));
values.push(f64::from_bits(prev_bits));
pos += 8;
for _ in 1..count {
if pos >= payload.len() {
return Err(EncodingError::CorruptData {
detail: "patas payload truncated".to_string(),
});
}
let flag = payload[pos];
pos += 1;
let tz_bytes = (flag >> 4) & 0x0F;
let sig_bytes = flag & 0x0F;
if tz_bytes + sig_bytes > 8 {
return Err(EncodingError::CorruptData {
detail: format!(
"patas flag byte claims {tz_bytes} trailing-zero and {sig_bytes} significant bytes, which do not fit in 8 — header is corrupt"
),
});
}
if sig_bytes == 0 {
values.push(f64::from_bits(prev_bits));
continue;
}
if pos + sig_bytes as usize > payload.len() {
return Err(EncodingError::CorruptData {
detail: "patas payload truncated at significant bytes".to_string(),
});
}
let mut sig_val = [0u8; 8];
sig_val[..sig_bytes as usize].copy_from_slice(&payload[pos..pos + sig_bytes as usize]);
pos += sig_bytes as usize;
let shifted = u64::from_le_bytes(sig_val);
let xor = shifted << (u32::from(tz_bytes) * 8);
prev_bits ^= xor;
values.push(f64::from_bits(prev_bits));
}
if values.len() != count {
return Err(EncodingError::CorruptData {
detail: format!("patas decoded {} values, expected {}", values.len(), count),
});
}
Ok(values)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_basic() {
let values = vec![1.0, 1.1, 1.2, 1.3, 1.4, 1.5];
let encoded = PatasEncoder::encode(&values).unwrap();
let decoded = PatasDecoder::decode(&encoded).unwrap();
assert_eq!(values, decoded);
}
#[test]
fn roundtrip_identical() {
let values = vec![42.0; 100];
let encoded = PatasEncoder::encode(&values).unwrap();
let decoded = PatasDecoder::decode(&encoded).unwrap();
assert_eq!(values, decoded);
assert_eq!(encoded.len(), 111);
}
#[test]
fn roundtrip_single() {
let values = vec![std::f64::consts::PI];
let encoded = PatasEncoder::encode(&values).unwrap();
let decoded = PatasDecoder::decode(&encoded).unwrap();
assert_eq!(values, decoded);
}
#[test]
fn roundtrip_special_values() {
let values = vec![
0.0,
f64::INFINITY,
f64::NEG_INFINITY,
-0.0,
f64::MAX,
f64::MIN,
];
let encoded = PatasEncoder::encode(&values).unwrap();
let decoded = PatasDecoder::decode(&encoded).unwrap();
for (a, b) in values.iter().zip(decoded.iter()) {
assert_eq!(a.to_bits(), b.to_bits());
}
}
#[test]
fn roundtrip_nan() {
let values = vec![1.0, f64::NAN, 2.0];
let encoded = PatasEncoder::encode(&values).unwrap();
let decoded = PatasDecoder::decode(&encoded).unwrap();
assert_eq!(values[0], decoded[0]);
assert!(decoded[1].is_nan());
assert_eq!(values[2], decoded[2]);
}
#[test]
fn empty_input_error() {
let result = PatasEncoder::encode(&[]);
assert!(result.is_err());
}
#[test]
fn truncated_payload_error() {
let result = PatasDecoder::decode(&[0; 4]);
assert!(result.is_err());
}
#[test]
fn compression_ratio_slowly_varying() {
let values: Vec<f64> = (0..1000).map(|i| 20.0 + (i as f64) * 0.001).collect();
let encoded = PatasEncoder::encode(&values).unwrap();
let raw_size = values.len() * 8;
assert!(
encoded.len() < raw_size,
"Expected compression, got {}/{}",
encoded.len(),
raw_size
);
let decoded = PatasDecoder::decode(&encoded).unwrap();
assert_eq!(values, decoded);
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn roundtrip_arbitrary(values in proptest::collection::vec(any::<f64>(), 1..500)) {
let encoded = PatasEncoder::encode(&values).unwrap();
let decoded = PatasDecoder::decode(&encoded).unwrap();
prop_assert_eq!(values.len(), decoded.len());
for (a, b) in values.iter().zip(decoded.iter()) {
prop_assert_eq!(a.to_bits(), b.to_bits(), "bitwise mismatch");
}
}
}
}
#[cfg(test)]
mod flag_validation_tests {
use super::*;
fn payload_with_flag(flag: u8, tail: &[u8]) -> Vec<u8> {
let mut p = Vec::new();
p.extend_from_slice(&2u32.to_le_bytes());
p.extend_from_slice(&1.0f64.to_bits().to_le_bytes());
p.push(flag);
p.extend_from_slice(tail);
p
}
#[test]
fn a_trailing_zero_count_past_the_width_of_a_u64_is_rejected() {
for tz in 8u8..=15 {
let flag = (tz << 4) | 1; let err = PatasDecoder::decode(&payload_with_flag(flag, &[0xFF]))
.expect_err("tz_bytes {tz} must be refused");
assert!(
err.to_string().contains("do not fit in 8"),
"tz={tz}: expected the flag guard, got: {err}"
);
}
}
#[test]
fn a_significant_byte_count_past_eight_is_rejected() {
for sig in 9u8..=15 {
let err = PatasDecoder::decode(&payload_with_flag(sig, &[0xFF; 16]))
.expect_err("sig_bytes {sig} must be refused");
assert!(
err.to_string().contains("do not fit in 8"),
"sig={sig}: expected the flag guard, got: {err}"
);
}
}
#[test]
fn every_flag_the_encoder_can_produce_still_decodes() {
let values = vec![
1.0,
1.0,
1.5,
1.5000001,
2.0,
-2.0,
0.0,
-0.0,
f64::MAX,
f64::MIN_POSITIVE,
1e300,
1e-300,
];
let encoded = PatasEncoder::encode(&values).expect("encode");
let decoded = PatasDecoder::decode(&encoded).expect("decode");
assert_eq!(decoded.len(), values.len());
for (a, b) in values.iter().zip(&decoded) {
assert_eq!(a.to_bits(), b.to_bits(), "round trip must be bitwise exact");
}
}
}