Initial Commit

This commit is contained in:
Gregory Campbell
2026-06-19 12:54:57 -04:00
parent e87e12db90
commit e9afcd2dd0
7 changed files with 824 additions and 0 deletions
+166
View File
@@ -0,0 +1,166 @@
use bit_vec::BitVec;
use rayon::prelude::*;
use rmp_serde;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, hash::Hash};
use crate::huffman::{self, Tree};
use Tree::*;
#[derive(serde::Serialize, serde::Deserialize)]
struct CompressedData<T: Eq + Hash> {
encoder: HashMap<T, BitVec>,
data: Vec<BitVec>,
}
pub fn compress<'a, T, FreqsF, TokenExtractor, TokensIter>(
lines: &'a [String],
get_freqs: FreqsF,
line_to_tokens: TokenExtractor,
) -> Result<Vec<u8>, Box<dyn std::error::Error>>
where
T: Clone + Eq + Hash + Send + Sync + Serialize,
FreqsF: Fn(&'a [String]) -> HashMap<T, u64>,
TokenExtractor: Fn(&'a str) -> TokensIter + Send + Sync,
TokensIter: Iterator<Item = T>,
{
let freqs = get_freqs(lines);
let tree = huffman::huffman_tree(&freqs);
let encoder = tree.to_encoder();
let data = lines
.par_iter()
.map(|line| {
let mut bits = BitVec::new();
for token in line_to_tokens(line) {
bits.extend(encoder.get(&token).unwrap().iter());
}
bits
})
.collect();
let compressed_data = CompressedData { encoder, data };
rmp_serde::encode::to_vec(&compressed_data).map_err(|err| err.into())
}
pub fn extract<'a, T, F>(
data: &'a [u8],
tokens_to_line: F,
) -> Result<Vec<String>, Box<dyn std::error::Error>>
where
T: Clone + Eq + Hash + Send + Sync + Deserialize<'a>,
F: Fn(Vec<T>) -> String + Send + Sync,
{
let CompressedData { encoder, data }: CompressedData<T> = rmp_serde::decode::from_slice(data)?;
let decoder = build_decoder(&encoder);
let lines = data
.par_iter()
.map(|line| {
let mut tokens = Vec::new();
let mut node = &decoder;
for bit in line.iter() {
node = if bit {
node.right.as_ref().expect("invalid Huffman bit sequence")
} else {
node.left.as_ref().expect("invalid Huffman bit sequence")
};
if let Some(token) = &node.token {
tokens.push(token.clone());
node = &decoder;
}
}
tokens_to_line(tokens)
})
.collect();
Ok(lines)
}
struct DecoderNode<T> {
token: Option<T>,
left: Option<Box<DecoderNode<T>>>,
right: Option<Box<DecoderNode<T>>>,
}
impl<T> Default for DecoderNode<T> {
fn default() -> Self {
DecoderNode {
token: None,
left: None,
right: None,
}
}
}
fn build_decoder<T: Clone + Eq + Hash>(encoder: &HashMap<T, BitVec>) -> DecoderNode<T> {
let mut root = DecoderNode::default();
for (token, code) in encoder {
let mut node = &mut root;
for bit in code.iter() {
node = if bit {
node.right.get_or_insert_with(|| Box::new(DecoderNode::default()))
} else {
node.left.get_or_insert_with(|| Box::new(DecoderNode::default()))
};
}
node.token = Some(token.clone());
}
root
}
impl<T: Eq + Clone + Hash> Tree<T> {
pub fn to_encoder(&self) -> HashMap<T, BitVec> {
let mut encoder = HashMap::new();
let mut stack = vec![(self, BitVec::new())];
while let Some((node, path)) = stack.pop() {
match node {
Leaf { token, .. } => {
encoder.insert(token.clone(), path.clone());
}
Node { left, right, .. } => {
let mut left_path = path.clone();
left_path.push(false);
stack.push((left, left_path));
let mut right_path = path.clone();
right_path.push(true);
stack.push((right, right_path));
}
}
}
encoder
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::freq::{char_frequencies, word_frequencies};
#[test]
fn compress_decompress_test() {
let lines = vec![
"hey there! nice to meet you.".to_string(),
"Serde is a framework for serializing and deserializing Rust data structures"
.to_string(),
];
let data = compress(&lines, char_frequencies, |line| line.chars()).unwrap();
let res_lines = extract(&data, |x: Vec<char>| x.into_iter().collect()).unwrap();
assert_eq!(&lines, &res_lines);
let data = compress(&lines, word_frequencies, |line| {
line.split_ascii_whitespace().map(|token| token.to_string())
})
.unwrap();
let res_lines = extract(&data, |x: Vec<String>| x.join(" ")).unwrap();
assert_eq!(&lines, &res_lines);
}
}
+48
View File
@@ -0,0 +1,48 @@
use rayon::prelude::*;
use std::collections::HashMap;
pub fn char_frequencies(lines: &[String]) -> HashMap<char, u64> {
lines
.par_iter()
.fold(
HashMap::new,
|mut freqs: HashMap<_, _>, line: &String| {
for ch in line.chars() {
*freqs.entry(ch).or_insert(0) += 1;
}
freqs
},
)
.reduce(
HashMap::new,
|mut freqs1, freqs2| {
freqs2
.into_iter()
.for_each(|(ch, n)| *freqs1.entry(ch).or_insert(0) += n);
freqs1
},
)
}
pub fn word_frequencies(lines: &[String]) -> HashMap<String, u64> {
lines
.par_iter()
.fold(
HashMap::new,
|mut freqs: HashMap<_, _>, line: &String| {
for word in line.split_ascii_whitespace() {
*freqs.entry(word.to_string()).or_insert(0) += 1;
}
freqs
},
)
.reduce(
HashMap::new,
|mut freqs1, freqs2| {
freqs2
.into_iter()
.for_each(|(word, n)| *freqs1.entry(word).or_insert(0) += n);
freqs1
},
)
}
+158
View File
@@ -0,0 +1,158 @@
use std::{
cmp::Reverse,
collections::{BinaryHeap, HashMap},
};
use Tree::*;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Tree<T> {
Leaf {
freq: u64,
token: T,
},
Node {
freq: u64,
left: Box<Tree<T>>,
right: Box<Tree<T>>,
},
}
#[allow(dead_code)]
impl<T: Clone> Tree<T> {
pub fn freq(&self) -> u64 {
match self {
Leaf { freq, .. } => *freq,
Node { freq, .. } => *freq,
}
}
pub fn token(&self) -> Option<T> {
match self {
Leaf { token, .. } => Some(token.clone()),
Node { .. } => None,
}
}
pub fn left(&self) -> Option<&Tree<T>> {
match self {
Node { left, .. } => Some(left),
Leaf { .. } => None,
}
}
pub fn right(&self) -> Option<&Tree<T>> {
match self {
Node { right, .. } => Some(right),
Leaf { .. } => None,
}
}
}
impl<T: Clone + Eq> Ord for Tree<T> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.freq().cmp(&other.freq())
}
}
impl<T: Clone + Eq> PartialOrd for Tree<T> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
pub fn huffman_tree<T: Eq + Clone>(freqs: &HashMap<T, u64>) -> Tree<T> {
let mut heap = BinaryHeap::new();
for (token, freq) in freqs {
let (freq, token) = (*freq, token.clone());
heap.push(Reverse(Leaf { freq, token }))
}
while heap.len() > 1 {
let node1 = heap.pop().unwrap().0;
let node2 = heap.pop().unwrap().0;
let merged_node = Node {
freq: node1.freq() + node2.freq(),
left: Box::new(node1),
right: Box::new(node2),
};
heap.push(Reverse(merged_node));
}
heap.pop().unwrap().0
}
#[cfg(test)]
mod tests {
use super::*;
use crate::freq::char_frequencies;
#[test]
fn learn_frequencies_test() {
let input = vec!["this is an epic sentence".to_string(), "xyz ".to_string()];
let freqs = char_frequencies(&input);
assert_eq!(freqs[&' '], 5);
assert_eq!(freqs[&'t'], 2);
assert_eq!(freqs[&'i'], 3);
assert_eq!(freqs[&'p'], 1);
assert_eq!(freqs[&'z'], 1);
assert_eq!(freqs.keys().len(), 13);
}
#[test]
fn huffman_tree_test() {
let mut freqs = HashMap::new();
freqs.insert('a', 40);
freqs.insert('b', 35);
freqs.insert('c', 20);
freqs.insert('d', 5);
let tree = huffman_tree(&freqs);
assert_eq!(tree.freq(), 100);
// the most frequent character only requires 1 bit
assert_eq!(tree.left().and_then(|n| n.token()), Some('a'));
assert_eq!(tree.left().map(|n| n.freq()), Some(40));
// the second most frequent character requires 2 bits
assert_eq!(
tree.right().and_then(|t| t.right()).and_then(|n| n.token()),
Some('b')
);
assert_eq!(
tree.right().and_then(|t| t.right()).map(|n| n.freq()),
Some(35)
);
// the least frequent characters require 3 bits
assert_eq!(
tree.right()
.and_then(|t| t.left())
.and_then(|t| t.left())
.and_then(|n| n.token()),
Some('d')
);
assert_eq!(
tree.right()
.and_then(|t| t.left())
.and_then(|t| t.left())
.map(|n| n.freq()),
Some(5)
);
assert_eq!(
tree.right()
.and_then(|t| t.left())
.and_then(|t| t.right())
.and_then(|n| n.token()),
Some('c')
);
assert_eq!(
tree.right()
.and_then(|t| t.left())
.and_then(|t| t.right())
.map(|n| n.freq()),
Some(20)
);
}
}
+106
View File
@@ -0,0 +1,106 @@
mod compression;
mod freq;
mod huffman;
use clap::Parser;
use clap::ValueEnum;
use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;
use std::time;
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
#[arg(value_enum)]
action: Action,
#[arg(value_enum)]
mode: Mode,
input: PathBuf,
output: PathBuf,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum Action {
Compress,
Extract,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum Mode {
Words,
Chars,
}
// cargo run --release -- compress words data/wikisent2.txt data/words.huffman
// cargo run --release -- extract words data/words.huffman data/extracted.txt
// (to check if the extraction was correct: `diff data/wikisent2.txt data/extracted.txt`)
//
// cargo run --release -- compress chars data/wikisent2.txt data/chars.huffman
// cargo run --release -- extract chars data/chars.huffman data/extracted.txt
//
// to compare with zip:
// time zip data/test.zip data/wikisent2.txt
// time unzip data/test.zip -d data/test_zip
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
match args.action {
Action::Compress => {
let timer = time::Instant::now();
let text = fs::read_to_string(args.input)?;
let lines: Vec<_> = text.split('\n').map(|x| x.to_string()).collect();
let time = timer.elapsed();
let lines_count = lines.len();
println!("Read the source file with {lines_count} lines in {time:?}");
let timer = time::Instant::now();
let compressed = match args.mode {
Mode::Words => compression::compress(&lines, freq::word_frequencies, |line| {
line.split_ascii_whitespace().map(|token| token.to_string())
}),
Mode::Chars => {
compression::compress(&lines, freq::char_frequencies, |line| line.chars())
}
}?;
let time = timer.elapsed();
println!("Compressed as {mode:?} in {time:?}.", mode = args.mode);
let timer = time::Instant::now();
let mut out_f = File::create(&args.output)?;
out_f.write_all(&compressed)?;
let time = timer.elapsed();
println!(
"Wrote to {output_path:?} in {time:?}",
output_path = args.output
);
}
Action::Extract => {
let timer = time::Instant::now();
let data = fs::read(&args.input)?;
let time = timer.elapsed();
println!("Read the compressed file in {time:?}");
let timer = time::Instant::now();
let content = match args.mode {
Mode::Words => compression::extract(&data, |tokens: Vec<String>| tokens.join(" "))?,
Mode::Chars => {
compression::extract(&data, |tokens: Vec<char>| tokens.into_iter().collect())?
}
};
let time = timer.elapsed();
let lines_count = content.len();
println!("Extracted file with {lines_count} lines in {time:?}.");
let timer = time::Instant::now();
fs::write(&args.output, content.join("\n"))?;
let time = timer.elapsed();
println!(
"Wrote to {output_path:?} in {time:?}",
output_path = args.output
);
}
}
Ok(())
}