use std::collections::HashMap;
use std::error::Error;
use std::fmt;
#[derive(Debug)]
pub enum DictError {
ExceedsDictSize(usize),
InvalidIndex(usize),
EmptyInput,
}
impl fmt::Display for DictError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DictError::ExceedsDictSize(size) => write!(f, "Dictionary size exceeds capacity: {}", size),
DictError::InvalidIndex(idx) => write!(f, "Invalid dictionary index: {}", idx),
DictError::EmptyInput => write!(f, "Empty input provided"),
}
}
}
impl Error for DictError {}
pub struct DictionaryEncoder {
dictionary: HashMap<String, u16>,
reverse_dict: Vec<String>,
encoded_values: Vec<u16>,
statistics: EncodingStatistics,
}
#[derive(Debug, Clone, Default)]
pub struct EncodingStatistics {
pub unique_values: usize,
pub total_values: usize,
pub cardinality_percent: f64,
pub original_bytes: usize,
pub encoded_bytes: usize,
}
impl DictionaryEncoder {
pub fn encode(values: &[String]) -> Result<Self, DictError> {
if values.is_empty() {
return Err(DictError::EmptyInput);
}
let mut dictionary = HashMap::new();
let mut reverse_dict = Vec::new();
let mut encoded_values = Vec::new();
let original_bytes: usize = values.iter().map(|s| s.len()).sum();
for value in values {
if !dictionary.contains_key(value) {
let idx = dictionary.len() as u16;
if idx >= u16::MAX {
return Err(DictError::ExceedsDictSize(dictionary.len()));
}
dictionary.insert(value.clone(), idx);
reverse_dict.push(value.clone());
}
encoded_values.push(dictionary[value]);
}
let unique_values = dictionary.len();
let cardinality_percent = (unique_values as f64 / values.len() as f64) * 100.0;
let encoded_bytes = encoded_values.len() * 2 + dictionary.values().map(|_| 1).sum::<usize>() * 2 + reverse_dict.iter().map(|s| s.len()).sum::<usize>();
let statistics = EncodingStatistics {
unique_values,
total_values: values.len(),
cardinality_percent,
original_bytes,
encoded_bytes,
};
Ok(Self {
dictionary,
reverse_dict,
encoded_values,
statistics,
})
}
pub fn indices(&self) -> &[u16] {
&self.encoded_values
}
pub fn dictionary(&self) -> &HashMap<String, u16> {
&self.dictionary
}
pub fn reverse_dictionary(&self) -> &[String] {
&self.reverse_dict
}
pub fn statistics(&self) -> &EncodingStatistics {
&self.statistics
}
pub fn compression_ratio(&self) -> f64 {
self.statistics.encoded_bytes as f64 / self.statistics.original_bytes as f64
}
pub fn serialize(&self) -> Vec<u8> {
let mut result = Vec::new();
result.extend_from_slice(&(self.reverse_dict.len() as u16).to_le_bytes());
for entry in &self.reverse_dict {
let bytes = entry.as_bytes();
result.extend_from_slice(&(bytes.len() as u16).to_le_bytes());
result.extend_from_slice(bytes);
}
result.extend_from_slice(&(self.encoded_values.len() as u32).to_le_bytes());
for idx in &self.encoded_values {
result.extend_from_slice(&idx.to_le_bytes());
}
result
}
}
pub struct DictionaryDecoder {
reverse_dict: Vec<String>,
encoded_values: Vec<u16>,
}
impl DictionaryDecoder {
pub fn deserialize(data: &[u8]) -> Result<Self, DictError> {
let mut pos = 0;
if data.len() < 2 {
return Err(DictError::InvalidIndex(0));
}
let num_entries = u16::from_le_bytes([data[pos], data[pos + 1]]) as usize;
pos += 2;
let mut reverse_dict = Vec::new();
for _ in 0..num_entries {
if pos + 2 > data.len() {
return Err(DictError::InvalidIndex(pos));
}
let str_len = u16::from_le_bytes([data[pos], data[pos + 1]]) as usize;
pos += 2;
if pos + str_len > data.len() {
return Err(DictError::InvalidIndex(pos));
}
let s = String::from_utf8_lossy(&data[pos..pos + str_len]).to_string();
reverse_dict.push(s);
pos += str_len;
}
if pos + 4 > data.len() {
return Err(DictError::InvalidIndex(pos));
}
let num_values = u32::from_le_bytes([
data[pos],
data[pos + 1],
data[pos + 2],
data[pos + 3],
]) as usize;
pos += 4;
let mut encoded_values = Vec::new();
for _ in 0..num_values {
if pos + 2 > data.len() {
return Err(DictError::InvalidIndex(pos));
}
let idx = u16::from_le_bytes([data[pos], data[pos + 1]]);
encoded_values.push(idx);
pos += 2;
}
Ok(Self {
reverse_dict,
encoded_values,
})
}
pub fn decode(&self) -> Result<Vec<String>, DictError> {
let mut result = Vec::new();
for &idx in &self.encoded_values {
let idx_usize = idx as usize;
if idx_usize >= self.reverse_dict.len() {
return Err(DictError::InvalidIndex(idx_usize));
}
result.push(self.reverse_dict[idx_usize].clone());
}
Ok(result)
}
pub fn decode_value(&self, index: u16) -> Result<String, DictError> {
let idx_usize = index as usize;
if idx_usize >= self.reverse_dict.len() {
return Err(DictError::InvalidIndex(idx_usize));
}
Ok(self.reverse_dict[idx_usize].clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dictionary_encoding() {
let values = vec![
"customer_1".to_string(),
"customer_2".to_string(),
"customer_1".to_string(),
"customer_3".to_string(),
"customer_1".to_string(),
];
let encoder = DictionaryEncoder::encode(&values).unwrap();
assert_eq!(encoder.statistics().unique_values, 3);
assert_eq!(encoder.statistics().total_values, 5);
assert_eq!(encoder.indices(), &[0, 1, 0, 2, 0]);
}
#[test]
fn test_dictionary_roundtrip() {
let values = vec![
"apple".to_string(),
"banana".to_string(),
"apple".to_string(),
"cherry".to_string(),
];
let encoder = DictionaryEncoder::encode(&values).unwrap();
let serialized = encoder.serialize();
let decoder = DictionaryDecoder::deserialize(&serialized).unwrap();
let decoded = decoder.decode().unwrap();
assert_eq!(decoded, values);
}
#[test]
fn test_compression_ratio() {
let values = vec!["same".to_string(); 1000]; let encoder = DictionaryEncoder::encode(&values).unwrap();
let ratio = encoder.compression_ratio();
assert!(ratio < 0.6, "Should achieve significant compression on identical data");
}
#[test]
fn test_empty_input() {
let values: Vec<String> = vec![];
let result = DictionaryEncoder::encode(&values);
assert!(matches!(result, Err(DictError::EmptyInput)));
}
}