use pco::data_types::Number;
use pco::standalone::{simple_compress, simple_decompress_into};
use pco::ChunkConfig;
use crate::coding::MAX_BLOCK_VALUES;
use crate::error::{EncodingError, Result};
const HEADER: usize = 4;
pub struct PcoEncoder;
pub struct PcoDecoder;
impl PcoEncoder {
pub fn encode_f64(values: &[f64]) -> Result<Vec<u8>> {
Self::encode(values)
}
pub fn encode_i64(values: &[i64]) -> Result<Vec<u8>> {
Self::encode(values)
}
pub fn encode_u64(values: &[u64]) -> Result<Vec<u8>> {
Self::encode(values)
}
pub fn encode_timestamps(values: &[i64]) -> Result<Vec<u8>> {
Self::encode_with(values, &ChunkConfig::default())
}
fn encode<T: Number>(values: &[T]) -> Result<Vec<u8>> {
Self::encode_with(values, &ChunkConfig::default())
}
fn encode_with<T: Number>(values: &[T], config: &ChunkConfig) -> Result<Vec<u8>> {
if values.is_empty() {
return Err(EncodingError::EmptyInput {
context: "pco encode",
});
}
let n = u32::try_from(values.len()).map_err(|_| EncodingError::CorruptData {
detail: format!("pco: {} values exceed a block", values.len()),
})?;
let body = simple_compress(values, config).map_err(|e| EncodingError::CorruptData {
detail: format!("pco encode: {e}"),
})?;
let mut out = Vec::with_capacity(HEADER + body.len());
out.extend_from_slice(&n.to_le_bytes());
out.extend_from_slice(&body);
Ok(out)
}
}
impl PcoDecoder {
pub fn decode_f64(data: &[u8]) -> Result<Vec<f64>> {
Self::decode(data)
}
pub fn decode_i64(data: &[u8]) -> Result<Vec<i64>> {
Self::decode(data)
}
pub fn decode_u64(data: &[u8]) -> Result<Vec<u64>> {
Self::decode(data)
}
fn decode<T: Number + Default + Copy>(data: &[u8]) -> Result<Vec<T>> {
if data.len() < HEADER {
return Err(EncodingError::CorruptData {
detail: "pco: truncated header".into(),
});
}
let n = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
if n == 0 || n > MAX_BLOCK_VALUES {
return Err(EncodingError::CorruptData {
detail: format!("pco: declared {n} values, ceiling is {MAX_BLOCK_VALUES}"),
});
}
let mut out = vec![T::default(); n];
let progress = simple_decompress_into(&data[HEADER..], &mut out).map_err(|e| {
EncodingError::CorruptData {
detail: format!("pco decode: {e}"),
}
})?;
if progress.n_processed != n || !progress.finished {
return Err(EncodingError::CountMismatch {
expected: n,
actual: progress.n_processed,
});
}
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn roundtrip_arbitrary_f64(values in prop::collection::vec(any::<f64>(), 1..300)) {
let decoded = PcoDecoder::decode_f64(&PcoEncoder::encode_f64(&values).unwrap()).unwrap();
prop_assert_eq!(decoded.len(), values.len());
for (a, b) in values.iter().zip(&decoded) {
prop_assert_eq!(a.to_bits(), b.to_bits());
}
}
#[test]
fn roundtrip_arbitrary_i64(values in prop::collection::vec(any::<i64>(), 1..300)) {
let decoded = PcoDecoder::decode_i64(&PcoEncoder::encode_i64(&values).unwrap()).unwrap();
prop_assert_eq!(decoded, values);
}
#[test]
fn roundtrip_arbitrary_u64(values in prop::collection::vec(any::<u64>(), 1..300)) {
let decoded = PcoDecoder::decode_u64(&PcoEncoder::encode_u64(&values).unwrap()).unwrap();
prop_assert_eq!(decoded, values);
}
#[test]
fn roundtrip_arbitrary_timestamps(values in prop::collection::vec(any::<i64>(), 1..300)) {
let decoded =
PcoDecoder::decode_i64(&PcoEncoder::encode_timestamps(&values).unwrap()).unwrap();
prop_assert_eq!(decoded, values);
}
#[test]
fn arbitrary_bytes_do_not_decode(data in prop::collection::vec(any::<u8>(), 0..512)) {
let _ = PcoDecoder::decode_f64(&data);
let _ = PcoDecoder::decode_i64(&data);
let _ = PcoDecoder::decode_u64(&data);
}
}
#[test]
fn round_trips_all_three_types() {
let f: Vec<f64> = (0..5000)
.map(|i| 230.0 + f64::from(i % 400) / 100.0)
.collect();
assert_eq!(
PcoDecoder::decode_f64(&PcoEncoder::encode_f64(&f).unwrap()).unwrap(),
f
);
let i: Vec<i64> = (0..5000).map(|i| 42_000 + i64::from(i) * 3).collect();
assert_eq!(
PcoDecoder::decode_i64(&PcoEncoder::encode_i64(&i).unwrap()).unwrap(),
i
);
let u: Vec<u64> = (0..5000u64).map(|i| i * 7).collect();
assert_eq!(
PcoDecoder::decode_u64(&PcoEncoder::encode_u64(&u).unwrap()).unwrap(),
u
);
let specials = vec![f64::NAN, f64::INFINITY, -0.0, f64::MIN_POSITIVE, 1e300];
let back = PcoDecoder::decode_f64(&PcoEncoder::encode_f64(&specials).unwrap()).unwrap();
for (a, b) in specials.iter().zip(&back) {
assert_eq!(a.to_bits(), b.to_bits());
}
}
#[test]
fn beats_alp_on_decimal_data() {
let f: Vec<f64> = (0..8192)
.map(|i| 230.0 + f64::from((i * 37) % 4000) / 100.0)
.collect();
let pco = PcoEncoder::encode_f64(&f).unwrap().len();
let alp = crate::alp::AlpEncoder::encode(&f).unwrap().len();
assert!(pco * 4 < alp, "pco {pco} B vs ALP {alp} B");
}
#[test]
fn a_corrupt_header_cannot_allocate_and_a_wrong_count_is_an_error() {
let f: Vec<f64> = (0..100).map(f64::from).collect();
let mut enc = PcoEncoder::encode_f64(&f).unwrap();
enc[..4].copy_from_slice(&u32::MAX.to_le_bytes());
assert!(matches!(
PcoDecoder::decode_f64(&enc),
Err(EncodingError::CorruptData { .. })
));
enc[..4].copy_from_slice(&50u32.to_le_bytes());
assert!(PcoDecoder::decode_f64(&enc).is_err());
enc[..4].copy_from_slice(&150u32.to_le_bytes());
assert!(PcoDecoder::decode_f64(&enc).is_err());
assert!(PcoDecoder::decode_f64(&[7, 0, 0, 0, 1, 2, 3]).is_err());
assert!(PcoDecoder::decode_f64(&[1, 0]).is_err());
}
}